@fluojs/jwt 1.0.0 → 1.0.1

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.ko.md CHANGED
@@ -59,6 +59,8 @@ JWT 설정이 다른 provider에서 와야 한다면, `JwtModule.forRootAsync(..
59
59
 
60
60
  비동기 등록도 동기 경로와 동일한 JWT provider surface를 export하며, 여기에는 `RefreshTokenService`가 포함됩니다. 단, 이 서비스를 실제로 resolve하려면 `refreshToken` 옵션이 구성되어 있어야 합니다.
61
61
 
62
+ `forRootAsync(...)`는 `inject`에 나열된 provider에서 module-level `JwtVerifierOptions` 객체 하나를 resolve합니다. 이 factory는 요청별 상태를 받지 않습니다. 테넌트별 secret이나 identity provider가 필요한 경우 tenant lookup은 애플리케이션의 auth layer에 두고, token verification 중에는 `kid` 같은 token metadata와 미리 구성한 `keys[]`, `jwksUri`, 또는 `secretOrKeyProvider`를 사용해 검증 material을 선택하세요.
63
+
62
64
  ```typescript
63
65
  import { Module, type Token } from '@fluojs/core';
64
66
  import { JwtModule } from '@fluojs/jwt';
@@ -148,10 +150,14 @@ const verifier = new DefaultJwtVerifier({
148
150
 
149
151
  `jwksRequestTimeoutMs`의 기본값은 `5_000`이며, 예산을 넘기면 진행 중인 JWKS fetch를 abort합니다.
150
152
 
153
+ JWKS key는 `jwksCacheTtl` 밀리초 동안 cache되며 기본값은 `600_000`입니다. in-memory cache는 `jwksCacheMaxEntries`로 제한되고 기본값은 `100`입니다. lookup 전 만료된 entry를 정리하고, 제한을 넘으면 가장 오래 보관된 key를 제거합니다. 수동 shutdown이나 identity-provider 재설정 시에는 `JwksClient.dispose()` / `DefaultJwtVerifier.dispose()`로 보관 중인 remote key material을 비울 수 있습니다. `jwksCacheTtl`을 `0`으로 설정하면 bounded fetch timeout은 유지하면서 key 보관만 비활성화합니다.
154
+
151
155
  `JwtService.verify(token, options)`는 호출 단위의 알고리즘/클레임 정책 재정의(`issuer`, `audience`, `clockSkewSeconds`, `maxAge`, `requireExp`)를 적용하더라도, 내부 JWKS client나 정적 key-resolution cache를 다시 만들지 않습니다. 호출 단위 검증은 `jwksUri`, `keys[]`, `publicKey`, `secret`, `secretOrKeyProvider` 같은 구성된 key source 자체를 교체하지는 않습니다.
152
156
 
153
157
  호환되는 키가 여러 개 설정되어 있으면 `kid`가 검증 키를 구분합니다. 호환되는 정적 키가 하나뿐이면 `kid` 없이도 토큰을 검증할 수 있고, JWKS 기반 검증은 원격 key set과 cache policy를 따릅니다.
154
158
 
159
+ 멀티테넌트 시스템에서는 발행된 토큰 header에 tenant-specific `kid`를 넣고 호환되는 key source를 미리 구성하는 방식을 권장합니다. `secretOrKeyProvider`는 decoded token header만 인자로 받으므로, request header, route param, 기타 request-context tenant hint는 JWT verifier 호출 전에 애플리케이션 수준 strategy/guard 코드에서 처리해야 합니다.
160
+
155
161
  ### 리프레시 토큰
156
162
 
157
163
  `RefreshTokenService`는 전용 HMAC refresh-token 경로를 사용합니다. `refreshToken.secret`은 access-token 서명 키와 별도로 설정하세요. Rotation은 `RefreshTokenStore.rotate(...)`를 사용해 현재 토큰을 소비 처리하고 대체 토큰을 같은 durable store 작업 안에서 저장할 수 있습니다. 따라서 성공한 rotation은 저장된 후속 토큰 없이 기존 토큰만 소비하지 않습니다. 기존 atomic `consume(...)` hook만 구현한 store도 계속 지원하지만, 대체 토큰 저장의 내구성은 store가 소유한 `rotate(...)` 작업에 달려 있습니다.
@@ -162,7 +168,7 @@ JWT 서명과 검증에는 `algorithms`에 지원되는 알고리즘이 하나
162
168
 
163
169
  액세스 토큰 TTL도 양의 유한 숫자여야 합니다. `accessTokenTtlSeconds`를 생략하면 `DefaultJwtSigner`는 문서화된 기본값인 `3600`초를 사용합니다. 소수 초는 JWT NumericDate `exp` 클레임에 그대로 보존됩니다. `0`, 음수 또는 유한하지 않은 값이 제공되면 토큰을 발행하기 전에 `JwtConfigurationError`로 실패합니다.
164
170
 
165
- 검증은 잘못된 시간 정책에 대해 fail closed로 동작합니다. 검증에 참여하는 `exp`, `nbf`, `iat` 클레임은 유한한 JWT NumericDate 숫자여야 하며, `clockSkewSeconds`도 음수가 아닌 유한 숫자여야 합니다. 유한하지 않은 값은 expiration, not-before, age check를 늘리는 대신 거부됩니다.
171
+ 검증은 잘못된 시간 정책에 대해 fail closed로 동작합니다. 검증에 참여하는 `exp`, `nbf`, `iat` 클레임은 유한한 JWT NumericDate 숫자여야 하며, `clockSkewSeconds`도 음수가 아닌 유한 숫자여야 합니다. 유한하지 않은 값은 expiration, not-before, age check를 늘리는 대신 거부됩니다. verifier 시간이 `exp` NumericDate에 도달하면 토큰은 만료된 것으로 처리되며, 양수 clock skew가 경계를 덮지 않는 한 equality도 만료로 간주합니다.
166
172
 
167
173
  ## 공개 API 개요
168
174
 
@@ -179,6 +185,7 @@ JWT 서명과 검증에는 `algorithms`에 지원되는 알고리즘이 하나
179
185
  - `JwtVerifierOptions`: 알고리즘, 키, 검증 정책 설정을 위한 타입입니다.
180
186
  - `SignOptions`, `VerifyOptions`: 호출 단위 서명 및 검증 재정의 타입입니다.
181
187
  - `JwtClaims`, `JwtSigner`, `JwtVerifier`, `JwtKeyEntry`, `JwtAlgorithm`: 공개 서명 및 검증 계약입니다.
188
+ - `RefreshTokenOptions`, `RefreshTokenStore`, `RefreshTokenRecord`, `RefreshTokenConsumeInput`, `RefreshTokenRotateInput`, `RefreshTokenConsumeResult`: refresh-token 저장, rotation, replay detection 계약입니다.
182
189
 
183
190
  ### 에러와 diagnostics
184
191
  - `JwtVerificationError`, `JwtInvalidTokenError`, `JwtExpiredTokenError`, `JwtConfigurationError`: 타입이 지정된 JWT 실패입니다.
package/README.md CHANGED
@@ -59,6 +59,8 @@ Use `JwtModule.forRootAsync(...)` when your JWT settings must come from another
59
59
 
60
60
  Async registration exports the same JWT provider surface as the synchronous path, including `RefreshTokenService`; resolving that service still requires `refreshToken` options to be configured.
61
61
 
62
+ `forRootAsync(...)` resolves one module-level `JwtVerifierOptions` object from the providers listed in `inject`. It does not receive per-request state. For tenant-specific secrets or identity providers, keep tenant lookup in your application auth layer and use token metadata such as `kid` with configured `keys[]`, `jwksUri`, or `secretOrKeyProvider` to select verification material during token verification.
63
+
62
64
  ```typescript
63
65
  import { Module, type Token } from '@fluojs/core';
64
66
  import { JwtModule } from '@fluojs/jwt';
@@ -148,10 +150,14 @@ const verifier = new DefaultJwtVerifier({
148
150
 
149
151
  `jwksRequestTimeoutMs` defaults to `5_000` and aborts the outbound JWKS fetch once that budget is exceeded.
150
152
 
153
+ JWKS keys are cached for `jwksCacheTtl` milliseconds (`600_000` by default) and the in-memory cache is bounded by `jwksCacheMaxEntries` (`100` by default). Expired entries are pruned before lookups, the oldest retained key is evicted when the bound is exceeded, and `JwksClient.dispose()` / `DefaultJwtVerifier.dispose()` clears retained remote key material during manual shutdown or identity-provider reconfiguration. A `jwksCacheTtl` of `0` disables key retention while still using bounded fetch timeouts.
154
+
151
155
  `JwtService.verify(token, options)` applies per-call algorithm and claim-policy overrides (`issuer`, `audience`, `clockSkewSeconds`, `maxAge`, `requireExp`) without rebuilding the underlying JWKS client or static key-resolution cache. Per-call verification does not replace configured key sources such as `jwksUri`, `keys[]`, `publicKey`, `secret`, or `secretOrKeyProvider`.
152
156
 
153
157
  When multiple compatible keys are configured, `kid` disambiguates the verification key. A single compatible static key can verify tokens without `kid`; JWKS-backed verification relies on the remote key set and its cache policy.
154
158
 
159
+ For multi-tenant systems, prefer putting a tenant-specific `kid` in issued token headers and configuring compatible key sources up front. `secretOrKeyProvider` is called with the decoded token header only, so request headers, route params, or other request-context tenant hints must be handled by application-level strategy/guard code before calling the JWT verifier.
160
+
155
161
  ### Refresh tokens
156
162
 
157
163
  `RefreshTokenService` uses a dedicated HMAC refresh-token path. Configure `refreshToken.secret` separately from access-token signing keys. Rotation can use `RefreshTokenStore.rotate(...)` to atomically mark the current token as consumed and persist the replacement token in the same durable store operation, so a successful rotation never consumes the old token without a stored successor. Stores that only implement the older atomic `consume(...)` hook remain supported, but durable replacement persistence depends on the store-owned `rotate(...)` operation.
@@ -162,7 +168,7 @@ JWT signing and verification require at least one supported algorithm in `algori
162
168
 
163
169
  Access-token TTL must also be a positive finite number. When `accessTokenTtlSeconds` is omitted, `DefaultJwtSigner` uses the documented `3600` second default. Fractional seconds are preserved in the JWT NumericDate `exp` claim; when the option is provided as `0`, a negative number, or a non-finite value, signing fails with `JwtConfigurationError` before a token is issued.
164
170
 
165
- Verification fails closed on malformed time policy. `exp`, `nbf`, and `iat` claims that participate in verification must be finite JWT NumericDate numbers, and `clockSkewSeconds` must be a non-negative finite number. Non-finite values are rejected instead of extending expiration, not-before, or age checks.
171
+ Verification fails closed on malformed time policy. `exp`, `nbf`, and `iat` claims that participate in verification must be finite JWT NumericDate numbers, and `clockSkewSeconds` must be a non-negative finite number. Non-finite values are rejected instead of extending expiration, not-before, or age checks. A token is expired when verifier time reaches its `exp` NumericDate; equality is treated as expired unless positive clock skew still covers the boundary.
166
172
 
167
173
  ## Public API Overview
168
174
 
@@ -179,6 +185,7 @@ Verification fails closed on malformed time policy. `exp`, `nbf`, and `iat` clai
179
185
  - `JwtVerifierOptions`: Configuration for algorithms, keys, and validation policy.
180
186
  - `SignOptions` and `VerifyOptions`: Per-call signing and verification overrides.
181
187
  - `JwtClaims`, `JwtSigner`, `JwtVerifier`, `JwtKeyEntry`, `JwtAlgorithm`: Public signing and verification contracts.
188
+ - `RefreshTokenOptions`, `RefreshTokenStore`, `RefreshTokenRecord`, `RefreshTokenConsumeInput`, `RefreshTokenRotateInput`, and `RefreshTokenConsumeResult`: Refresh-token storage, rotation, and replay-detection contracts.
182
189
 
183
190
  ### Errors and diagnostics
184
191
  - `JwtVerificationError`, `JwtInvalidTokenError`, `JwtExpiredTokenError`, `JwtConfigurationError`: Typed JWT failures.
@@ -6,10 +6,21 @@ export declare class JwksClient {
6
6
  private readonly uri;
7
7
  private readonly cacheTtl;
8
8
  private readonly requestTimeoutMs;
9
+ private readonly cacheMaxEntries;
9
10
  private readonly cache;
10
- constructor(uri: string, cacheTtl?: number, requestTimeoutMs?: number);
11
+ private lifecycleGeneration;
12
+ constructor(uri: string, cacheTtl?: number, requestTimeoutMs?: number, cacheMaxEntries?: number);
13
+ /**
14
+ * Clears all cached JWKS key material held by this client.
15
+ *
16
+ * Call this during application shutdown when the verifier/client lifecycle is
17
+ * owned manually, or when rotating identity-provider configuration.
18
+ */
19
+ dispose(): void;
11
20
  private isAbortError;
12
21
  getSigningKey(kid: string): Promise<KeyObject>;
22
+ private pruneExpiredCacheEntries;
23
+ private evictOldestCacheEntries;
13
24
  private fetchKeys;
14
25
  }
15
26
  //# sourceMappingURL=jwks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"jwks.d.ts","sourceRoot":"","sources":["../../src/signing/jwks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAa9D;;GAEG;AACH,qBAAa,UAAU;IAInB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IALnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4D;gBAG/D,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAgB,EAC1B,gBAAgB,GAAE,MAAc;IAGnD,OAAO,CAAC,YAAY;IAId,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;YA+BtC,SAAS;CAqCxB"}
1
+ {"version":3,"file":"jwks.d.ts","sourceRoot":"","sources":["../../src/signing/jwks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAiC9D;;GAEG;AACH,qBAAa,UAAU;IAKnB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAPlC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4D;IAClF,OAAO,CAAC,mBAAmB,CAAK;gBAGb,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAgB,EAC1B,gBAAgB,GAAE,MAAc,EAChC,eAAe,GAAE,MAAuC;IAO3E;;;;;OAKG;IACH,OAAO,IAAI,IAAI;IAKf,OAAO,CAAC,YAAY;IAId,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IA0CpD,OAAO,CAAC,wBAAwB;IAQhC,OAAO,CAAC,uBAAuB;YAYjB,SAAS;CAsCxB"}
@@ -1,25 +1,63 @@
1
1
  import { createPublicKey } from 'node:crypto';
2
2
  import { JwtConfigurationError, JwtInvalidTokenError } from '../errors.js';
3
+ const DEFAULT_JWKS_CACHE_MAX_ENTRIES = 100;
4
+ function assertNonNegativeFiniteNumber(value, label) {
5
+ if (!Number.isFinite(value) || value < 0) {
6
+ throw new JwtConfigurationError(`${label} must be a non-negative finite number.`);
7
+ }
8
+ }
9
+ function assertPositiveFiniteNumber(value, label) {
10
+ if (!Number.isFinite(value) || value <= 0) {
11
+ throw new JwtConfigurationError(`${label} must be a positive finite number.`);
12
+ }
13
+ }
14
+ function assertPositiveInteger(value, label) {
15
+ if (!Number.isInteger(value) || value <= 0) {
16
+ throw new JwtConfigurationError(`${label} must be a positive integer.`);
17
+ }
18
+ }
19
+
3
20
  /**
4
21
  * Represents the jwks client.
5
22
  */
6
23
  export class JwksClient {
7
24
  cache = new Map();
8
- constructor(uri, cacheTtl = 600_000, requestTimeoutMs = 5_000) {
25
+ lifecycleGeneration = 0;
26
+ constructor(uri, cacheTtl = 600_000, requestTimeoutMs = 5_000, cacheMaxEntries = DEFAULT_JWKS_CACHE_MAX_ENTRIES) {
9
27
  this.uri = uri;
10
28
  this.cacheTtl = cacheTtl;
11
29
  this.requestTimeoutMs = requestTimeoutMs;
30
+ this.cacheMaxEntries = cacheMaxEntries;
31
+ assertNonNegativeFiniteNumber(cacheTtl, 'JWKS cache ttl');
32
+ assertPositiveFiniteNumber(requestTimeoutMs, 'JWKS request timeout');
33
+ assertPositiveInteger(cacheMaxEntries, 'JWKS cache max entries');
34
+ }
35
+
36
+ /**
37
+ * Clears all cached JWKS key material held by this client.
38
+ *
39
+ * Call this during application shutdown when the verifier/client lifecycle is
40
+ * owned manually, or when rotating identity-provider configuration.
41
+ */
42
+ dispose() {
43
+ this.lifecycleGeneration += 1;
44
+ this.cache.clear();
12
45
  }
13
46
  isAbortError(error) {
14
47
  return error instanceof Error && error.name === 'AbortError';
15
48
  }
16
49
  async getSigningKey(kid) {
17
50
  const now = Date.now();
51
+ this.pruneExpiredCacheEntries(now);
18
52
  const cached = this.cache.get(kid);
19
53
  if (cached && cached.expiresAt > now) {
20
54
  return cached.key;
21
55
  }
56
+ const fetchGeneration = this.lifecycleGeneration;
22
57
  const keys = await this.fetchKeys();
58
+ if (fetchGeneration !== this.lifecycleGeneration) {
59
+ throw new JwtConfigurationError('JWKS client was disposed while fetching keys.');
60
+ }
23
61
  const jwk = keys.find(entry => entry.kid === kid);
24
62
  if (!jwk) {
25
63
  throw new JwtInvalidTokenError('JWT key id was not found in JWKS.');
@@ -33,18 +71,38 @@ export class JwksClient {
33
71
  } catch {
34
72
  throw new JwtConfigurationError('Unable to parse JWKS key into a public key.');
35
73
  }
36
- this.cache.set(kid, {
37
- expiresAt: now + this.cacheTtl,
38
- key
39
- });
74
+ if (this.cacheTtl > 0 && fetchGeneration === this.lifecycleGeneration) {
75
+ this.cache.set(kid, {
76
+ expiresAt: now + this.cacheTtl,
77
+ key
78
+ });
79
+ this.evictOldestCacheEntries();
80
+ }
40
81
  return key;
41
82
  }
83
+ pruneExpiredCacheEntries(now) {
84
+ for (const [kid, cached] of this.cache) {
85
+ if (cached.expiresAt <= now) {
86
+ this.cache.delete(kid);
87
+ }
88
+ }
89
+ }
90
+ evictOldestCacheEntries() {
91
+ while (this.cache.size > this.cacheMaxEntries) {
92
+ const oldestKid = this.cache.keys().next().value;
93
+ if (oldestKid === undefined) {
94
+ return;
95
+ }
96
+ this.cache.delete(oldestKid);
97
+ }
98
+ }
42
99
  async fetchKeys() {
43
100
  let response;
44
101
  const controller = new AbortController();
45
102
  const timeout = setTimeout(() => {
46
103
  controller.abort();
47
104
  }, this.requestTimeoutMs);
105
+ timeout.unref?.();
48
106
  try {
49
107
  response = await fetch(this.uri, {
50
108
  signal: controller.signal
@@ -23,6 +23,14 @@ export declare class DefaultJwtVerifier {
23
23
  private readonly refreshVerificationOptions;
24
24
  constructor(options: JwtVerifierOptions);
25
25
  verifyAccessToken(token: string): Promise<JwtPrincipal>;
26
+ /**
27
+ * Releases verifier-owned remote JWKS cache entries.
28
+ *
29
+ * Call this when disposing a manually managed verifier or replacing its remote
30
+ * identity-provider configuration. Static keys and provider callbacks are not
31
+ * owned by the verifier and are therefore left untouched.
32
+ */
33
+ dispose(): void;
26
34
  /**
27
35
  * Verifies a JWT access token with per-call claim-policy overrides while reusing configured key sources.
28
36
  *
@@ -1 +1 @@
1
- {"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/signing/verifier.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAA0B,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAG1G;;GAEG;AACH,eAAO,MAAM,WAAW,eAAiC,CAAC;AAE1D;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAI3D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAOjE,CAAC;AAmDF,KAAK,gCAAgC,GAAG,IAAI,CAC1C,kBAAkB,EAClB,YAAY,GAAG,UAAU,GAAG,kBAAkB,GAAG,QAAQ,GAAG,QAAQ,GAAG,YAAY,CACpF,CAAC;AA+LF;;GAEG;AACH,qBACa,kBAAkB;IAMjB,OAAO,CAAC,QAAQ,CAAC,OAAO;IALpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAyB;IACpD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAqB;IAC/D,OAAO,CAAC,QAAQ,CAAC,0BAA0B,CAAiC;gBAE/C,OAAO,EAAE,kBAAkB;IAalD,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAI7D;;;;;;;;;;OAUG;IACG,8BAA8B,CAClC,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,OAAO,CAAC,gCAAgC,CAAC,GACnD,OAAO,CAAC,YAAY,CAAC;IAqBlB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAQ9D,OAAO,CAAC,gCAAgC;YAsB1B,WAAW;IA+BzB,OAAO,CAAC,kBAAkB;YAUZ,oBAAoB;YAgBpB,wBAAwB;YAsBxB,8BAA8B;YAsB9B,kBAAkB;IAWhC,OAAO,CAAC,mBAAmB;IAwB3B,OAAO,CAAC,oBAAoB;IA2B5B,OAAO,CAAC,yBAAyB;YAiBnB,oBAAoB;CAOnC"}
1
+ {"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/signing/verifier.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAA0B,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAG1G;;GAEG;AACH,eAAO,MAAM,WAAW,eAAiC,CAAC;AAE1D;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAI3D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAOjE,CAAC;AAmDF,KAAK,gCAAgC,GAAG,IAAI,CAC1C,kBAAkB,EAClB,YAAY,GAAG,UAAU,GAAG,kBAAkB,GAAG,QAAQ,GAAG,QAAQ,GAAG,YAAY,CACpF,CAAC;AA+LF;;GAEG;AACH,qBACa,kBAAkB;IAMjB,OAAO,CAAC,QAAQ,CAAC,OAAO;IALpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAyB;IACpD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAqB;IAC/D,OAAO,CAAC,QAAQ,CAAC,0BAA0B,CAAiC;gBAE/C,OAAO,EAAE,kBAAkB;IAalD,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAI7D;;;;;;OAMG;IACH,OAAO,IAAI,IAAI;IAIf;;;;;;;;;;OAUG;IACG,8BAA8B,CAClC,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,OAAO,CAAC,gCAAgC,CAAC,GACnD,OAAO,CAAC,YAAY,CAAC;IAqBlB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAQ9D,OAAO,CAAC,gCAAgC;YAsB1B,WAAW;IA+BzB,OAAO,CAAC,kBAAkB;YAUZ,oBAAoB;YAgBpB,wBAAwB;YAsBxB,8BAA8B;YAsB9B,kBAAkB;IAWhC,OAAO,CAAC,mBAAmB;IAwB3B,OAAO,CAAC,oBAAoB;IA2B5B,OAAO,CAAC,yBAAyB;YAiBnB,oBAAoB;CAOnC"}
@@ -206,7 +206,7 @@ class DefaultJwtVerifier {
206
206
  constructor(options) {
207
207
  this.options = options;
208
208
  assertJwtAlgorithms(options.algorithms, 'JWT verifier');
209
- this.jwksClient = options.jwksUri ? new JwksClient(options.jwksUri, options.jwksCacheTtl, options.jwksRequestTimeoutMs) : undefined;
209
+ this.jwksClient = options.jwksUri ? new JwksClient(options.jwksUri, options.jwksCacheTtl, options.jwksRequestTimeoutMs, options.jwksCacheMaxEntries) : undefined;
210
210
  this.keyResolutionState = createKeyResolutionState(options.keys);
211
211
  this.refreshVerificationOptions = options.refreshToken ? this.createRefreshVerificationOptions(normalizeRefreshTokenOptions(options.refreshToken)) : undefined;
212
212
  this.refreshKeyResolutionState = createKeyResolutionState(this.refreshVerificationOptions?.keys);
@@ -215,6 +215,17 @@ class DefaultJwtVerifier {
215
215
  return this.verifyToken(token, this.options, this.keyResolutionState, this.jwksClient);
216
216
  }
217
217
 
218
+ /**
219
+ * Releases verifier-owned remote JWKS cache entries.
220
+ *
221
+ * Call this when disposing a manually managed verifier or replacing its remote
222
+ * identity-provider configuration. Static keys and provider callbacks are not
223
+ * owned by the verifier and are therefore left untouched.
224
+ */
225
+ dispose() {
226
+ this.jwksClient?.dispose();
227
+ }
228
+
218
229
  /**
219
230
  * Verifies a JWT access token with per-call claim-policy overrides while reusing configured key sources.
220
231
  *
@@ -326,7 +337,7 @@ class DefaultJwtVerifier {
326
337
  assertFiniteNumericDateClaim(payload, 'exp');
327
338
  assertFiniteNumericDateClaim(payload, 'nbf');
328
339
  this.validateMaxAgeClaims(payload, options.maxAge, clockSkew, now);
329
- if (typeof payload.exp === 'number' && payload.exp + clockSkew < now) {
340
+ if (typeof payload.exp === 'number' && payload.exp + clockSkew <= now) {
330
341
  throw new JwtExpiredTokenError();
331
342
  }
332
343
  if (typeof payload.nbf === 'number' && payload.nbf - clockSkew > now) {
package/dist/types.d.ts CHANGED
@@ -25,6 +25,8 @@ export interface JwtVerifierOptions {
25
25
  global?: boolean;
26
26
  issuer?: string;
27
27
  jwksCacheTtl?: number;
28
+ /** Maximum number of remote JWKS public keys retained in the in-memory cache. Defaults to `100`. */
29
+ jwksCacheMaxEntries?: number;
28
30
  jwksRequestTimeoutMs?: number;
29
31
  jwksUri?: string;
30
32
  keys?: JwtKeyEntry[];
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAEnH;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,YAAY,EAAE,CAAC;IAC3B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACrH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,YAAY,CAAC,EAAE,mBAAmB,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,SAAU,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACxD,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CACzD;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACrD"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "signing",
10
10
  "verification"
11
11
  ],
12
- "version": "1.0.0",
12
+ "version": "1.0.1",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -36,9 +36,9 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@fluojs/core": "^1.0.0",
40
- "@fluojs/di": "^1.0.0",
41
- "@fluojs/runtime": "^1.0.0"
39
+ "@fluojs/di": "^1.0.3",
40
+ "@fluojs/core": "^1.0.3",
41
+ "@fluojs/runtime": "^1.1.1"
42
42
  },
43
43
  "devDependencies": {
44
44
  "vitest": "^3.2.4"