@rdlabo/workers-hono-kit 0.9.4 → 0.9.6
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.
|
@@ -28,8 +28,9 @@ export declare const SECURETOKEN_JWK_URL = "https://www.googleapis.com/service_a
|
|
|
28
28
|
*
|
|
29
29
|
* This replaces the `firebase-admin` Auth surface (`verifyIdToken` / `getUser` /
|
|
30
30
|
* `deleteUser`) in environments where the Node SDK cannot run, such as Cloudflare Workers.
|
|
31
|
-
* Token verification
|
|
32
|
-
* project id, an RS256 signature, a non-empty subject (the
|
|
31
|
+
* Token verification follows Firebase's documented third-party JWT validation requirements:
|
|
32
|
+
* issuer and audience equal to the project id, an RS256 signature, a non-empty subject (the
|
|
33
|
+
* uid), and valid `exp`, `iat`, and `auth_time` timestamps.
|
|
33
34
|
*
|
|
34
35
|
* @remarks
|
|
35
36
|
* The verification key is supplied as `keyResolver`:
|
|
@@ -65,13 +66,15 @@ export declare class JoseFirebaseVerifier implements FirebaseVerifier {
|
|
|
65
66
|
* Verify a Firebase ID token and return its decoded payload.
|
|
66
67
|
*
|
|
67
68
|
* Checks the RS256 signature against the configured key, enforces the expected issuer and
|
|
68
|
-
* audience (the project id), and applies
|
|
69
|
-
* subject of at most 128 characters
|
|
69
|
+
* audience (the project id), and applies Firebase's documented ID-token checks: a required
|
|
70
|
+
* future `exp`, a non-empty string subject of at most 128 characters, plus finite `iat` and
|
|
71
|
+
* `auth_time` values that are not in the future. Timestamp comparisons use strict zero clock
|
|
72
|
+
* tolerance.
|
|
70
73
|
*
|
|
71
74
|
* @param idToken - The raw Firebase ID token (JWT) to verify.
|
|
72
75
|
* @returns The decoded payload, with `uid` set from `sub` and `email` lifted to a top-level field.
|
|
73
76
|
* @throws If the signature, issuer, audience, or expiry are invalid, if the subject is
|
|
74
|
-
* missing/non-string/too long, or if `auth_time` is missing or in the future.
|
|
77
|
+
* missing/non-string/too long, or if `iat`/`auth_time` is missing, non-finite, or in the future.
|
|
75
78
|
*/
|
|
76
79
|
verifyIdToken(idToken: string): Promise<DecodedIdToken>;
|
|
77
80
|
/**
|
|
@@ -14,8 +14,9 @@ export const SECURETOKEN_JWK_URL = 'https://www.googleapis.com/service_accounts/
|
|
|
14
14
|
*
|
|
15
15
|
* This replaces the `firebase-admin` Auth surface (`verifyIdToken` / `getUser` /
|
|
16
16
|
* `deleteUser`) in environments where the Node SDK cannot run, such as Cloudflare Workers.
|
|
17
|
-
* Token verification
|
|
18
|
-
* project id, an RS256 signature, a non-empty subject (the
|
|
17
|
+
* Token verification follows Firebase's documented third-party JWT validation requirements:
|
|
18
|
+
* issuer and audience equal to the project id, an RS256 signature, a non-empty subject (the
|
|
19
|
+
* uid), and valid `exp`, `iat`, and `auth_time` timestamps.
|
|
19
20
|
*
|
|
20
21
|
* @remarks
|
|
21
22
|
* The verification key is supplied as `keyResolver`:
|
|
@@ -48,29 +49,42 @@ export class JoseFirebaseVerifier {
|
|
|
48
49
|
* Verify a Firebase ID token and return its decoded payload.
|
|
49
50
|
*
|
|
50
51
|
* Checks the RS256 signature against the configured key, enforces the expected issuer and
|
|
51
|
-
* audience (the project id), and applies
|
|
52
|
-
* subject of at most 128 characters
|
|
52
|
+
* audience (the project id), and applies Firebase's documented ID-token checks: a required
|
|
53
|
+
* future `exp`, a non-empty string subject of at most 128 characters, plus finite `iat` and
|
|
54
|
+
* `auth_time` values that are not in the future. Timestamp comparisons use strict zero clock
|
|
55
|
+
* tolerance.
|
|
53
56
|
*
|
|
54
57
|
* @param idToken - The raw Firebase ID token (JWT) to verify.
|
|
55
58
|
* @returns The decoded payload, with `uid` set from `sub` and `email` lifted to a top-level field.
|
|
56
59
|
* @throws If the signature, issuer, audience, or expiry are invalid, if the subject is
|
|
57
|
-
* missing/non-string/too long, or if `auth_time` is missing or in the future.
|
|
60
|
+
* missing/non-string/too long, or if `iat`/`auth_time` is missing, non-finite, or in the future.
|
|
58
61
|
*/
|
|
59
62
|
async verifyIdToken(idToken) {
|
|
63
|
+
const now = this.nowSeconds();
|
|
60
64
|
const options = {
|
|
61
65
|
issuer: `https://securetoken.google.com/${this.opts.projectId}`,
|
|
62
66
|
audience: this.opts.projectId,
|
|
63
67
|
algorithms: ['RS256'],
|
|
68
|
+
requiredClaims: ['exp'],
|
|
69
|
+
currentDate: new Date(now * 1000),
|
|
70
|
+
clockTolerance: 0,
|
|
64
71
|
};
|
|
65
72
|
// Branch so each call matches a single jwtVerify overload (static key vs getKey fn).
|
|
66
73
|
const key = this.opts.keyResolver;
|
|
67
74
|
const { payload } = typeof key === 'function' ? await jwtVerify(idToken, key, options) : await jwtVerify(idToken, key, options);
|
|
68
|
-
//
|
|
75
|
+
// Apply Firebase's documented checks beyond signature/iss/aud/exp.
|
|
69
76
|
if (!payload.sub || typeof payload.sub !== 'string' || payload.sub.length > 128) {
|
|
70
77
|
throw new Error('Firebase ID token has an invalid subject');
|
|
71
78
|
}
|
|
79
|
+
if (!Number.isFinite(payload.exp)) {
|
|
80
|
+
throw new Error('Firebase ID token has an invalid exp');
|
|
81
|
+
}
|
|
82
|
+
const issuedAt = payload.iat;
|
|
83
|
+
if (typeof issuedAt !== 'number' || !Number.isFinite(issuedAt) || issuedAt > now) {
|
|
84
|
+
throw new Error('Firebase ID token has an invalid iat');
|
|
85
|
+
}
|
|
72
86
|
const authTime = payload.auth_time;
|
|
73
|
-
if (typeof authTime !== 'number' || authTime >
|
|
87
|
+
if (typeof authTime !== 'number' || !Number.isFinite(authTime) || authTime > now) {
|
|
74
88
|
throw new Error('Firebase ID token has an invalid auth_time');
|
|
75
89
|
}
|
|
76
90
|
return { ...payload, uid: payload.sub, email: payload.email };
|
|
@@ -125,6 +139,10 @@ export class JoseFirebaseVerifier {
|
|
|
125
139
|
* @internal
|
|
126
140
|
*/
|
|
127
141
|
nowSeconds() {
|
|
128
|
-
|
|
142
|
+
const now = this.opts.now ? this.opts.now() : Math.floor(Date.now() / 1000);
|
|
143
|
+
if (!Number.isFinite(now)) {
|
|
144
|
+
throw new Error('Firebase verifier clock returned an invalid time');
|
|
145
|
+
}
|
|
146
|
+
return now;
|
|
129
147
|
}
|
|
130
148
|
}
|
|
@@ -31,6 +31,8 @@ export interface PerfLogOptions {
|
|
|
31
31
|
* When provided, write one data point per request to a **Workers Analytics Engine** dataset. Query
|
|
32
32
|
* percentiles by route/colo with the SQL API (≈90-day retention). Non-blocking. Layout:
|
|
33
33
|
* `doubles = [t_app_ms, cold(0|1), status]`, `blobs = [path, colo, method]`, `indexes = [path]`.
|
|
34
|
+
* Route patterns longer than Analytics Engine's 96-byte index limit use a stable hash as the index;
|
|
35
|
+
* the complete route remains available in `blob1`.
|
|
34
36
|
*/
|
|
35
37
|
dataset?: AnalyticsEngineDatasetLike;
|
|
36
38
|
/**
|
|
@@ -18,6 +18,20 @@
|
|
|
18
18
|
// a cold start are labelled warm (only the very first flips the flag) even though they pay cold-init
|
|
19
19
|
// waits — a minor warm-side contamination, negligible at the low request rates this targets.
|
|
20
20
|
let isolateWarm = false;
|
|
21
|
+
const ANALYTICS_INDEX_MAX_BYTES = 96;
|
|
22
|
+
function analyticsIndex(path) {
|
|
23
|
+
const bytes = new TextEncoder().encode(path);
|
|
24
|
+
if (bytes.byteLength <= ANALYTICS_INDEX_MAX_BYTES) {
|
|
25
|
+
return path;
|
|
26
|
+
}
|
|
27
|
+
// FNV-1a 64-bit keeps sampling deterministic without adding async crypto work to every response.
|
|
28
|
+
let hash = 0xcbf29ce484222325n;
|
|
29
|
+
for (const byte of bytes) {
|
|
30
|
+
hash ^= BigInt(byte);
|
|
31
|
+
hash = BigInt.asUintN(64, hash * 0x100000001b3n);
|
|
32
|
+
}
|
|
33
|
+
return `route:${hash.toString(16).padStart(16, '0')}`;
|
|
34
|
+
}
|
|
21
35
|
/**
|
|
22
36
|
* Create a Hono middleware that records a per-request latency data point and emits it to Workers
|
|
23
37
|
* Logs (`console`) and/or Workers Analytics Engine (`dataset`).
|
|
@@ -84,11 +98,17 @@ export function perfLog(options = {}) {
|
|
|
84
98
|
// In-code sampling thins Analytics Engine writes only; Workers Logs volume is controlled separately
|
|
85
99
|
// by the observability `head_sampling_rate`. Low-traffic Workers should leave `sampleRate` at 1.
|
|
86
100
|
if (sink && (rate >= 1 || Math.random() < rate)) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
try {
|
|
102
|
+
sink.writeDataPoint({
|
|
103
|
+
doubles: [tApp, cold ? 1 : 0, status],
|
|
104
|
+
blobs: [path, colo, method],
|
|
105
|
+
indexes: [analyticsIndex(path)],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
// Telemetry must never replace an otherwise successful application response with a 500.
|
|
110
|
+
console.warn('[perfLog] Analytics Engine write failed', error);
|
|
111
|
+
}
|
|
92
112
|
}
|
|
93
113
|
if (emitConsole) {
|
|
94
114
|
console.log(JSON.stringify({ perf: { cold, colo, method, path, status, t_app: tApp } }));
|