cloudflare-next-intl 0.7.7 → 0.7.8

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.
@@ -3,20 +3,28 @@ import type { FirebaseAppCheckConfig } from '../../types/types';
3
3
  * Mints a fresh App Check token server-side via a service account, for use
4
4
  * when the client-written App Check cookie (see `appCheckTokenCookieName`)
5
5
  * is absent — e.g. a cold navigation before `AuthUserProvider` has run and
6
- * had a chance to write it. Requires `clientEmail`/`privateKey`/`appId` on
7
- * `firebaseAuth.appCheck`; returns `undefined` (never throws) if the
8
- * exchange fails, so a caller can always fall back to "no App Check token"
9
- * exactly as before this existed.
6
+ * had a chance to write it. Requires `clientEmail`/`appId` on
7
+ * `firebaseAuth.appCheck`, plus either `privateKey` or the
8
+ * `oauthClientId`/`oauthClientSecret`/`oauthRefreshToken` triple; returns
9
+ * `undefined` (never throws) if the exchange fails, so a caller can always
10
+ * fall back to "no App Check token" exactly as before this existed.
10
11
  *
11
- * Signs a short-lived custom JWT with the service account's private key
12
- * (`jose`, Edge/WebCrypto-compatible no `firebase-admin`), then exchanges
13
- * it for an App Check token via `exchangeCustomToken`, authenticated with
14
- * the project's Web API key (`?key=`) `exchangeCustomToken` otherwise
15
- * rejects the call outright as an unregistered/unidentified caller
16
- * (403 `PERMISSION_DENIED`), before the custom token itself is even
17
- * evaluated. Not cached beyond the caller's own request-scoped `cache()`
18
- * wrapper a fresh mint costs one signing operation plus one network
19
- * round-trip, acceptable per-request but not worth doing more than once per
20
- * request.
12
+ * Signs a short-lived custom JWT, then exchanges it for an App Check token
13
+ * via `exchangeCustomToken`, authenticated with the project's Web API key
14
+ * (`?key=`) `exchangeCustomToken` otherwise rejects the call outright as
15
+ * an unregistered/unidentified caller (403 `PERMISSION_DENIED`), before the
16
+ * custom token itself is even evaluated. Not cached beyond the caller's own
17
+ * request-scoped `cache()` wrapper a fresh mint costs one signing
18
+ * operation plus one network round-trip, acceptable per-request but not
19
+ * worth doing more than once per request.
20
+ *
21
+ * The custom token is signed one of two ways, `privateKey` taking priority
22
+ * when both are set:
23
+ * - `privateKey` set: signed locally (`jose`, Edge/WebCrypto-compatible —
24
+ * no `firebase-admin`).
25
+ * - OAuth triple set instead: signed remotely via
26
+ * `sign_custom_token_remote.ts` (IAM Credentials `signJwt`) — the way to
27
+ * mint tokens when a GCP org policy blocks creating the service-account
28
+ * key `privateKey` would otherwise require.
21
29
  */
22
30
  export default function mintServerAppCheckToken(projectId: string, apiKey: string, appCheck: FirebaseAppCheckConfig | undefined): Promise<string | undefined>;
@@ -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`/`privateKey`/`appId` on
25
- * `firebaseAuth.appCheck`; returns `undefined` (never throws) if the
26
- * exchange fails, so a caller can always fall back to "no App Check token"
27
- * exactly as before this existed.
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 with the service account's private key
30
- * (`jose`, Edge/WebCrypto-compatible no `firebase-admin`), then exchanges
31
- * it for an App Check token via `exchangeCustomToken`, authenticated with
32
- * the project's Web API key (`?key=`) `exchangeCustomToken` otherwise
33
- * rejects the call outright as an unregistered/unidentified caller
34
- * (403 `PERMISSION_DENIED`), before the custom token itself is even
35
- * evaluated. Not cached beyond the caller's own request-scoped `cache()`
36
- * wrapper a fresh mint costs one signing operation plus one network
37
- * round-trip, acceptable per-request but not worth doing more than once per
38
- * request.
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.privateKey || !appCheck.appId)
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 { SignJWT, importPKCS8 } = await import('jose');
45
- const privateKey = await importPKCS8(appCheck.privateKey.replace(/\\n/g, '\n'), 'RS256');
46
- const customToken = await new SignJWT({ app_id: appCheck.appId })
47
- .setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
48
- .setIssuer(appCheck.clientEmail)
49
- .setSubject(appCheck.clientEmail)
50
- .setAudience(APP_CHECK_CUSTOM_TOKEN_AUDIENCE)
51
- .setIssuedAt()
52
- .setExpirationTime(CUSTOM_TOKEN_LIFETIME)
53
- .sign(privateKey);
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
+ }
@@ -621,9 +621,10 @@ export interface FirebaseAppCheckConfig {
621
621
  * Service account client email, used ONLY server-side to mint an App
622
622
  * Check token when the client-written App Check cookie is absent (e.g.
623
623
  * a cold navigation before `AuthUserProvider` has run — see
624
- * `appCheckTokenCookieName`). Required alongside `privateKey` and
625
- * `appId` for server-side minting. Never sent to the client — read only
626
- * by `firebase_server.ts`.
624
+ * `appCheckTokenCookieName`). Required alongside `appId`, plus either
625
+ * `privateKey` or the `oauthClientId`/`oauthClientSecret`/
626
+ * `oauthRefreshToken` triple, for server-side minting. Never sent to the
627
+ * client — read only by `firebase_server.ts`.
627
628
  */
628
629
  clientEmail: string;
629
630
  /**
@@ -633,13 +634,35 @@ export interface FirebaseAppCheckConfig {
633
634
  * (e.g. `process.env.FIREBASE_PRIVATE_KEY`), never exposed to the
634
635
  * browser. Escaped `\n` sequences (common when stored in a single-line
635
636
  * env var) are unescaped automatically before use.
636
- */
637
- privateKey: string;
637
+ *
638
+ * Omit this and set the `oauthClientId`/`oauthClientSecret`/
639
+ * `oauthRefreshToken` triple instead when your GCP org enforces
640
+ * `iam.disableServiceAccountKeyCreation`, which blocks issuing this key
641
+ * in the first place. When both are set, `privateKey` takes priority.
642
+ */
643
+ privateKey?: string;
644
+ /**
645
+ * Application Default Credentials OAuth client ID — the `client_id`
646
+ * field from `application_default_credentials.json` (see
647
+ * `gcloud auth application-default login`). Paired with
648
+ * `oauthClientSecret` and `oauthRefreshToken` as an alternative to
649
+ * `privateKey`: instead of signing the App Check custom token locally,
650
+ * it's signed remotely via IAM Credentials `signJwt`, authenticated as
651
+ * this OAuth identity. That identity needs
652
+ * `roles/iam.serviceAccountTokenCreator` on `clientEmail`. Use this when
653
+ * a service-account key can't be created (see `privateKey`). Ignored
654
+ * when `privateKey` is set.
655
+ */
656
+ oauthClientId?: string;
657
+ /** OAuth client secret paired with `oauthClientId`. Same ADC-JSON `client_secret` field, same secret-handling rules as `privateKey`. */
658
+ oauthClientSecret?: string;
659
+ /** OAuth refresh token paired with `oauthClientId`. Same ADC-JSON `refresh_token` field, same secret-handling rules as `privateKey`. */
660
+ oauthRefreshToken?: string;
638
661
  /**
639
662
  * Firebase App Check app ID (e.g. `"1:1234567890:web:abcdef123456"`),
640
- * required alongside `clientEmail`/`privateKey` for server-side minting.
641
- * Distinct from the Firebase Auth `appId` on `FirebaseAuthRoutingConfig`
642
- * itself — App Check registers apps separately.
663
+ * required alongside `clientEmail` for server-side minting. Distinct
664
+ * from the Firebase Auth `appId` on `FirebaseAuthRoutingConfig` itself —
665
+ * App Check registers apps separately.
643
666
  */
644
667
  appId: string;
645
668
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
4
4
  "description": "Optimized Next Intl Package Special for App Router and Cloudflare",
5
5
 
6
6
  "main": "dist/index.js",