cloudflare-next-intl 0.7.7 → 0.8.0
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 +141 -0
- package/bin/db_codegen.mjs +110 -0
- package/dist/src/db/access_token.d.ts +14 -0
- package/dist/src/db/access_token.js +30 -0
- package/dist/src/db/codegen_paths.d.ts +13 -0
- package/dist/src/db/codegen_paths.js +32 -0
- package/dist/src/db/connection.d.ts +42 -0
- package/dist/src/db/connection.js +178 -0
- package/dist/src/db/context.d.ts +57 -0
- package/dist/src/db/context.js +125 -0
- package/dist/src/db/helpers.d.ts +57 -0
- package/dist/src/db/helpers.js +112 -0
- package/dist/src/db/index.d.ts +24 -0
- package/dist/src/db/index.js +22 -0
- package/dist/src/db/require_config.d.ts +15 -0
- package/dist/src/db/require_config.js +20 -0
- package/dist/src/db/resolve_mode.d.ts +17 -0
- package/dist/src/db/resolve_mode.js +18 -0
- package/dist/src/db/supabase_config.d.ts +18 -0
- package/dist/src/db/supabase_config.js +22 -0
- package/dist/src/db/supabase_transport.d.ts +29 -0
- package/dist/src/db/supabase_transport.js +49 -0
- package/dist/src/firebase_auth/server/mint_server_app_check_token.d.ts +22 -14
- package/dist/src/firebase_auth/server/mint_server_app_check_token.js +53 -25
- package/dist/src/firebase_auth/server/sign_custom_token_remote.d.ts +18 -0
- package/dist/src/firebase_auth/server/sign_custom_token_remote.js +39 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types/types.d.ts +122 -8
- package/llms.txt +35 -0
- package/package.json +22 -5
- package/supabase/cfni_exec.sql +34 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import config from '@intl-config';
|
|
2
2
|
import reportError from '../../error_handling/report_error';
|
|
3
|
+
import signCustomTokenRemote from './sign_custom_token_remote';
|
|
3
4
|
// Matches `firebase-admin`'s own `AppCheckTokenGenerator.createCustomToken`
|
|
4
5
|
// exactly (`token-generator.js`) — this specific audience (the App Check
|
|
5
6
|
// TOKEN EXCHANGE service, not the App Check API resource name itself) is
|
|
@@ -21,36 +22,63 @@ const CUSTOM_TOKEN_LIFETIME = '5m';
|
|
|
21
22
|
* Mints a fresh App Check token server-side via a service account, for use
|
|
22
23
|
* when the client-written App Check cookie (see `appCheckTokenCookieName`)
|
|
23
24
|
* is absent — e.g. a cold navigation before `AuthUserProvider` has run and
|
|
24
|
-
* had a chance to write it. Requires `clientEmail`/`
|
|
25
|
-
* `firebaseAuth.appCheck
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* had a chance to write it. Requires `clientEmail`/`appId` on
|
|
26
|
+
* `firebaseAuth.appCheck`, plus either `privateKey` or the
|
|
27
|
+
* `oauthClientId`/`oauthClientSecret`/`oauthRefreshToken` triple; returns
|
|
28
|
+
* `undefined` (never throws) if the exchange fails, so a caller can always
|
|
29
|
+
* fall back to "no App Check token" exactly as before this existed.
|
|
28
30
|
*
|
|
29
|
-
* Signs a short-lived custom JWT
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
31
|
+
* Signs a short-lived custom JWT, then exchanges it for an App Check token
|
|
32
|
+
* via `exchangeCustomToken`, authenticated with the project's Web API key
|
|
33
|
+
* (`?key=`) — `exchangeCustomToken` otherwise rejects the call outright as
|
|
34
|
+
* an unregistered/unidentified caller (403 `PERMISSION_DENIED`), before the
|
|
35
|
+
* custom token itself is even evaluated. Not cached beyond the caller's own
|
|
36
|
+
* request-scoped `cache()` wrapper — a fresh mint costs one signing
|
|
37
|
+
* operation plus one network round-trip, acceptable per-request but not
|
|
38
|
+
* worth doing more than once per request.
|
|
39
|
+
*
|
|
40
|
+
* The custom token is signed one of two ways, `privateKey` taking priority
|
|
41
|
+
* when both are set:
|
|
42
|
+
* - `privateKey` set: signed locally (`jose`, Edge/WebCrypto-compatible —
|
|
43
|
+
* no `firebase-admin`).
|
|
44
|
+
* - OAuth triple set instead: signed remotely via
|
|
45
|
+
* `sign_custom_token_remote.ts` (IAM Credentials `signJwt`) — the way to
|
|
46
|
+
* mint tokens when a GCP org policy blocks creating the service-account
|
|
47
|
+
* key `privateKey` would otherwise require.
|
|
39
48
|
*/
|
|
40
49
|
export default async function mintServerAppCheckToken(projectId, apiKey, appCheck) {
|
|
41
|
-
if (!appCheck?.clientEmail || !appCheck.
|
|
50
|
+
if (!appCheck?.clientEmail || !appCheck.appId)
|
|
51
|
+
return undefined;
|
|
52
|
+
const hasOauthTriple = appCheck.oauthClientId && appCheck.oauthClientSecret && appCheck.oauthRefreshToken;
|
|
53
|
+
if (!appCheck.privateKey && !hasOauthTriple)
|
|
42
54
|
return undefined;
|
|
43
55
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.
|
|
49
|
-
.
|
|
50
|
-
.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
const claims = {
|
|
57
|
+
iss: appCheck.clientEmail,
|
|
58
|
+
sub: appCheck.clientEmail,
|
|
59
|
+
aud: APP_CHECK_CUSTOM_TOKEN_AUDIENCE,
|
|
60
|
+
iat: Math.floor(Date.now() / 1000),
|
|
61
|
+
exp: Math.floor(Date.now() / 1000) + 300,
|
|
62
|
+
app_id: appCheck.appId,
|
|
63
|
+
};
|
|
64
|
+
const customToken = appCheck.privateKey
|
|
65
|
+
? await (async () => {
|
|
66
|
+
const { SignJWT, importPKCS8 } = await import('jose');
|
|
67
|
+
const privateKey = await importPKCS8(appCheck.privateKey.replace(/\\n/g, '\n'), 'RS256');
|
|
68
|
+
return new SignJWT({ app_id: appCheck.appId })
|
|
69
|
+
.setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
|
|
70
|
+
.setIssuer(appCheck.clientEmail)
|
|
71
|
+
.setSubject(appCheck.clientEmail)
|
|
72
|
+
.setAudience(APP_CHECK_CUSTOM_TOKEN_AUDIENCE)
|
|
73
|
+
.setIssuedAt()
|
|
74
|
+
.setExpirationTime(CUSTOM_TOKEN_LIFETIME)
|
|
75
|
+
.sign(privateKey);
|
|
76
|
+
})()
|
|
77
|
+
: await signCustomTokenRemote(appCheck.clientEmail, claims, {
|
|
78
|
+
clientId: appCheck.oauthClientId,
|
|
79
|
+
clientSecret: appCheck.oauthClientSecret,
|
|
80
|
+
refreshToken: appCheck.oauthRefreshToken,
|
|
81
|
+
});
|
|
54
82
|
const url = `https://firebaseappcheck.googleapis.com/v1/projects/${projectId}/apps/${appCheck.appId}:exchangeCustomToken?key=${apiKey}`;
|
|
55
83
|
const res = await fetch(url, {
|
|
56
84
|
method: 'POST',
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signs the App Check custom token claims remotely via IAM Credentials'
|
|
3
|
+
* `signJwt`, instead of locally with a service-account private key. Lets
|
|
4
|
+
* `mintServerAppCheckToken` work under an org policy that enforces
|
|
5
|
+
* `iam.disableServiceAccountKeyCreation` — that constraint blocks
|
|
6
|
+
* `serviceAccounts.keys.create` only; it does not affect `signJwt`, which
|
|
7
|
+
* signs using a key Google holds and never exports.
|
|
8
|
+
*
|
|
9
|
+
* The caller's OAuth identity (the refresh token) must carry
|
|
10
|
+
* `roles/iam.serviceAccountTokenCreator` on `clientEmail` — grant it with:
|
|
11
|
+
* `gcloud iam service-accounts add-iam-policy-binding <clientEmail>
|
|
12
|
+
* --member="user:<you>" --role="roles/iam.serviceAccountTokenCreator"`.
|
|
13
|
+
*/
|
|
14
|
+
export default function signCustomTokenRemote(clientEmail: string, claims: Record<string, unknown>, oauth: {
|
|
15
|
+
clientId: string;
|
|
16
|
+
clientSecret: string;
|
|
17
|
+
refreshToken: string;
|
|
18
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signs the App Check custom token claims remotely via IAM Credentials'
|
|
3
|
+
* `signJwt`, instead of locally with a service-account private key. Lets
|
|
4
|
+
* `mintServerAppCheckToken` work under an org policy that enforces
|
|
5
|
+
* `iam.disableServiceAccountKeyCreation` — that constraint blocks
|
|
6
|
+
* `serviceAccounts.keys.create` only; it does not affect `signJwt`, which
|
|
7
|
+
* signs using a key Google holds and never exports.
|
|
8
|
+
*
|
|
9
|
+
* The caller's OAuth identity (the refresh token) must carry
|
|
10
|
+
* `roles/iam.serviceAccountTokenCreator` on `clientEmail` — grant it with:
|
|
11
|
+
* `gcloud iam service-accounts add-iam-policy-binding <clientEmail>
|
|
12
|
+
* --member="user:<you>" --role="roles/iam.serviceAccountTokenCreator"`.
|
|
13
|
+
*/
|
|
14
|
+
export default async function signCustomTokenRemote(clientEmail, claims, oauth) {
|
|
15
|
+
const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
18
|
+
body: new URLSearchParams({
|
|
19
|
+
client_id: oauth.clientId,
|
|
20
|
+
client_secret: oauth.clientSecret,
|
|
21
|
+
refresh_token: oauth.refreshToken,
|
|
22
|
+
grant_type: 'refresh_token',
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
if (!tokenRes.ok) {
|
|
26
|
+
throw new Error(`oauth2 refresh_token exchange failed: ${tokenRes.status} ${await tokenRes.text()}`);
|
|
27
|
+
}
|
|
28
|
+
const { access_token: accessToken } = await tokenRes.json();
|
|
29
|
+
const signRes = await fetch(`https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${clientEmail}:signJwt`, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
|
32
|
+
body: JSON.stringify({ payload: JSON.stringify(claims) }),
|
|
33
|
+
});
|
|
34
|
+
if (!signRes.ok) {
|
|
35
|
+
throw new Error(`iamcredentials.signJwt failed: ${signRes.status} ${await signRes.text()}`);
|
|
36
|
+
}
|
|
37
|
+
const { signedJwt } = await signRes.json();
|
|
38
|
+
return signedJwt;
|
|
39
|
+
}
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
|
@@ -85,6 +85,14 @@ export interface RoutingConfig<AppLocales extends Locales, AppLocalePrefixMode e
|
|
|
85
85
|
* if this field is missing at call time rather than silently no-op'ing.
|
|
86
86
|
*/
|
|
87
87
|
firebaseAuth?: FirebaseAuthRoutingConfig;
|
|
88
|
+
/**
|
|
89
|
+
* Configures the optional `db` submodule (Postgres/Drizzle access over a
|
|
90
|
+
* Cloudflare Hyperdrive or plain connection string). Omit entirely to keep
|
|
91
|
+
* it fully disabled — no file in this package imports `pg`/`drizzle-orm`
|
|
92
|
+
* unless a `db` export is actually called, and every such export throws a
|
|
93
|
+
* clear error if this field is missing at call time.
|
|
94
|
+
*/
|
|
95
|
+
db?: DbRoutingConfig;
|
|
88
96
|
/**
|
|
89
97
|
* Configures the optional `cookie_consent` submodule (cookie-consent +
|
|
90
98
|
* privacy-policy-update banners). Omit entirely to keep it disabled —
|
|
@@ -621,9 +629,10 @@ export interface FirebaseAppCheckConfig {
|
|
|
621
629
|
* Service account client email, used ONLY server-side to mint an App
|
|
622
630
|
* Check token when the client-written App Check cookie is absent (e.g.
|
|
623
631
|
* a cold navigation before `AuthUserProvider` has run — see
|
|
624
|
-
* `appCheckTokenCookieName`). Required alongside `
|
|
625
|
-
* `
|
|
626
|
-
*
|
|
632
|
+
* `appCheckTokenCookieName`). Required alongside `appId`, plus either
|
|
633
|
+
* `privateKey` or the `oauthClientId`/`oauthClientSecret`/
|
|
634
|
+
* `oauthRefreshToken` triple, for server-side minting. Never sent to the
|
|
635
|
+
* client — read only by `firebase_server.ts`.
|
|
627
636
|
*/
|
|
628
637
|
clientEmail: string;
|
|
629
638
|
/**
|
|
@@ -633,13 +642,35 @@ export interface FirebaseAppCheckConfig {
|
|
|
633
642
|
* (e.g. `process.env.FIREBASE_PRIVATE_KEY`), never exposed to the
|
|
634
643
|
* browser. Escaped `\n` sequences (common when stored in a single-line
|
|
635
644
|
* env var) are unescaped automatically before use.
|
|
636
|
-
|
|
637
|
-
|
|
645
|
+
*
|
|
646
|
+
* Omit this and set the `oauthClientId`/`oauthClientSecret`/
|
|
647
|
+
* `oauthRefreshToken` triple instead when your GCP org enforces
|
|
648
|
+
* `iam.disableServiceAccountKeyCreation`, which blocks issuing this key
|
|
649
|
+
* in the first place. When both are set, `privateKey` takes priority.
|
|
650
|
+
*/
|
|
651
|
+
privateKey?: string;
|
|
652
|
+
/**
|
|
653
|
+
* Application Default Credentials OAuth client ID — the `client_id`
|
|
654
|
+
* field from `application_default_credentials.json` (see
|
|
655
|
+
* `gcloud auth application-default login`). Paired with
|
|
656
|
+
* `oauthClientSecret` and `oauthRefreshToken` as an alternative to
|
|
657
|
+
* `privateKey`: instead of signing the App Check custom token locally,
|
|
658
|
+
* it's signed remotely via IAM Credentials `signJwt`, authenticated as
|
|
659
|
+
* this OAuth identity. That identity needs
|
|
660
|
+
* `roles/iam.serviceAccountTokenCreator` on `clientEmail`. Use this when
|
|
661
|
+
* a service-account key can't be created (see `privateKey`). Ignored
|
|
662
|
+
* when `privateKey` is set.
|
|
663
|
+
*/
|
|
664
|
+
oauthClientId?: string;
|
|
665
|
+
/** OAuth client secret paired with `oauthClientId`. Same ADC-JSON `client_secret` field, same secret-handling rules as `privateKey`. */
|
|
666
|
+
oauthClientSecret?: string;
|
|
667
|
+
/** OAuth refresh token paired with `oauthClientId`. Same ADC-JSON `refresh_token` field, same secret-handling rules as `privateKey`. */
|
|
668
|
+
oauthRefreshToken?: string;
|
|
638
669
|
/**
|
|
639
670
|
* Firebase App Check app ID (e.g. `"1:1234567890:web:abcdef123456"`),
|
|
640
|
-
* required alongside `clientEmail
|
|
641
|
-
*
|
|
642
|
-
*
|
|
671
|
+
* required alongside `clientEmail` for server-side minting. Distinct
|
|
672
|
+
* from the Firebase Auth `appId` on `FirebaseAuthRoutingConfig` itself —
|
|
673
|
+
* App Check registers apps separately.
|
|
643
674
|
*/
|
|
644
675
|
appId: string;
|
|
645
676
|
}
|
|
@@ -786,3 +817,86 @@ export interface IntlSitemap {
|
|
|
786
817
|
lastModified: Date | string | undefined;
|
|
787
818
|
videos?: Videos[] | undefined;
|
|
788
819
|
}
|
|
820
|
+
export interface SupabaseDbConfig {
|
|
821
|
+
/**
|
|
822
|
+
* Supabase project URL, e.g. `https://abc.supabase.co`. Defaults to
|
|
823
|
+
* `process.env.NEXT_PUBLIC_SUPABASE_URL`.
|
|
824
|
+
*/
|
|
825
|
+
url?: string;
|
|
826
|
+
/**
|
|
827
|
+
* Supabase anon (publishable) key. Defaults to
|
|
828
|
+
* `process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY`. This is the only key the
|
|
829
|
+
* `db` module ever needs — never put a service-role key here.
|
|
830
|
+
*/
|
|
831
|
+
anonKey?: string;
|
|
832
|
+
/**
|
|
833
|
+
* Name of the Postgres function that runs the generated SQL. Defaults to
|
|
834
|
+
* `'cfni_exec'` — the function shipped in `supabase/cfni_exec.sql`.
|
|
835
|
+
*/
|
|
836
|
+
execFunction?: string;
|
|
837
|
+
}
|
|
838
|
+
export interface DbRoutingConfig {
|
|
839
|
+
/**
|
|
840
|
+
* Postgres connection string. Omit to resolve it from the Cloudflare
|
|
841
|
+
* Hyperdrive binding named by `hyperdriveBinding` instead (the normal
|
|
842
|
+
* production setup); a value here always wins over the binding, which is
|
|
843
|
+
* what makes local dev / build-time evaluation work.
|
|
844
|
+
*/
|
|
845
|
+
connectionString?: string;
|
|
846
|
+
/**
|
|
847
|
+
* Name of the Hyperdrive binding on `env` whose `connectionString` is used
|
|
848
|
+
* when `connectionString` is not set. Defaults to `'HYPERDRIVE'`. Requires
|
|
849
|
+
* `generate.getCloudflareContext` to be configured.
|
|
850
|
+
*/
|
|
851
|
+
hyperdriveBinding?: string;
|
|
852
|
+
/**
|
|
853
|
+
* Whether the pooled client is closed once the last in-flight
|
|
854
|
+
* `withPublicDb`/`withUserDb` call of the request finishes.
|
|
855
|
+
* Defaults to `true` (one connection per request, released to Hyperdrive
|
|
856
|
+
* immediately). Set `false` to keep the connection open for the lifetime
|
|
857
|
+
* of the isolate — faster for a long-lived server, but it holds a
|
|
858
|
+
* Hyperdrive connection slot between requests.
|
|
859
|
+
*/
|
|
860
|
+
disconnectAfterRequest?: boolean;
|
|
861
|
+
/**
|
|
862
|
+
* Postgres role assumed inside `withUserDb`'s transaction. Defaults
|
|
863
|
+
* to `'authenticated'` (the Supabase RLS convention).
|
|
864
|
+
*/
|
|
865
|
+
authenticatedRole?: string;
|
|
866
|
+
/**
|
|
867
|
+
* Resolves the user id injected as `request.jwt.claims->>'sub'` inside
|
|
868
|
+
* `withUserDb`. Omit when `firebaseAuth` is configured — the uid then
|
|
869
|
+
* comes from this package's own `getAuthUser()` automatically. Provide it
|
|
870
|
+
* to use a different auth source (or when `firebaseAuth` is absent).
|
|
871
|
+
*/
|
|
872
|
+
getUserId?: () => Promise<string | null> | string | null;
|
|
873
|
+
/** Milliseconds `disconnectPostgres` waits for `client.end()` before giving up. Defaults to `2000`. */
|
|
874
|
+
disconnectTimeoutMs?: number;
|
|
875
|
+
/**
|
|
876
|
+
* Reaches Postgres through the Supabase Data API instead of a direct
|
|
877
|
+
* connection, using only your project URL and anon key. Set this when you
|
|
878
|
+
* have no Postgres password to give the package — `withPublicDb` and
|
|
879
|
+
* `withUserDb` behave the same either way, so switching is a config change
|
|
880
|
+
* with no app-code change.
|
|
881
|
+
*
|
|
882
|
+
* Ignored when `connectionString` or `hyperdriveBinding` is set: a direct
|
|
883
|
+
* connection always wins, so adding this block cannot silently reroute
|
|
884
|
+
* live traffic. Requires the `cfni_exec` function from
|
|
885
|
+
* `supabase/cfni_exec.sql` to be installed in your database.
|
|
886
|
+
*
|
|
887
|
+
* No multi-statement transactions: unlike connection-string mode, each
|
|
888
|
+
* statement inside a `withUserDb` callback is its own round-trip. Do not
|
|
889
|
+
* rely on multi-statement atomicity in this mode.
|
|
890
|
+
*/
|
|
891
|
+
supabase?: SupabaseDbConfig;
|
|
892
|
+
/**
|
|
893
|
+
* Resolves the JWT sent as `Authorization: Bearer` for `withUserDb` in
|
|
894
|
+
* Supabase mode, which is what makes PostgREST resolve the caller as
|
|
895
|
+
* `authenticated` and apply RLS. Omit when `firebaseAuth` is configured —
|
|
896
|
+
* the signed-in user's Firebase ID token is then used automatically.
|
|
897
|
+
*
|
|
898
|
+
* Unused in connection-string mode, which identifies the user with
|
|
899
|
+
* `getUserId` and `set_config` instead.
|
|
900
|
+
*/
|
|
901
|
+
getAccessToken?: () => Promise<string | null> | string | null;
|
|
902
|
+
}
|
package/llms.txt
CHANGED
|
@@ -23,6 +23,8 @@ other subpath can be used.
|
|
|
23
23
|
- `./localeStaticParams` — `generateStaticParams` helper for locale segments.
|
|
24
24
|
- `./use` — `useLocale`/`useTranslations`; resolves to the RSC or client implementation automatically via the `react-server` export condition. Both throw `"... must be used within an IntlProvider"` if called outside one.
|
|
25
25
|
- `./ThemeSwitcher` — optional light/dark toggle component.
|
|
26
|
+
- `./db` — `withPublicDb(fn)` / `withUserDb(fn, uid?)` server-side Postgres/Drizzle context helpers (require `db` set on your `RoutingConfig`; direct Postgres or Supabase Data API, see below).
|
|
27
|
+
- `./dbHelpers` — generic Drizzle SQL helper functions (`excluded`, `onConflictSet`, `ago`, `currentDate`, `windowCount`, `unnestLateral`, `ascNullsLast`, `alwaysTrue`, `lateral`, `aliasColumn`, `minOf`, `maxOf`, `roundReal`, `multiply`, `scalarFromCte`) for use with `./db`.
|
|
26
28
|
|
|
27
29
|
## `firebaseAuth*` subpaths (require `firebaseAuth` set on your `RoutingConfig`)
|
|
28
30
|
|
|
@@ -44,6 +46,38 @@ other subpath can be used.
|
|
|
44
46
|
- `./cookieConsentAnalytics` — `CookieConsentAnalytics`: gates Cloudflare Web Analytics / Google Ads / Google Analytics / AdSense / Microsoft Clarity behind consent; rendered automatically by `IntlProvider` when `cookieConsent.secrets` or `getSecrets` is set (and `autoWireAnalytics !== false`). Never renders in local dev (`NODE_ENV === 'development'`) unless `cookieConsent.enableAnalyticsInDevMode` is `true`.
|
|
45
47
|
- Country-based gating (`cookieConsent.getCountryCode` / `getCloudflareContext` + `gdprCountries`): resolved server-side by `IntlProvider` into a `requiresConsent` boolean passed to `CookieConsentProvider`. Neither getter set → gating off, consent always implicitly granted. `getCountryCode` (direct country resolver) takes precedence over `getCloudflareContext` (reads `cf.country`) when both are set. `getCloudflareContext` accepts `@opennextjs/cloudflare`'s `getCloudflareContext` function directly (its exact overloaded signature — `CookieConsentGetCloudflareContext`), called internally with `{ async: true }`. Unresolved country (or a `null` context) always requires consent (fail-safe). `./cookieConsent` also exports `defaultGdprCountries` (EU/EEA + UK + Switzerland).
|
|
46
48
|
|
|
49
|
+
## `db*` subpaths (require `db` set on your `RoutingConfig`)
|
|
50
|
+
|
|
51
|
+
Two transports, picked by which `db` fields are set — `pg`/`drizzle-orm`/`@supabase/supabase-js` ship as dependencies and load via dynamic `import()`, so nothing bundles unless a `db` export is called.
|
|
52
|
+
|
|
53
|
+
- Direct Postgres (wins if configured): `db.connectionString` — Postgres connection string; when omitted, resolved from the Hyperdrive binding named by `db.hyperdriveBinding` (default `'HYPERDRIVE'`) via `generate.getCloudflareContext`. An explicit `connectionString` always wins (enables local dev / build-time evaluation).
|
|
54
|
+
- Supabase Data API (used only when neither of the above is set): `db.supabase` — `{ url?, anonKey?, execFunction? }`, defaulting `url`/`anonKey` to `NEXT_PUBLIC_SUPABASE_URL`/`NEXT_PUBLIC_SUPABASE_ANON_KEY`. Requires installing `supabase/cfni_exec.sql` (a `security invoker` SQL-exec function) in your database. No multi-statement transactions — each statement in a `withUserDb` callback is its own round-trip.
|
|
55
|
+
- `db.disconnectAfterRequest` — direct-Postgres mode only: closes the pooled client once the last in-flight `withPublicDb`/`withUserDb` call of the request finishes. Defaults to `true`; set `false` to keep the connection open for the isolate's lifetime (holds a Hyperdrive slot between requests).
|
|
56
|
+
- `db.authenticatedRole` — direct-Postgres mode only: Postgres role assumed inside `withUserDb`'s transaction. Defaults to `'authenticated'` (Supabase RLS convention).
|
|
57
|
+
- `db.getUserId` — direct-Postgres mode only: resolves the user id injected as `request.jwt.claims->>'sub'` in `withUserDb`. Omit when `firebaseAuth` is configured — the uid is then taken automatically from the signed-in Firebase user via this package's own `getAuthUser()`.
|
|
58
|
+
- `db.getAccessToken` — Supabase mode only: resolves the JWT sent as `Authorization: Bearer` in `withUserDb`, which is what makes PostgREST resolve `authenticated` and apply RLS. Omit when `firebaseAuth` is configured — the signed-in user's Firebase ID token is used automatically.
|
|
59
|
+
- `db.disconnectTimeoutMs` — direct-Postgres mode only: ms `disconnectPostgres` waits for `client.end()` before giving up. Defaults to `2000`.
|
|
60
|
+
- `withPublicDb(fn)` — anonymous role. Direct-Postgres mode: the request's pooled connection, no transaction, no role switch. Supabase mode: the anon key as the PostgREST bearer token. Either way, no user id is attached — RLS keyed on `auth.jwt()` denies access.
|
|
61
|
+
- `withUserDb(fn, uid?)` — signed-in-user role. Direct-Postgres mode: a transaction with `set_config('request.jwt.claims', ...)` + `set local role`, `uid` resolution order explicit arg → `db.getUserId()` → Firebase auth uid → throws. Supabase mode: identity rides on the JWT from `db.getAccessToken`/Firebase instead (`uid` param is ignored), no transaction wraps the call.
|
|
62
|
+
- `./dbHelpers` functions are plain Drizzle `sql`-building utilities with no config dependency — usable standalone.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// src/i18n/intl_config.ts
|
|
66
|
+
import { getCloudflareContext } from "@opennextjs/cloudflare";
|
|
67
|
+
|
|
68
|
+
export default setIntlConfig({
|
|
69
|
+
locales: ["en", "uk"] as const,
|
|
70
|
+
defaultLocale: "en",
|
|
71
|
+
generate: { getCloudflareContext },
|
|
72
|
+
db: { hyperdriveBinding: "HYPERDRIVE" },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// anywhere on the server
|
|
76
|
+
import { withPublicDb } from "cloudflare-next-intl/db";
|
|
77
|
+
|
|
78
|
+
const rows = await withPublicDb((db) => db.select().from(bonds).limit(10));
|
|
79
|
+
```
|
|
80
|
+
|
|
47
81
|
## Conventions
|
|
48
82
|
|
|
49
83
|
- Every exported function/component has a JSDoc comment with an `@example` where usage isn't obvious from the signature alone.
|
|
@@ -56,3 +90,4 @@ other subpath can be used.
|
|
|
56
90
|
- Missing `firebaseAuth` on `RoutingConfig` throws immediately from any `firebaseAuth*` export, naming the missing config field.
|
|
57
91
|
- `useLocale`/`useTranslations` (from `./use`) and `useAuthUser` (from `./useFirebaseAuthUser`) resolve to a Server- or Client-Component implementation automatically via the `react-server` export condition — always import from the subpath, never from an internal file path.
|
|
58
92
|
- No unified `useFirebaseAuthUser` re-export inside `cloudflare-next-intl/firebaseAuth`'s barrel — that barrel can't replicate the `react-server` condition split, so import `cloudflare-next-intl/useFirebaseAuthUser` directly for the hook.
|
|
93
|
+
- Missing `db` on `RoutingConfig` throws immediately from `withPublicDb`/`withUserDb`, naming the missing config field. `pg`, `drizzle-orm`, and `@supabase/supabase-js` ship as dependencies and load via dynamic `import()` — nothing to install yourself, and nothing bundles unless a `db` export is called.
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudflare-next-intl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Optimized Next Intl Package Special for App Router and Cloudflare",
|
|
5
|
-
|
|
6
5
|
"main": "dist/index.js",
|
|
7
6
|
"types": "dist/index.d.ts",
|
|
8
7
|
"sideEffects": false,
|
|
8
|
+
"bin": {
|
|
9
|
+
"cfni-db-codegen": "bin/db_codegen.mjs"
|
|
10
|
+
},
|
|
9
11
|
"files": [
|
|
10
12
|
"dist",
|
|
13
|
+
"bin",
|
|
14
|
+
"supabase",
|
|
11
15
|
"LICENSE",
|
|
12
16
|
"README.md",
|
|
13
17
|
"llms.txt"
|
|
@@ -172,6 +176,14 @@
|
|
|
172
176
|
"./createServerErrorAction": {
|
|
173
177
|
"types": "./dist/src/error_handling/create_server_error_action.d.ts",
|
|
174
178
|
"import": "./dist/src/error_handling/create_server_error_action.js"
|
|
179
|
+
},
|
|
180
|
+
"./db": {
|
|
181
|
+
"types": "./dist/src/db/index.d.ts",
|
|
182
|
+
"import": "./dist/src/db/index.js"
|
|
183
|
+
},
|
|
184
|
+
"./dbHelpers": {
|
|
185
|
+
"types": "./dist/src/db/helpers.d.ts",
|
|
186
|
+
"import": "./dist/src/db/helpers.js"
|
|
175
187
|
}
|
|
176
188
|
},
|
|
177
189
|
"scripts": {
|
|
@@ -216,8 +228,11 @@
|
|
|
216
228
|
"homepage": "https://github.com/demian-ilnytskyi/cloudflare-next-intl#readme",
|
|
217
229
|
"dependencies": {
|
|
218
230
|
"@microsoft/clarity": "^1.0.2",
|
|
231
|
+
"@supabase/supabase-js": "^2.112.3",
|
|
232
|
+
"drizzle-orm": "^0.45.2",
|
|
219
233
|
"firebase": "^12.17.0",
|
|
220
|
-
"jose": "^6.2.8"
|
|
234
|
+
"jose": "^6.2.8",
|
|
235
|
+
"pg": "^8.23.0"
|
|
221
236
|
},
|
|
222
237
|
"peerDependencies": {
|
|
223
238
|
"next": ">=12.0.0",
|
|
@@ -236,16 +251,18 @@
|
|
|
236
251
|
"@testing-library/jest-dom": "^7.0.0",
|
|
237
252
|
"@testing-library/react": "^16.3.2",
|
|
238
253
|
"@types/node": "^20.14.5",
|
|
254
|
+
"@types/pg": "^8.23.1",
|
|
239
255
|
"@types/react": "^19.0.0",
|
|
240
256
|
"@types/react-dom": "^19.0.0",
|
|
241
257
|
"@vitest/coverage-v8": "^3.2.7",
|
|
258
|
+
"drizzle-kit": "^0.31.10",
|
|
242
259
|
"eslint": "^9.28.0",
|
|
243
260
|
"eslint-config-next": "15.3.3",
|
|
244
261
|
"eslint-config-prettier": "^10.1.2",
|
|
245
262
|
"jsdom": "^29.1.1",
|
|
246
263
|
"next": "^15.3.0",
|
|
247
|
-
"react": "
|
|
248
|
-
"react-dom": "
|
|
264
|
+
"react": "19.2.0",
|
|
265
|
+
"react-dom": "19.2.0",
|
|
249
266
|
"tsup": "^8.5.1",
|
|
250
267
|
"typescript": "^5.5.3",
|
|
251
268
|
"typescript-eslint": "^8.33.1",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- Executes a statement generated by cloudflare-next-intl's `db` module and
|
|
2
|
+
-- returns its rows as positional JSON arrays, which is the shape
|
|
3
|
+
-- drizzle-orm/pg-proxy maps result columns from.
|
|
4
|
+
--
|
|
5
|
+
-- SECURITY INVOKER is load-bearing: the statement runs with the privileges of
|
|
6
|
+
-- the caller (anon or authenticated, per the request's JWT), so row-level
|
|
7
|
+
-- security still applies exactly as it does over the REST API. Do not change
|
|
8
|
+
-- this to SECURITY DEFINER, and do not add a role parameter — either would
|
|
9
|
+
-- turn this into a privilege-escalation primitive.
|
|
10
|
+
create or replace function public.cfni_exec(statement text, params jsonb default '[]'::jsonb)
|
|
11
|
+
returns json
|
|
12
|
+
language plpgsql
|
|
13
|
+
security invoker
|
|
14
|
+
as $$
|
|
15
|
+
declare
|
|
16
|
+
result json;
|
|
17
|
+
args text[];
|
|
18
|
+
begin
|
|
19
|
+
select coalesce(array_agg(value #>> '{}' order by ordinality), '{}')
|
|
20
|
+
into args
|
|
21
|
+
from jsonb_array_elements(params) with ordinality as t(value, ordinality);
|
|
22
|
+
|
|
23
|
+
execute format('select coalesce(json_agg(json_build_array(r.*)), ''[]''::json) from (%s) r', statement)
|
|
24
|
+
into result
|
|
25
|
+
using args;
|
|
26
|
+
|
|
27
|
+
return result;
|
|
28
|
+
end;
|
|
29
|
+
$$;
|
|
30
|
+
|
|
31
|
+
revoke all on function public.cfni_exec(text, jsonb) from public;
|
|
32
|
+
grant execute on function public.cfni_exec(text, jsonb) to authenticated;
|
|
33
|
+
-- Grant to anon only if your app calls withPublicDb:
|
|
34
|
+
grant execute on function public.cfni_exec(text, jsonb) to anon;
|