@tern-secure/backend 1.2.0-canary.v20251127221555 → 1.2.0-canary.v20251202162458
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/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/types.d.ts +42 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/admin/index.d.ts +1 -1
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +8 -1
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/index.mjs +10 -70
- package/dist/admin/index.mjs.map +1 -1
- package/dist/app-check/AppCheckApi.d.ts +14 -0
- package/dist/app-check/AppCheckApi.d.ts.map +1 -0
- package/dist/app-check/generator.d.ts +9 -0
- package/dist/app-check/generator.d.ts.map +1 -0
- package/dist/app-check/index.d.ts +18 -0
- package/dist/app-check/index.d.ts.map +1 -0
- package/dist/app-check/index.js +1052 -0
- package/dist/app-check/index.js.map +1 -0
- package/dist/app-check/index.mjs +13 -0
- package/dist/app-check/index.mjs.map +1 -0
- package/dist/app-check/serverAppCheck.d.ts +33 -0
- package/dist/app-check/serverAppCheck.d.ts.map +1 -0
- package/dist/app-check/types.d.ts +21 -0
- package/dist/app-check/types.d.ts.map +1 -0
- package/dist/app-check/verifier.d.ts +16 -0
- package/dist/app-check/verifier.d.ts.map +1 -0
- package/dist/auth/credential.d.ts +5 -5
- package/dist/auth/credential.d.ts.map +1 -1
- package/dist/auth/getauth.d.ts +2 -1
- package/dist/auth/getauth.d.ts.map +1 -1
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +819 -394
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/index.mjs +5 -3
- package/dist/chunk-3OGMNIOJ.mjs +174 -0
- package/dist/chunk-3OGMNIOJ.mjs.map +1 -0
- package/dist/{chunk-GFH5CXQR.mjs → chunk-AW5OXT7N.mjs} +2 -2
- package/dist/chunk-IEJQ7F4A.mjs +778 -0
- package/dist/chunk-IEJQ7F4A.mjs.map +1 -0
- package/dist/{chunk-NXYWC6YO.mjs → chunk-TUYCJY35.mjs} +182 -6
- package/dist/chunk-TUYCJY35.mjs.map +1 -0
- package/dist/constants.d.ts +10 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/fireRestApi/endpoints/AppCheckApi.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1570 -1183
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +97 -135
- package/dist/index.mjs.map +1 -1
- package/dist/jwt/crypto-signer.d.ts +21 -0
- package/dist/jwt/crypto-signer.d.ts.map +1 -0
- package/dist/jwt/index.d.ts +2 -1
- package/dist/jwt/index.d.ts.map +1 -1
- package/dist/jwt/index.js +119 -2
- package/dist/jwt/index.js.map +1 -1
- package/dist/jwt/index.mjs +7 -3
- package/dist/jwt/signJwt.d.ts +8 -2
- package/dist/jwt/signJwt.d.ts.map +1 -1
- package/dist/jwt/types.d.ts +6 -0
- package/dist/jwt/types.d.ts.map +1 -1
- package/dist/jwt/verifyJwt.d.ts +7 -1
- package/dist/jwt/verifyJwt.d.ts.map +1 -1
- package/dist/tokens/authstate.d.ts +2 -0
- package/dist/tokens/authstate.d.ts.map +1 -1
- package/dist/tokens/c-authenticateRequestProcessor.d.ts +2 -2
- package/dist/tokens/c-authenticateRequestProcessor.d.ts.map +1 -1
- package/dist/tokens/keys.d.ts.map +1 -1
- package/dist/tokens/request.d.ts.map +1 -1
- package/dist/tokens/types.d.ts +6 -4
- package/dist/tokens/types.d.ts.map +1 -1
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/{auth/utils.d.ts → utils/fetcher.d.ts} +2 -1
- package/dist/utils/fetcher.d.ts.map +1 -0
- package/dist/utils/mapDecode.d.ts +2 -1
- package/dist/utils/mapDecode.d.ts.map +1 -1
- package/dist/utils/token-generator.d.ts +4 -0
- package/dist/utils/token-generator.d.ts.map +1 -0
- package/package.json +13 -3
- package/dist/auth/constants.d.ts +0 -6
- package/dist/auth/constants.d.ts.map +0 -1
- package/dist/auth/utils.d.ts.map +0 -1
- package/dist/chunk-DJLDUW7J.mjs +0 -414
- package/dist/chunk-DJLDUW7J.mjs.map +0 -1
- package/dist/chunk-NXYWC6YO.mjs.map +0 -1
- package/dist/chunk-WIVOBOZR.mjs +0 -86
- package/dist/chunk-WIVOBOZR.mjs.map +0 -1
- package/dist/utils/gemini_admin-init.d.ts +0 -10
- package/dist/utils/gemini_admin-init.d.ts.map +0 -1
- /package/dist/{chunk-GFH5CXQR.mjs.map → chunk-AW5OXT7N.mjs.map} +0 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CACHE_CONTROL_REGEX,
|
|
3
|
+
DEFAULT_CACHE_DURATION,
|
|
4
|
+
FIREBASE_APP_CHECK_AUDIENCE,
|
|
5
|
+
GOOGLE_AUTH_TOKEN_HOST,
|
|
6
|
+
GOOGLE_AUTH_TOKEN_PATH,
|
|
7
|
+
GOOGLE_PUBLIC_KEYS_URL,
|
|
8
|
+
GOOGLE_TOKEN_AUDIENCE,
|
|
9
|
+
MAX_CACHE_LAST_UPDATED_AT_SECONDS,
|
|
10
|
+
ONE_DAY_IN_MILLIS,
|
|
11
|
+
ONE_HOUR_IN_SECONDS,
|
|
12
|
+
ONE_MINUTE_IN_MILLIS,
|
|
13
|
+
ONE_MINUTE_IN_SECONDS,
|
|
14
|
+
TOKEN_EXPIRY_THRESHOLD_MILLIS,
|
|
15
|
+
appCheckAdmin,
|
|
16
|
+
loadAdminConfig
|
|
17
|
+
} from "./chunk-3OGMNIOJ.mjs";
|
|
18
|
+
import {
|
|
19
|
+
IAMSigner,
|
|
20
|
+
ServiceAccountSigner,
|
|
21
|
+
TokenVerificationError,
|
|
22
|
+
TokenVerificationErrorReason,
|
|
23
|
+
createCustomToken,
|
|
24
|
+
fetchJson,
|
|
25
|
+
ternDecodeJwt,
|
|
26
|
+
ternSignJwt,
|
|
27
|
+
verifyAppCheckJwt,
|
|
28
|
+
verifyJwt
|
|
29
|
+
} from "./chunk-TUYCJY35.mjs";
|
|
30
|
+
|
|
31
|
+
// src/tokens/keys.ts
|
|
32
|
+
var cache = {};
|
|
33
|
+
var lastUpdatedAt = 0;
|
|
34
|
+
var googleExpiresAt = 0;
|
|
35
|
+
function getFromCache(kid) {
|
|
36
|
+
return cache[kid];
|
|
37
|
+
}
|
|
38
|
+
function getCacheValues() {
|
|
39
|
+
return Object.values(cache);
|
|
40
|
+
}
|
|
41
|
+
function setInCache(kid, certificate, shouldExpire = true) {
|
|
42
|
+
cache[kid] = certificate;
|
|
43
|
+
lastUpdatedAt = shouldExpire ? Date.now() : -1;
|
|
44
|
+
}
|
|
45
|
+
async function fetchPublicKeys(keyUrl) {
|
|
46
|
+
const url = new URL(keyUrl);
|
|
47
|
+
const response = await fetch(url);
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new TokenVerificationError({
|
|
50
|
+
message: `Error loading public keys from ${url.href} with code=${response.status} `,
|
|
51
|
+
reason: TokenVerificationErrorReason.TokenInvalid
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const data = await response.json();
|
|
55
|
+
const expiresAt = getExpiresAt(response);
|
|
56
|
+
return {
|
|
57
|
+
keys: data,
|
|
58
|
+
expiresAt
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function loadJWKFromRemote({
|
|
62
|
+
keyURL,
|
|
63
|
+
skipJwksCache,
|
|
64
|
+
kid
|
|
65
|
+
}) {
|
|
66
|
+
const finalKeyURL = keyURL || GOOGLE_PUBLIC_KEYS_URL;
|
|
67
|
+
if (skipJwksCache || isCacheExpired() || !getFromCache(kid)) {
|
|
68
|
+
const { keys, expiresAt } = await fetchPublicKeys(finalKeyURL);
|
|
69
|
+
if (!keys || Object.keys(keys).length === 0) {
|
|
70
|
+
throw new TokenVerificationError({
|
|
71
|
+
message: `The JWKS endpoint ${finalKeyURL} returned no keys`,
|
|
72
|
+
reason: TokenVerificationErrorReason.RemoteJWKFailedToLoad
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
googleExpiresAt = expiresAt;
|
|
76
|
+
Object.entries(keys).forEach(([keyId, cert2]) => {
|
|
77
|
+
setInCache(keyId, cert2);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const cert = getFromCache(kid);
|
|
81
|
+
if (!cert) {
|
|
82
|
+
getCacheValues();
|
|
83
|
+
const availableKids = Object.keys(cache).sort().join(", ");
|
|
84
|
+
throw new TokenVerificationError({
|
|
85
|
+
message: `No public key found for kid "${kid}". Available kids: [${availableKids}]`,
|
|
86
|
+
reason: TokenVerificationErrorReason.TokenInvalid
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return cert;
|
|
90
|
+
}
|
|
91
|
+
function isCacheExpired() {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
if (lastUpdatedAt === -1) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const cacheAge = now - lastUpdatedAt;
|
|
97
|
+
const maxCacheAge = MAX_CACHE_LAST_UPDATED_AT_SECONDS * 1e3;
|
|
98
|
+
const localCacheExpired = cacheAge >= maxCacheAge;
|
|
99
|
+
const googleCacheExpired = now >= googleExpiresAt;
|
|
100
|
+
const isExpired = localCacheExpired || googleCacheExpired;
|
|
101
|
+
if (isExpired) {
|
|
102
|
+
cache = {};
|
|
103
|
+
}
|
|
104
|
+
return isExpired;
|
|
105
|
+
}
|
|
106
|
+
function getExpiresAt(res) {
|
|
107
|
+
const cacheControlHeader = res.headers.get("cache-control");
|
|
108
|
+
if (!cacheControlHeader) {
|
|
109
|
+
return Date.now() + DEFAULT_CACHE_DURATION;
|
|
110
|
+
}
|
|
111
|
+
const maxAgeMatch = cacheControlHeader.match(CACHE_CONTROL_REGEX);
|
|
112
|
+
const maxAge = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) : DEFAULT_CACHE_DURATION / 1e3;
|
|
113
|
+
return Date.now() + maxAge * 1e3;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/tokens/verify.ts
|
|
117
|
+
async function verifyToken(token, options) {
|
|
118
|
+
const { data: decodedResult, errors } = ternDecodeJwt(token);
|
|
119
|
+
if (errors) {
|
|
120
|
+
return { errors };
|
|
121
|
+
}
|
|
122
|
+
const { header } = decodedResult;
|
|
123
|
+
const { kid } = header;
|
|
124
|
+
if (!kid) {
|
|
125
|
+
return {
|
|
126
|
+
errors: [
|
|
127
|
+
new TokenVerificationError({
|
|
128
|
+
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
129
|
+
message: 'JWT "kid" header is missing.'
|
|
130
|
+
})
|
|
131
|
+
]
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const key = options.jwtKey || await loadJWKFromRemote({ ...options, kid });
|
|
136
|
+
if (!key) {
|
|
137
|
+
return {
|
|
138
|
+
errors: [
|
|
139
|
+
new TokenVerificationError({
|
|
140
|
+
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
141
|
+
message: `No public key found for kid "${kid}".`
|
|
142
|
+
})
|
|
143
|
+
]
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return await verifyJwt(token, { ...options, key });
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error instanceof TokenVerificationError) {
|
|
149
|
+
return { errors: [error] };
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
errors: [error]
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/auth/getauth.ts
|
|
158
|
+
var API_KEY_ERROR = "API Key is required";
|
|
159
|
+
var NO_DATA_ERROR = "No token data received";
|
|
160
|
+
function parseFirebaseResponse(data) {
|
|
161
|
+
if (typeof data === "string") {
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(data);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw new Error(`Failed to parse Firebase response: ${error}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return data;
|
|
169
|
+
}
|
|
170
|
+
function getAuth(options) {
|
|
171
|
+
const { apiKey } = options;
|
|
172
|
+
const effectiveApiKey = apiKey || process.env.NEXT_PUBLIC_FIREBASE_API_KEY;
|
|
173
|
+
async function getUserData(idToken, localId) {
|
|
174
|
+
if (!effectiveApiKey) {
|
|
175
|
+
throw new Error(API_KEY_ERROR);
|
|
176
|
+
}
|
|
177
|
+
const response = await options.apiClient?.userData.getUserData(effectiveApiKey, {
|
|
178
|
+
idToken,
|
|
179
|
+
localId
|
|
180
|
+
});
|
|
181
|
+
if (!response?.data) {
|
|
182
|
+
throw new Error(NO_DATA_ERROR);
|
|
183
|
+
}
|
|
184
|
+
const parsedData = parseFirebaseResponse(response.data);
|
|
185
|
+
return parsedData;
|
|
186
|
+
}
|
|
187
|
+
async function refreshExpiredIdToken(refreshToken, opts) {
|
|
188
|
+
if (!effectiveApiKey) {
|
|
189
|
+
return { data: null, error: new Error(API_KEY_ERROR) };
|
|
190
|
+
}
|
|
191
|
+
const response = await options.apiClient?.tokens.refreshToken(effectiveApiKey, {
|
|
192
|
+
refresh_token: refreshToken,
|
|
193
|
+
request_origin: opts.referer
|
|
194
|
+
});
|
|
195
|
+
if (!response?.data) {
|
|
196
|
+
return {
|
|
197
|
+
data: null,
|
|
198
|
+
error: new Error(NO_DATA_ERROR)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const parsedData = parseFirebaseResponse(response.data);
|
|
202
|
+
return {
|
|
203
|
+
data: {
|
|
204
|
+
idToken: parsedData.id_token,
|
|
205
|
+
refreshToken: parsedData.refresh_token
|
|
206
|
+
},
|
|
207
|
+
error: null
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
async function customForIdAndRefreshToken(customToken, opts) {
|
|
211
|
+
if (!effectiveApiKey) {
|
|
212
|
+
throw new Error("API Key is required to create custom token");
|
|
213
|
+
}
|
|
214
|
+
const data = await options.apiClient?.tokens.exchangeCustomForIdAndRefreshTokens(
|
|
215
|
+
effectiveApiKey,
|
|
216
|
+
{
|
|
217
|
+
token: customToken,
|
|
218
|
+
returnSecureToken: true
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
referer: opts.referer,
|
|
222
|
+
appCheckToken: opts.appCheckToken
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
if (!data) {
|
|
226
|
+
throw new Error("No data received from Firebase token exchange");
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
idToken: data.idToken,
|
|
230
|
+
refreshToken: data.refreshToken
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
async function createCustomIdAndRefreshToken(idToken, opts) {
|
|
234
|
+
const decoded = await verifyToken(idToken, options);
|
|
235
|
+
const { data, errors } = decoded;
|
|
236
|
+
if (errors) {
|
|
237
|
+
throw errors[0];
|
|
238
|
+
}
|
|
239
|
+
const customToken = await createCustomToken(data.uid, {
|
|
240
|
+
emailVerified: data.email_verified,
|
|
241
|
+
source_sign_in_provider: data.firebase.sign_in_provider
|
|
242
|
+
});
|
|
243
|
+
const idAndRefreshTokens = await customForIdAndRefreshToken(customToken, {
|
|
244
|
+
referer: opts.referer,
|
|
245
|
+
appCheckToken: opts.appCheckToken
|
|
246
|
+
});
|
|
247
|
+
const decodedCustomIdToken = await verifyToken(idAndRefreshTokens.idToken, options);
|
|
248
|
+
if (decodedCustomIdToken.errors) {
|
|
249
|
+
throw decodedCustomIdToken.errors[0];
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
...idAndRefreshTokens,
|
|
253
|
+
customToken,
|
|
254
|
+
auth_time: decodedCustomIdToken.data.auth_time
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function createAppCheckToken() {
|
|
258
|
+
const adminConfig = loadAdminConfig();
|
|
259
|
+
const appId = process.env.NEXT_PUBLIC_FIREBASE_APP_ID || "";
|
|
260
|
+
const appCheck = getAppCheck(adminConfig, options.tenantId);
|
|
261
|
+
try {
|
|
262
|
+
const appCheckResponse = await appCheck.createToken(adminConfig.projectId, appId);
|
|
263
|
+
return {
|
|
264
|
+
data: {
|
|
265
|
+
token: appCheckResponse.token,
|
|
266
|
+
ttl: appCheckResponse.ttl
|
|
267
|
+
},
|
|
268
|
+
error: null
|
|
269
|
+
};
|
|
270
|
+
} catch (error) {
|
|
271
|
+
return { data: null, error };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async function verifyAppCheckToken2(token) {
|
|
275
|
+
const adminConfig = loadAdminConfig();
|
|
276
|
+
const appCheck = getAppCheck(adminConfig, options.tenantId);
|
|
277
|
+
try {
|
|
278
|
+
const decodedToken = await appCheck.verifyToken(token, adminConfig.projectId, {});
|
|
279
|
+
return {
|
|
280
|
+
data: decodedToken,
|
|
281
|
+
error: null
|
|
282
|
+
};
|
|
283
|
+
} catch (error) {
|
|
284
|
+
return { data: null, error };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
getUserData,
|
|
289
|
+
customForIdAndRefreshToken,
|
|
290
|
+
createCustomIdAndRefreshToken,
|
|
291
|
+
refreshExpiredIdToken,
|
|
292
|
+
createAppCheckToken,
|
|
293
|
+
verifyAppCheckToken: verifyAppCheckToken2
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/auth/credential.ts
|
|
298
|
+
var accessTokenCache = /* @__PURE__ */ new Map();
|
|
299
|
+
async function requestAccessToken(urlString, init) {
|
|
300
|
+
const json = await fetchJson(urlString, init);
|
|
301
|
+
if (!json.access_token || !json.expires_in) {
|
|
302
|
+
throw new Error("Invalid access token response");
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
accessToken: json.access_token,
|
|
306
|
+
expirationTime: Date.now() + json.expires_in * 1e3
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
var ServiceAccountManager = class {
|
|
310
|
+
projectId;
|
|
311
|
+
privateKey;
|
|
312
|
+
clientEmail;
|
|
313
|
+
constructor(serviceAccount) {
|
|
314
|
+
this.projectId = serviceAccount.projectId;
|
|
315
|
+
this.privateKey = serviceAccount.privateKey;
|
|
316
|
+
this.clientEmail = serviceAccount.clientEmail;
|
|
317
|
+
}
|
|
318
|
+
fetchAccessToken = async (url) => {
|
|
319
|
+
const token = await this.createJwt();
|
|
320
|
+
const postData = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" + token;
|
|
321
|
+
return requestAccessToken(url, {
|
|
322
|
+
method: "POST",
|
|
323
|
+
headers: {
|
|
324
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
325
|
+
Authorization: `Bearer ${token}`,
|
|
326
|
+
Accept: "application/json"
|
|
327
|
+
},
|
|
328
|
+
body: postData
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
fetchAndCacheAccessToken = async (url) => {
|
|
332
|
+
const accessToken = await this.fetchAccessToken(url);
|
|
333
|
+
accessTokenCache.set(this.projectId, accessToken);
|
|
334
|
+
return accessToken;
|
|
335
|
+
};
|
|
336
|
+
getAccessToken = async (refresh) => {
|
|
337
|
+
const url = `https://${GOOGLE_AUTH_TOKEN_HOST}${GOOGLE_AUTH_TOKEN_PATH}`;
|
|
338
|
+
if (refresh) {
|
|
339
|
+
return this.fetchAndCacheAccessToken(url);
|
|
340
|
+
}
|
|
341
|
+
const cachedResponse = accessTokenCache.get(this.projectId);
|
|
342
|
+
if (!cachedResponse || cachedResponse.expirationTime - Date.now() <= TOKEN_EXPIRY_THRESHOLD_MILLIS) {
|
|
343
|
+
return this.fetchAndCacheAccessToken(url);
|
|
344
|
+
}
|
|
345
|
+
return cachedResponse;
|
|
346
|
+
};
|
|
347
|
+
createJwt = async () => {
|
|
348
|
+
const iat = Math.floor(Date.now() / 1e3);
|
|
349
|
+
const payload = {
|
|
350
|
+
aud: GOOGLE_TOKEN_AUDIENCE,
|
|
351
|
+
iat,
|
|
352
|
+
exp: iat + ONE_HOUR_IN_SECONDS,
|
|
353
|
+
iss: this.clientEmail,
|
|
354
|
+
sub: this.clientEmail,
|
|
355
|
+
scope: [
|
|
356
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
357
|
+
"https://www.googleapis.com/auth/firebase.database",
|
|
358
|
+
"https://www.googleapis.com/auth/firebase.messaging",
|
|
359
|
+
"https://www.googleapis.com/auth/identitytoolkit",
|
|
360
|
+
"https://www.googleapis.com/auth/userinfo.email"
|
|
361
|
+
].join(" ")
|
|
362
|
+
};
|
|
363
|
+
return ternSignJwt({
|
|
364
|
+
payload,
|
|
365
|
+
privateKey: this.privateKey
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// src/utils/token-generator.ts
|
|
371
|
+
function cryptoSignerFromCredential(credential, tenantId, serviceAccountId) {
|
|
372
|
+
if (credential instanceof ServiceAccountManager) {
|
|
373
|
+
return new ServiceAccountSigner(credential, tenantId);
|
|
374
|
+
}
|
|
375
|
+
return new IAMSigner(credential, tenantId, serviceAccountId);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/app-check/AppCheckApi.ts
|
|
379
|
+
function getSdkVersion() {
|
|
380
|
+
return "12.7.0";
|
|
381
|
+
}
|
|
382
|
+
var FIREBASE_APP_CHECK_CONFIG_HEADERS = {
|
|
383
|
+
"X-Firebase-Client": `fire-admin-node/${getSdkVersion()}`
|
|
384
|
+
};
|
|
385
|
+
var AppCheckApi = class {
|
|
386
|
+
constructor(credential) {
|
|
387
|
+
this.credential = credential;
|
|
388
|
+
}
|
|
389
|
+
async exchangeToken(params) {
|
|
390
|
+
const { projectId, appId, customToken, limitedUse = false } = params;
|
|
391
|
+
const token = await this.credential.getAccessToken(false);
|
|
392
|
+
if (!projectId || !appId) {
|
|
393
|
+
throw new Error("Project ID and App ID are required for App Check token exchange");
|
|
394
|
+
}
|
|
395
|
+
const endpoint = `https://firebaseappcheck.googleapis.com/v1/projects/${projectId}/apps/${appId}:exchangeCustomToken`;
|
|
396
|
+
const headers = {
|
|
397
|
+
"Content-Type": "application/json",
|
|
398
|
+
"Authorization": `Bearer ${token.accessToken}`
|
|
399
|
+
};
|
|
400
|
+
try {
|
|
401
|
+
const response = await fetch(endpoint, {
|
|
402
|
+
method: "POST",
|
|
403
|
+
headers,
|
|
404
|
+
body: JSON.stringify({ customToken, limitedUse })
|
|
405
|
+
});
|
|
406
|
+
if (!response.ok) {
|
|
407
|
+
const errorText = await response.text();
|
|
408
|
+
throw new Error(`App Check token exchange failed: ${response.status} ${errorText}`);
|
|
409
|
+
}
|
|
410
|
+
const data = await response.json();
|
|
411
|
+
return {
|
|
412
|
+
token: data.token,
|
|
413
|
+
ttl: data.ttl
|
|
414
|
+
};
|
|
415
|
+
} catch (error) {
|
|
416
|
+
console.warn("[ternsecure - appcheck api]unexpected error:", error);
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async exchangeDebugToken(params) {
|
|
421
|
+
const { projectId, appId, customToken, accessToken, limitedUse = false } = params;
|
|
422
|
+
if (!projectId || !appId) {
|
|
423
|
+
throw new Error("Project ID and App ID are required for App Check token exchange");
|
|
424
|
+
}
|
|
425
|
+
const endpoint = `https://firebaseappcheck.googleapis.com/v1beta/projects/${projectId}/apps/${appId}:exchangeDebugToken`;
|
|
426
|
+
const headers = {
|
|
427
|
+
...FIREBASE_APP_CHECK_CONFIG_HEADERS,
|
|
428
|
+
"Authorization": `Bearer ${accessToken}`
|
|
429
|
+
};
|
|
430
|
+
const body = {
|
|
431
|
+
customToken,
|
|
432
|
+
limitedUse
|
|
433
|
+
};
|
|
434
|
+
try {
|
|
435
|
+
const response = await fetch(endpoint, {
|
|
436
|
+
method: "POST",
|
|
437
|
+
headers,
|
|
438
|
+
body: JSON.stringify(body)
|
|
439
|
+
});
|
|
440
|
+
if (!response.ok) {
|
|
441
|
+
const errorText = await response.text();
|
|
442
|
+
throw new Error(`App Check token exchange failed: ${response.status} ${errorText}`);
|
|
443
|
+
}
|
|
444
|
+
const data = await response.json();
|
|
445
|
+
return {
|
|
446
|
+
token: data.token,
|
|
447
|
+
ttl: data.ttl
|
|
448
|
+
};
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.warn("[ternsecure - appcheck api]unexpected error:", error);
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// src/app-check/generator.ts
|
|
457
|
+
function transformMillisecondsToSecondsString(milliseconds) {
|
|
458
|
+
let duration;
|
|
459
|
+
const seconds = Math.floor(milliseconds / 1e3);
|
|
460
|
+
const nanos = Math.floor((milliseconds - seconds * 1e3) * 1e6);
|
|
461
|
+
if (nanos > 0) {
|
|
462
|
+
let nanoString = nanos.toString();
|
|
463
|
+
while (nanoString.length < 9) {
|
|
464
|
+
nanoString = "0" + nanoString;
|
|
465
|
+
}
|
|
466
|
+
duration = `${seconds}.${nanoString}s`;
|
|
467
|
+
} else {
|
|
468
|
+
duration = `${seconds}s`;
|
|
469
|
+
}
|
|
470
|
+
return duration;
|
|
471
|
+
}
|
|
472
|
+
var AppCheckTokenGenerator = class {
|
|
473
|
+
signer;
|
|
474
|
+
constructor(signer) {
|
|
475
|
+
this.signer = signer;
|
|
476
|
+
}
|
|
477
|
+
async createCustomToken(appId, options) {
|
|
478
|
+
if (!appId) {
|
|
479
|
+
throw new Error(
|
|
480
|
+
"appId is invalid"
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
let customOptions = {};
|
|
484
|
+
if (typeof options !== "undefined") {
|
|
485
|
+
customOptions = this.validateTokenOptions(options);
|
|
486
|
+
}
|
|
487
|
+
const account = await this.signer.getAccountId();
|
|
488
|
+
const iat = Math.floor(Date.now() / 1e3);
|
|
489
|
+
const body = {
|
|
490
|
+
iss: account,
|
|
491
|
+
sub: account,
|
|
492
|
+
app_id: appId,
|
|
493
|
+
aud: FIREBASE_APP_CHECK_AUDIENCE,
|
|
494
|
+
exp: iat + ONE_MINUTE_IN_SECONDS * 5,
|
|
495
|
+
iat,
|
|
496
|
+
...customOptions
|
|
497
|
+
};
|
|
498
|
+
return this.signer.sign(body);
|
|
499
|
+
}
|
|
500
|
+
validateTokenOptions(options) {
|
|
501
|
+
if (typeof options.ttlMillis !== "undefined") {
|
|
502
|
+
if (options.ttlMillis < ONE_MINUTE_IN_MILLIS * 30 || options.ttlMillis > ONE_DAY_IN_MILLIS * 7) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
"ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive)."
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
return { ttl: transformMillisecondsToSecondsString(options.ttlMillis) };
|
|
508
|
+
}
|
|
509
|
+
return {};
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// src/app-check/serverAppCheck.ts
|
|
514
|
+
import { Redis } from "@upstash/redis";
|
|
515
|
+
var ServerAppCheckManager = class _ServerAppCheckManager {
|
|
516
|
+
static instances = /* @__PURE__ */ new Map();
|
|
517
|
+
memoryCache = /* @__PURE__ */ new Map();
|
|
518
|
+
redisClient = null;
|
|
519
|
+
options;
|
|
520
|
+
pendingTokens = /* @__PURE__ */ new Map();
|
|
521
|
+
constructor(options) {
|
|
522
|
+
const defaultOptions = {
|
|
523
|
+
strategy: "memory",
|
|
524
|
+
ttlMillis: 36e5,
|
|
525
|
+
// 1 hour
|
|
526
|
+
refreshBufferMillis: 3e5,
|
|
527
|
+
// 5 minutes
|
|
528
|
+
keyPrefix: "appcheck:token:",
|
|
529
|
+
skipInMemoryFirst: false
|
|
530
|
+
};
|
|
531
|
+
this.options = { ...defaultOptions, ...options };
|
|
532
|
+
if (this.options.strategy === "redis" && this.options.redis) {
|
|
533
|
+
void this.initializeRedis(this.options.redis);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
initializeRedis = (config) => {
|
|
537
|
+
if (!config) {
|
|
538
|
+
throw new Error('[AppCheck] Redis configuration is required when strategy is "redis"');
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
this.redisClient = new Redis({
|
|
542
|
+
url: config.url,
|
|
543
|
+
token: config.token
|
|
544
|
+
});
|
|
545
|
+
console.info("[AppCheck] Redis client initialized for token caching");
|
|
546
|
+
} catch (error) {
|
|
547
|
+
console.error("[AppCheck] Failed to initialize Redis client:", error);
|
|
548
|
+
throw new Error('[AppCheck] Redis initialization failed. Install "@upstash/redis" package.');
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
static getInstance(options) {
|
|
552
|
+
const key = options?.strategy || "memory";
|
|
553
|
+
if (!_ServerAppCheckManager.instances.has(key)) {
|
|
554
|
+
_ServerAppCheckManager.instances.set(key, new _ServerAppCheckManager(options));
|
|
555
|
+
}
|
|
556
|
+
const instance = _ServerAppCheckManager.instances.get(key);
|
|
557
|
+
if (!instance) {
|
|
558
|
+
throw new Error("[AppCheck] Failed to get instance");
|
|
559
|
+
}
|
|
560
|
+
return instance;
|
|
561
|
+
}
|
|
562
|
+
buildCacheKey(appId) {
|
|
563
|
+
return `${this.options.keyPrefix}${appId}`;
|
|
564
|
+
}
|
|
565
|
+
getCachedToken = async (appId) => {
|
|
566
|
+
if (this.options.strategy === "memory") {
|
|
567
|
+
return this.memoryCache.get(appId) || null;
|
|
568
|
+
}
|
|
569
|
+
if (this.options.strategy === "redis") {
|
|
570
|
+
if (!this.options.skipInMemoryFirst) {
|
|
571
|
+
const memCached = this.memoryCache.get(appId);
|
|
572
|
+
if (memCached) {
|
|
573
|
+
return memCached;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (this.redisClient) {
|
|
577
|
+
try {
|
|
578
|
+
const key = this.buildCacheKey(appId);
|
|
579
|
+
const cached = await this.redisClient.get(key);
|
|
580
|
+
if (cached) {
|
|
581
|
+
const parsed = typeof cached === "string" ? JSON.parse(cached) : cached;
|
|
582
|
+
if (!this.options.skipInMemoryFirst) {
|
|
583
|
+
this.memoryCache.set(appId, parsed);
|
|
584
|
+
}
|
|
585
|
+
return parsed;
|
|
586
|
+
}
|
|
587
|
+
} catch (error) {
|
|
588
|
+
console.error("[AppCheck] Redis get error:", error);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
};
|
|
594
|
+
setCachedToken = async (appId, token, expiresAt) => {
|
|
595
|
+
const cachedToken = { token, expiresAt };
|
|
596
|
+
this.memoryCache.set(appId, cachedToken);
|
|
597
|
+
if (this.options.strategy === "memory") {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (this.options.strategy === "redis" && this.redisClient) {
|
|
601
|
+
try {
|
|
602
|
+
const key = this.buildCacheKey(appId);
|
|
603
|
+
const ttl = expiresAt - Date.now();
|
|
604
|
+
await this.redisClient.set(key, JSON.stringify(cachedToken), {
|
|
605
|
+
px: ttl
|
|
606
|
+
// Expiry in milliseconds (lowercase for Upstash)
|
|
607
|
+
});
|
|
608
|
+
} catch (error) {
|
|
609
|
+
console.error("[AppCheck] Redis set error:", error);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
getOrGenerateToken = async (appId) => {
|
|
614
|
+
const cached = await this.getCachedToken(appId);
|
|
615
|
+
const now = Date.now();
|
|
616
|
+
if (cached && cached.expiresAt > now + this.options.refreshBufferMillis) {
|
|
617
|
+
return cached.token;
|
|
618
|
+
}
|
|
619
|
+
const pending = this.pendingTokens.get(appId);
|
|
620
|
+
if (pending) {
|
|
621
|
+
return pending;
|
|
622
|
+
}
|
|
623
|
+
const tokenPromise = this.generateAndCacheToken(appId);
|
|
624
|
+
this.pendingTokens.set(appId, tokenPromise);
|
|
625
|
+
try {
|
|
626
|
+
const token = await tokenPromise;
|
|
627
|
+
return token;
|
|
628
|
+
} finally {
|
|
629
|
+
this.pendingTokens.delete(appId);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
/**
|
|
633
|
+
* Generate and cache a new token
|
|
634
|
+
*/
|
|
635
|
+
generateAndCacheToken = async (appId) => {
|
|
636
|
+
try {
|
|
637
|
+
const now = Date.now();
|
|
638
|
+
const appCheckToken = await appCheckAdmin.createToken(appId, {
|
|
639
|
+
ttlMillis: this.options.ttlMillis
|
|
640
|
+
});
|
|
641
|
+
const expiresAt = now + this.options.ttlMillis;
|
|
642
|
+
await this.setCachedToken(appId, appCheckToken.token, expiresAt);
|
|
643
|
+
return appCheckToken.token;
|
|
644
|
+
} catch (error) {
|
|
645
|
+
console.error("[AppCheck] Failed to generate token:", error);
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
clearCache = async (appId) => {
|
|
650
|
+
if (appId) {
|
|
651
|
+
this.memoryCache.delete(appId);
|
|
652
|
+
} else {
|
|
653
|
+
this.memoryCache.clear();
|
|
654
|
+
}
|
|
655
|
+
if (this.options.strategy === "redis" && this.redisClient) {
|
|
656
|
+
try {
|
|
657
|
+
if (appId) {
|
|
658
|
+
const key = this.buildCacheKey(appId);
|
|
659
|
+
await this.redisClient.del(key);
|
|
660
|
+
}
|
|
661
|
+
} catch (error) {
|
|
662
|
+
console.error("[AppCheck] Redis delete error:", error);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
getCacheStats() {
|
|
667
|
+
const now = Date.now();
|
|
668
|
+
const entries = Array.from(this.memoryCache.entries()).map(([appId, cached]) => ({
|
|
669
|
+
appId,
|
|
670
|
+
expiresIn: Math.max(0, cached.expiresAt - now)
|
|
671
|
+
}));
|
|
672
|
+
return {
|
|
673
|
+
strategy: this.options.strategy,
|
|
674
|
+
memorySize: this.memoryCache.size,
|
|
675
|
+
entries
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Close Redis connection
|
|
680
|
+
*/
|
|
681
|
+
disconnect() {
|
|
682
|
+
if (this.redisClient) {
|
|
683
|
+
this.redisClient = null;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// src/app-check/verifier.ts
|
|
689
|
+
import { createRemoteJWKSet } from "jose";
|
|
690
|
+
var getPublicKey = async (header, keyURL) => {
|
|
691
|
+
const jswksUrl = new URL(keyURL);
|
|
692
|
+
const getKey = createRemoteJWKSet(jswksUrl);
|
|
693
|
+
return getKey(header);
|
|
694
|
+
};
|
|
695
|
+
var verifyAppCheckToken = async (token, options) => {
|
|
696
|
+
const { data: decodedResult, errors } = ternDecodeJwt(token);
|
|
697
|
+
if (errors) {
|
|
698
|
+
throw errors[0];
|
|
699
|
+
}
|
|
700
|
+
const { header } = decodedResult;
|
|
701
|
+
const { kid } = header;
|
|
702
|
+
if (!kid) {
|
|
703
|
+
return {
|
|
704
|
+
errors: [
|
|
705
|
+
new TokenVerificationError({
|
|
706
|
+
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
707
|
+
message: 'JWT "kid" header is missing.'
|
|
708
|
+
})
|
|
709
|
+
]
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
const getPublicKeyForToken = () => getPublicKey(header, options.keyURL || "");
|
|
714
|
+
return await verifyAppCheckJwt(token, { ...options, key: getPublicKeyForToken });
|
|
715
|
+
} catch (error) {
|
|
716
|
+
if (error instanceof TokenVerificationError) {
|
|
717
|
+
return { errors: [error] };
|
|
718
|
+
}
|
|
719
|
+
return {
|
|
720
|
+
errors: [error]
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
var AppcheckTokenVerifier = class {
|
|
725
|
+
constructor(credential) {
|
|
726
|
+
this.credential = credential;
|
|
727
|
+
}
|
|
728
|
+
verifyToken = async (token, projectId, options) => {
|
|
729
|
+
const { data, errors } = await verifyAppCheckToken(token, options);
|
|
730
|
+
if (errors) {
|
|
731
|
+
throw errors[0];
|
|
732
|
+
}
|
|
733
|
+
return data;
|
|
734
|
+
};
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
// src/app-check/index.ts
|
|
738
|
+
var JWKS_URL = "https://firebaseappcheck.googleapis.com/v1/jwks";
|
|
739
|
+
var AppCheck = class {
|
|
740
|
+
client;
|
|
741
|
+
tokenGenerator;
|
|
742
|
+
appCheckTokenVerifier;
|
|
743
|
+
limitedUse;
|
|
744
|
+
constructor(credential, tenantId, limitedUse) {
|
|
745
|
+
this.client = new AppCheckApi(credential);
|
|
746
|
+
this.tokenGenerator = new AppCheckTokenGenerator(
|
|
747
|
+
cryptoSignerFromCredential(credential, tenantId)
|
|
748
|
+
);
|
|
749
|
+
this.appCheckTokenVerifier = new AppcheckTokenVerifier(credential);
|
|
750
|
+
this.limitedUse = limitedUse;
|
|
751
|
+
}
|
|
752
|
+
createToken = (projectId, appId, options) => {
|
|
753
|
+
return this.tokenGenerator.createCustomToken(appId, options).then((customToken) => {
|
|
754
|
+
return this.client.exchangeToken({ customToken, projectId, appId });
|
|
755
|
+
});
|
|
756
|
+
};
|
|
757
|
+
verifyToken = async (appCheckToken, projectId, options) => {
|
|
758
|
+
return this.appCheckTokenVerifier.verifyToken(appCheckToken, projectId, { keyURL: JWKS_URL, ...options }).then((decodedToken) => {
|
|
759
|
+
return {
|
|
760
|
+
appId: decodedToken.app_id,
|
|
761
|
+
token: decodedToken
|
|
762
|
+
};
|
|
763
|
+
});
|
|
764
|
+
};
|
|
765
|
+
};
|
|
766
|
+
function getAppCheck(serviceAccount, tenantId, limitedUse) {
|
|
767
|
+
return new AppCheck(new ServiceAccountManager(serviceAccount), tenantId, limitedUse);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export {
|
|
771
|
+
verifyToken,
|
|
772
|
+
ServerAppCheckManager,
|
|
773
|
+
AppCheck,
|
|
774
|
+
getAppCheck,
|
|
775
|
+
getAuth,
|
|
776
|
+
ServiceAccountManager
|
|
777
|
+
};
|
|
778
|
+
//# sourceMappingURL=chunk-IEJQ7F4A.mjs.map
|