@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 mirrors the admin SDK's checks: issuer and audience equal to the
32
- * project id, an RS256 signature, a non-empty subject (the uid), and a valid `auth_time`.
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 the admin SDK's extra checks: a non-empty string
69
- * subject of at most 128 characters and an `auth_time` that is a number not in the future.
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 mirrors the admin SDK's checks: issuer and audience equal to the
18
- * project id, an RS256 signature, a non-empty subject (the uid), and a valid `auth_time`.
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 the admin SDK's extra checks: a non-empty string
52
- * subject of at most 128 characters and an `auth_time` that is a number not in the future.
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
- // Mirror firebase-admin's extra checks beyond signature/iss/aud/exp:
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 > this.nowSeconds()) {
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
- return this.opts.now ? this.opts.now() : Math.floor(Date.now() / 1000);
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
- sink.writeDataPoint({
88
- doubles: [tApp, cold ? 1 : 0, status],
89
- blobs: [path, colo, method],
90
- indexes: [path],
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 } }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rdlabo/workers-hono-kit",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"