@fluojs/jwt 1.0.3 → 2.0.0

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
@@ -2,6 +2,8 @@
2
2
 
3
3
  <p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
4
4
 
5
+ Node.js 지원 범위는 `>=24.0.0 <27`입니다. 업그레이드 절차는 [Node.js 지원 및 마이그레이션](../../docs/reference/node-support.ko.md)을 참조하세요.
6
+
5
7
  HTTP에 독립적인 JWT 토큰 코어로, 액세스 토큰의 서명 및 검증을 담당하며 검증된 결과를 정규화된 `JwtPrincipal` 객체로 변환합니다.
6
8
 
7
9
  ## 목차
@@ -61,38 +63,47 @@ JWT 설정이 다른 provider에서 와야 한다면, `JwtModule.forRootAsync(..
61
63
 
62
64
  `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
65
 
64
- ```typescript
65
- import { Module, type Token } from '@fluojs/core';
66
- import { JwtModule } from '@fluojs/jwt';
66
+ 지원되는 계약은 `JwtModule.forRootAsync({ inject, useFactory, global? })`입니다. `JwtModule.forRootAsync(...)`의 `inject`에 지정한 의존성은 JWT options provider가 resolve되기 전에 application module graph에 먼저 등록해야 하며, `useFactory`는 최종 `JwtVerifierOptions`를 반환합니다. 최상위 `global?`은 반환된 module의 가시성을 제어하며, `useFactory`가 반환하는 최종 `JwtVerifierOptions`와는 별개입니다. NestJS dynamic-module `imports`, `useClass`, `useExisting`은 지원되는 typed configuration의 일부가 아니며 dynamic-module 의미도 없습니다. 추가 JavaScript object property는 runtime에서 읽지 않을 뿐 validate하거나 reject하지 않습니다. `JwtModule.forRootAsync(...)`의 의존성은 global로 visible한 module export 또는 `JwtRuntimeModule`이 resolve할 수 있는 application graph의 bootstrap runtime provider에서 와야 합니다. ordinary sibling 또는 parent module의 export만으로는 충분하지 않으며, `AuthModule.providers`에만 local인 provider는 JWT options provider에서 보이지 않습니다. `JwtModule.forRootAsync(...)`는 암묵적 module 또는 provider discovery를 지원하지 않습니다.
67
67
 
68
- const JWT_SETTINGS = Symbol('jwt-settings');
68
+ ```typescript
69
+ import { Module } from '@fluojs/core';
70
+ import { ConfigModule, ConfigService } from '@fluojs/config';
71
+ import { JwtModule, type JwtVerifierOptions } from '@fluojs/jwt';
69
72
 
70
73
  @Module({
71
74
  imports: [
72
- JwtModule.forRootAsync({
73
- inject: [JWT_SETTINGS],
74
- useFactory: async (settings) => ({
75
- accessTokenTtlSeconds: 900,
76
- algorithms: ['HS256'],
77
- audience: 'my-app',
78
- issuer: settings.issuer,
79
- secret: settings.secret,
80
- }),
75
+ ConfigModule.forRoot({
76
+ processEnv: { JWT_SECRET: process.env.JWT_SECRET },
81
77
  }),
82
- ],
83
- providers: [
84
- {
85
- provide: JWT_SETTINGS as Token<{ issuer: string; secret: string }>,
86
- useValue: {
87
- issuer: 'my-api',
88
- secret: 'your-secure-secret',
78
+ JwtModule.forRootAsync({
79
+ inject: [ConfigService],
80
+ useFactory: async (...deps: unknown[]): Promise<JwtVerifierOptions> => {
81
+ const [config] = deps;
82
+ if (!(config instanceof ConfigService)) {
83
+ throw new TypeError('ConfigService dependency is required');
84
+ }
85
+
86
+ const secret = config.snapshot()['JWT_SECRET'];
87
+ if (typeof secret !== 'string') {
88
+ throw new TypeError('JWT_SECRET must be a string');
89
+ }
90
+
91
+ return {
92
+ accessTokenTtlSeconds: 900,
93
+ algorithms: ['HS256'],
94
+ audience: 'my-app',
95
+ issuer: 'my-api',
96
+ secret,
97
+ };
89
98
  },
90
- },
99
+ }),
91
100
  ],
92
101
  })
93
102
  export class AuthModule {}
94
103
  ```
95
104
 
105
+ 여기서는 `ConfigModule.forRoot(...)`가 기본으로 `ConfigService`를 global export하므로 `JwtRuntimeModule` options provider가 이를 resolve할 수 있습니다. `AuthModule.providers`에만 선언한 provider는 그 imported module에서 보이지 않습니다.
106
+
96
107
  ### 토큰 서명 및 검증
97
108
 
98
109
  `DefaultJwtSigner`를 주입받아 토큰을 발행하고, `DefaultJwtVerifier`를 통해 검증합니다.
@@ -150,28 +161,65 @@ const verifier = new DefaultJwtVerifier({
150
161
 
151
162
  `jwksRequestTimeoutMs`의 기본값은 `5_000`이며, 예산을 넘기면 진행 중인 JWKS fetch를 abort합니다.
152
163
 
153
- JWKS key는 `jwksCacheTtl` 밀리초 동안 cache되며 기본값은 `600_000`입니다. in-memory cache는 `jwksCacheMaxEntries`로 제한되고 기본값은 `100`입니다. lookup 전 만료된 entry를 정리하고, 제한을 넘으면 가장 오래 보관된 key를 제거합니다. `JwtModule`은 관리 중인 `DefaultJwtVerifier` shutdown hook을 호출하므로 module teardown 중 보관 중인 remote key material이 정리됩니다. 수동으로 생성한 verifier나 client는 수동 shutdown 또는 identity-provider 재설정 시 여전히 `JwksClient.dispose()` / `DefaultJwtVerifier.dispose()`를 호출해야 합니다. `jwksCacheTtl`을 `0`으로 설정하면 bounded fetch timeout은 유지하면서 key 보관만 비활성화합니다.
164
+ JWKS key는 `jwksCacheTtl` 밀리초 동안 cache되며 기본값은 `600_000`입니다. in-memory cache는 `jwksCacheMaxEntries`로 제한되고 기본값은 `100`입니다. lookup 전 만료된 entry를 정리하고, 제한을 넘으면 가장 오래 보관된 key를 제거합니다. `JwtModule`은 관리 중인 `DefaultJwtVerifier` shutdown hook을 호출하므로 module teardown 중 보관 중인 remote key material이 정리됩니다. 수동으로 생성한 verifier나 client는 수동 shutdown 또는 identity-provider 재설정 시 여전히 `JwksClient.dispose()` / `DefaultJwtVerifier.dispose()`를 호출해야 합니다. 이 dispose method들은 보관 중인 JWKS key material을 정리하고 진행 중인 JWKS fetch를 abort합니다. `jwksCacheTtl`을 `0`으로 설정하면 bounded fetch timeout은 유지하면서 key 보관만 비활성화합니다.
154
165
 
155
- `JwtService.verify(token, options)`는 호출 단위의 알고리즘/클레임 정책 재정의(`issuer`, `audience`, `clockSkewSeconds`, `maxAge`, `requireExp`)를 적용하더라도, 내부 JWKS client나 정적 key-resolution cache를 다시 만들지 않습니다. 호출 단위 검증은 `jwksUri`, `keys[]`, `publicKey`, `secret`, `secretOrKeyProvider` 같은 구성된 key source 자체를 교체하지는 않습니다.
166
+ `DefaultJwtVerifier.verifyAccessTokenWithOverrides(token, options)`는 호출 단위의 알고리즘/클레임 정책 재정의(`algorithms`, `issuer`, `audience`, `clockSkewSeconds`, `maxAge`, `requireExp`)를 적용하더라도, 내부 JWKS client나 정적 key-resolution cache를 다시 만들지 않습니다. 호출 단위 검증은 `jwksUri`, `keys[]`, `publicKey`, `secret`, `secretOrKeyProvider` 같은 구성된 key source 자체를 교체하지는 않습니다.
156
167
 
157
- 호환되는 키가 여러 개 설정되어 있으면 `kid`가 검증 키를 구분합니다. 호환되는 정적 키가 하나뿐이면 `kid` 없이도 토큰을 검증할 수 있고, JWKS 기반 검증은 원격 key set과 cache policy를 따릅니다.
168
+ 호환되는 키가 여러 개 설정되어 있으면 `kid`가 검증 키를 구분합니다. `keys[]`의 모든 entry는 비어 있지 않고 고유한 `kid`를 가져야 합니다. `DefaultJwtSigner`와 `DefaultJwtVerifier`는 key rotation 중 서명과 검증이 서로 다른 키를 선택하지 않도록 construction 시점에 빈 값 또는 중복 값을 `JwtConfigurationError`로 거부합니다. 호환되는 정적 키가 하나뿐이면 `kid` 없이도 토큰을 검증할 수 있고, JWKS 기반 검증은 원격 key set과 cache policy를 따릅니다.
158
169
 
159
170
  멀티테넌트 시스템에서는 발행된 토큰 header에 tenant-specific `kid`를 넣고 호환되는 key source를 미리 구성하는 방식을 권장합니다. `secretOrKeyProvider`는 decoded token header만 인자로 받으므로, request header, route param, 기타 request-context tenant hint는 JWT verifier 호출 전에 애플리케이션 수준 strategy/guard 코드에서 처리해야 합니다.
160
171
 
161
172
  ### 리프레시 토큰
162
173
 
163
- `RefreshTokenService`는 전용 HMAC refresh-token 경로를 사용합니다. `refreshToken.secret`은 access-token 서명 키와 별도로 설정하세요. Rotation은 `RefreshTokenStore.rotate(...)`를 사용해 현재 토큰을 소비 처리하고 대체 토큰을 같은 durable store 작업 안에서 저장할 있습니다. 따라서 성공한 rotation은 저장된 후속 토큰 없이 기존 토큰만 소비하지 않습니다. 기존 atomic `consume(...)` hook만 구현한 store도 계속 지원하지만, 대체 토큰 저장의 내구성은 store가 소유한 `rotate(...)` 작업에 달려 있습니다.
174
+ `RefreshTokenService`는 전용 HMAC refresh-token 경로를 사용합니다. `refreshToken.secret`은 access-token 서명 키와 별도로 설정하세요. access-token `algorithms`가 비대칭 알고리즘만 허용할 때는 `refreshToken.algorithms`에 `['HS256']` 같은 명시적인 HMAC allowlist를 설정하세요. 그렇지 않으면 기존 구성과의 호환성을 위해 top-level HMAC algorithms에서 refresh policy를 파생합니다. policy는 refresh-token 서명과 검증에만 적용되므로 access-token 검증 범위를 넓히지 않습니다.
175
+
176
+ ```typescript
177
+ const options = {
178
+ algorithms: ['RS256'],
179
+ privateKey: '...private PEM...',
180
+ publicKey: '...public PEM...',
181
+ refreshToken: {
182
+ algorithms: ['HS256'],
183
+ secret: 'refresh-secret',
184
+ expiresInSeconds: 300,
185
+ rotation: false,
186
+ store,
187
+ },
188
+ };
189
+ ```
190
+
191
+ Rotation은 `RefreshTokenStore.rotate(...)`를 사용해 현재 토큰을 소비 처리하고 대체 토큰을 같은 durable store 작업 안에서 저장할 수 있습니다. 따라서 성공한 rotation은 저장된 후속 토큰 없이 기존 토큰만 소비하지 않습니다. 기존 atomic `consume(...)` hook만 구현한 store도 계속 지원합니다. Consume이 성공하면 service가 `save(...)`로 대체 토큰을 저장하지만, 두 쓰기를 원자적으로 만드는 경로는 store 소유의 `rotate(...)`뿐입니다.
192
+
193
+ 재사용을 감지하면 optional `revokeByFamily(family)` capability를 구현한 store는 침해된 token family만 revoke합니다. 기존 store는 source-compatible 상태를 유지합니다. `revokeByFamily(...)`가 없으면 `RefreshTokenService`는 보수적으로 `revokeBySubject(subject)`로 fallback하며, 이 경우 해당 subject의 독립적인 refresh-token family도 함께 revoke됩니다. 다른 family가 침해된 뒤에도 별도 device 또는 session family를 유지해야 하는 production store는 `revokeByFamily(...)`를 구현하세요.
194
+
195
+ 단일 세션 로그아웃에서는 caller가 제시한 compact token을 `revokePresentedRefreshToken(...)`에 전달하세요.
196
+
197
+ ```typescript
198
+ await refreshTokens.revokePresentedRefreshToken(refreshToken);
199
+ ```
200
+
201
+ 이 method는 일치하는 record를 revoke하기 전에 signature, expiry, `type`, `jti`, `family`, `sub` claim을 검증합니다. `revokeRefreshToken(tokenId)`는 신뢰할 수 있는 record ID를 이미 가진 caller를 위한 API로 유지됩니다. raw compact token을 이 ID 기반 method에 전달하지 마세요.
164
202
 
165
203
  ## 설정 가드레일
166
204
 
167
- JWT 서명과 검증에는 `algorithms`에 지원되는 알고리즘이 하나 이상 필요합니다. 기본 signer는 `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`를 지원하며, 빈 알고리즘 목록은 모호한 토큰을 발행하거나 수락하지 않도록 즉시 실패합니다.
205
+ JWT 서명과 access-token 검증에는 `algorithms`에 지원되는 알고리즘이 하나 이상 필요합니다. Refresh-token 서명과 검증은 `refreshToken.algorithms`가 설정되어 있으면 이를 사용하고, 설정되지 않으면 backward compatibility를 위해 top-level HMAC algorithms를 사용합니다. 기본 signer는 `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`를 지원하며, 빈 알고리즘 목록은 모호한 토큰을 발행하거나 수락하지 않도록 즉시 실패합니다.
168
206
 
169
207
  액세스 토큰 TTL도 양의 유한 숫자여야 합니다. `accessTokenTtlSeconds`를 생략하면 `DefaultJwtSigner`는 문서화된 기본값인 `3600`초를 사용합니다. 소수 초는 JWT NumericDate `exp` 클레임에 그대로 보존됩니다. `0`, 음수 또는 유한하지 않은 값이 제공되면 토큰을 발행하기 전에 `JwtConfigurationError`로 실패합니다.
170
208
 
171
209
  검증은 잘못된 시간 정책에 대해 fail closed로 동작합니다. 검증에 참여하는 `exp`, `nbf`, `iat` 클레임은 유한한 JWT NumericDate 숫자여야 하며, `clockSkewSeconds`도 음수가 아닌 유한 숫자여야 합니다. 유한하지 않은 값은 expiration, not-before, age check를 늘리는 대신 거부됩니다. verifier 시간이 `exp` NumericDate에 도달하면 토큰은 만료된 것으로 처리되며, 양수 clock skew가 경계를 덮지 않는 한 equality도 만료로 간주합니다.
172
210
 
211
+ ### Node 런타임 경계
212
+
173
213
  루트 `@fluojs/jwt` import surface는 runtime-specific 인증 경로를 선택하기 전에도 안전하게 로드할 수 있습니다. Node.js `node:crypto` primitive는 서명, 검증, JWKS key parsing, refresh-token id 생성이 실제로 실행될 때만 lazy load됩니다. 이 방식은 기존 public export를 유지하면서 module import 시점의 Node-specific crypto 작업을 피합니다.
174
214
 
215
+ Lazy loading은 import-time 안전성 속성일 뿐입니다. 서명이나 검증을 runtime 간 이식 가능하게 만들지는 **않습니다**. 서명, 검증, JWKS key parsing, refresh-token id 생성 경로가 실행되면 해당 경로는 Node.js 호환 `node:crypto` 구현(`createHmac`, `createSign`, `createVerify`, `createPublicKey`, `timingSafeEqual`, `randomUUID`)을 필요로 합니다. Bun은 Node 호환성 레이어로 이를 만족하지만, Deno와 Cloudflare Workers는 이러한 연산에 호환되는 `node:crypto` surface를 제공하지 않으므로 지원되는 JWT 서명/검증 runtime이 아닙니다. `@fluojs/jwt`를 Node-runtime auth 패키지로 취급하세요. import-time 로딩은 lazy로 유지하되, 호환되는 `node:crypto` polyfill 없이 non-Node runtime에서 토큰을 서명하거나 검증할 수 있다고 가정하지 마세요.
216
+
217
+ ### `decode()` trust boundary
218
+
219
+ `JwtService.decode(token)`는 서명, `alg`, `exp`, `nbf`, `iss`, `aud` 또는 기타 클레임을 검증하지 않고 JWT payload segment를 읽습니다. 반환된 객체는 **검증되지 않은 입력(unverified input)**이며, 권한 결정(authorization decisions), 신원 확인(identity resolution), 또는 접근을 허가하는 모든 코드 경로에 사용해서는 안 됩니다. 검증된 클레임은 `JwtService.verify(token, options)`로 얻으세요. 정규화된 `JwtPrincipal`이 필요하면 호출 단위 재정의 없이 `DefaultJwtVerifier.verifyAccessToken(token)`을 사용하고, 호출 단위 `algorithms`, `audience`, `issuer`, `clockSkewSeconds`, `maxAge`, `requireExp`를 보존해야 하면 `DefaultJwtVerifier.verifyAccessTokenWithOverrides(token, options)`을 사용하세요.
220
+
221
+ `decode()`는 진단(diagnostics) 및 비권위적 검사(non-authoritative inspection)에만 사용됩니다. 예를 들어 로깅을 위해 토큰 메타데이터를 읽거나 `verify()` 호출 전에 검증 키를 선택할 때 사용할 수 있습니다. `decode()` 출력에서 읽은 모든 클레임 값 — `sub`, `roles`, `scopes`, `iss`, `aud`, `exp` 포함 — 은 `verify()`가 성공하기 전까지 공격자가 제어한 값으로 취급해야 합니다. `decode()` 출력을 기준으로 요청을 허가하거나 거부하는 분기를 만들지 말고, 검증되지 않은 클레임을 검증된 것처럼 downstream 코드에 노출하지 마세요.
222
+
175
223
  ## 공개 API 개요
176
224
 
177
225
  ### 주요 클래스
@@ -180,22 +228,24 @@ JWT 서명과 검증에는 `algorithms`에 지원되는 알고리즘이 하나
180
228
  - `DefaultJwtVerifier`: 토큰 검증 및 정규화를 담당하는 클래스입니다.
181
229
  - `JwtService`: 서명과 검증 기능을 결합한 편의용 파사드(facade)입니다.
182
230
  - `JwksClient`: 제한된 요청 시간 안에서 원격 JWKS 키를 가져오고 캐싱합니다.
183
- - `RefreshTokenService`: `refreshToken` 옵션이 구성된 경우 refresh token을 발행, 회전, 폐기합니다.
231
+ - `RefreshTokenService`: `refreshToken` 옵션이 구성된 경우 refresh token을 발행, 회전, 폐기합니다. `revokePresentedRefreshToken(...)`은 compact refresh token을 검증한 뒤 record를 revoke하며, `revokeRefreshToken(tokenId)`는 신뢰된 ID를 받는 대안입니다.
184
232
 
185
233
  ### 타입
186
234
  - `JwtPrincipal`: 정규화된 사용자 식별 객체 (`subject`, `roles`, `scopes`, `claims`).
187
235
  - `JwtVerifierOptions`: 알고리즘, 키, 검증 정책 설정을 위한 타입입니다.
188
236
  - `SignOptions`, `VerifyOptions`: 호출 단위 서명 및 검증 재정의 타입입니다.
189
237
  - `JwtClaims`, `JwtSigner`, `JwtVerifier`, `JwtKeyEntry`, `JwtAlgorithm`: 공개 서명 및 검증 계약입니다.
190
- - `RefreshTokenOptions`, `RefreshTokenStore`, `RefreshTokenRecord`, `RefreshTokenConsumeInput`, `RefreshTokenRotateInput`, `RefreshTokenConsumeResult`: refresh-token 저장, rotation, replay detection 계약입니다.
238
+ - `RefreshTokenOptions`, `RefreshTokenStore`, `RefreshTokenRecord`, `RefreshTokenConsumeInput`, `RefreshTokenRotateInput`, `RefreshTokenConsumeResult`: refresh-token 저장, rotation, family-scoped revocation, replay detection 계약입니다. `RefreshTokenStore.revokeByFamily(...)`는 subject-revocation store와의 호환성을 위해 optional입니다.
239
+ - `JwtPlatformStatusSnapshot`, `JwtStatusAdapterInput`: platform diagnostic helper와 함께 export되는 status snapshot 및 adapter input 타입입니다.
191
240
 
192
241
  ### 에러와 diagnostics
193
242
  - `JwtVerificationError`, `JwtInvalidTokenError`, `JwtExpiredTokenError`, `JwtConfigurationError`: 타입이 지정된 JWT 실패입니다.
194
243
  - `createJwtPlatformStatusSnapshot(...)`, `createJwtPlatformDiagnosticIssues(...)`: status 및 diagnostic helper입니다.
195
- - `JWT_OPTIONS`, `HMAC_HASH`, `ASYMMETRIC_HASH`: 모듈과 검증 레이어에서 사용하는 export token/constant입니다.
244
+ - `JWT_OPTIONS`, `HMAC_HASH`, `ASYMMETRIC_HASH`: 모듈과 검증 레이어에서 사용하는 export token/constant입니다. `HMAC_HASH`와 `ASYMMETRIC_HASH`는 readonly lookup 값이므로 변경하지 마세요.
196
245
 
197
246
  ### Deprecated compatibility helper
198
247
  - `normalizeRefreshTokenOptions(...)`: 기존 caller의 root import 호환성만을 위해 유지됩니다. package normalization 내부 helper를 직접 호출하기보다 `JwtModule.forRoot(...)` / `JwtModule.forRootAsync(...)`와 `RefreshTokenService`를 사용하세요.
248
+ - `createJwtCoreProviders(...)`: 기존 direct module composition caller의 root import 호환성만을 위해 유지됩니다. registration이 published module surface와 정렬되도록 `JwtModule.forRoot(...)` / `JwtModule.forRootAsync(...)`를 사용하세요.
199
249
 
200
250
  ## 관련 패키지
201
251
 
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  <p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
4
4
 
5
+ Node.js support is `>=24.0.0 <27`. See [Node.js support and migration](../../docs/reference/node-support.md) before upgrading.
6
+
5
7
  HTTP-agnostic JWT token core that handles signing access tokens and verifying them into a normalized `JwtPrincipal`.
6
8
 
7
9
  ## Table of Contents
@@ -61,38 +63,47 @@ Async registration exports the same JWT provider surface as the synchronous path
61
63
 
62
64
  `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
65
 
64
- ```typescript
65
- import { Module, type Token } from '@fluojs/core';
66
- import { JwtModule } from '@fluojs/jwt';
66
+ The supported contract is `JwtModule.forRootAsync({ inject, useFactory, global? })`: dependencies named by `inject` must already be registered in the application module graph before the JWT options provider resolves, and `useFactory` returns the final `JwtVerifierOptions`. The top-level `global?` controls returned module visibility and is distinct from the final `JwtVerifierOptions` returned by `useFactory`. NestJS dynamic-module `imports`, `useClass`, and `useExisting` are not part of the supported typed configuration and have no dynamic-module semantics; extra JavaScript object properties are unread at runtime, not validated or rejected. For `JwtModule.forRootAsync(...)`, dependencies must come from a globally visible module export or bootstrap runtime providers in the application graph that `JwtRuntimeModule` can resolve. An ordinary sibling or parent module export alone, and a provider local only to `AuthModule.providers`, are not visible to the JWT options provider. `JwtModule.forRootAsync(...)` performs no implicit module or provider discovery.
67
67
 
68
- const JWT_SETTINGS = Symbol('jwt-settings');
68
+ ```typescript
69
+ import { Module } from '@fluojs/core';
70
+ import { ConfigModule, ConfigService } from '@fluojs/config';
71
+ import { JwtModule, type JwtVerifierOptions } from '@fluojs/jwt';
69
72
 
70
73
  @Module({
71
74
  imports: [
72
- JwtModule.forRootAsync({
73
- inject: [JWT_SETTINGS],
74
- useFactory: async (settings) => ({
75
- accessTokenTtlSeconds: 900,
76
- algorithms: ['HS256'],
77
- audience: 'my-app',
78
- issuer: settings.issuer,
79
- secret: settings.secret,
80
- }),
75
+ ConfigModule.forRoot({
76
+ processEnv: { JWT_SECRET: process.env.JWT_SECRET },
81
77
  }),
82
- ],
83
- providers: [
84
- {
85
- provide: JWT_SETTINGS as Token<{ issuer: string; secret: string }>,
86
- useValue: {
87
- issuer: 'my-api',
88
- secret: 'your-secure-secret',
78
+ JwtModule.forRootAsync({
79
+ inject: [ConfigService],
80
+ useFactory: async (...deps: unknown[]): Promise<JwtVerifierOptions> => {
81
+ const [config] = deps;
82
+ if (!(config instanceof ConfigService)) {
83
+ throw new TypeError('ConfigService dependency is required');
84
+ }
85
+
86
+ const secret = config.snapshot()['JWT_SECRET'];
87
+ if (typeof secret !== 'string') {
88
+ throw new TypeError('JWT_SECRET must be a string');
89
+ }
90
+
91
+ return {
92
+ accessTokenTtlSeconds: 900,
93
+ algorithms: ['HS256'],
94
+ audience: 'my-app',
95
+ issuer: 'my-api',
96
+ secret,
97
+ };
89
98
  },
90
- },
99
+ }),
91
100
  ],
92
101
  })
93
102
  export class AuthModule {}
94
103
  ```
95
104
 
105
+ Here, `ConfigModule.forRoot(...)` exports `ConfigService` globally by default, so the `JwtRuntimeModule` options provider can resolve it. A provider declared only in `AuthModule.providers` is not visible to that imported module.
106
+
96
107
  ### Sign and Verify Tokens
97
108
 
98
109
  Inject `DefaultJwtSigner` to issue tokens and `DefaultJwtVerifier` to validate them.
@@ -150,28 +161,65 @@ const verifier = new DefaultJwtVerifier({
150
161
 
151
162
  `jwksRequestTimeoutMs` defaults to `5_000` and aborts the outbound JWKS fetch once that budget is exceeded.
152
163
 
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 `JwtModule` calls the managed `DefaultJwtVerifier` shutdown hook so retained remote key material is cleared during module teardown. Manually constructed verifiers or clients should still call `JwksClient.dispose()` / `DefaultJwtVerifier.dispose()` during manual shutdown or identity-provider reconfiguration. A `jwksCacheTtl` of `0` disables key retention while still using bounded fetch timeouts.
164
+ 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 `JwtModule` calls the managed `DefaultJwtVerifier` shutdown hook so retained remote key material is cleared during module teardown. Manually constructed verifiers or clients should still call `JwksClient.dispose()` / `DefaultJwtVerifier.dispose()` during manual shutdown or identity-provider reconfiguration. These dispose methods clear retained JWKS key material and abort active JWKS fetches. A `jwksCacheTtl` of `0` disables key retention while still using bounded fetch timeouts.
154
165
 
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`.
166
+ `DefaultJwtVerifier.verifyAccessTokenWithOverrides(token, options)` applies per-call algorithm and claim-policy overrides (`algorithms`, `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`.
156
167
 
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.
168
+ When multiple compatible keys are configured, `kid` disambiguates the verification key. Every `keys[]` entry must have a non-empty, unique `kid`; `DefaultJwtSigner` and `DefaultJwtVerifier` reject empty or duplicate values with `JwtConfigurationError` during construction so key rotation cannot select different keys for signing and verification. A single compatible static key can verify tokens without `kid`; JWKS-backed verification relies on the remote key set and its cache policy.
158
169
 
159
170
  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
171
 
161
172
  ### Refresh tokens
162
173
 
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.
174
+ `RefreshTokenService` uses a dedicated HMAC refresh-token path. Configure `refreshToken.secret` separately from access-token signing keys. Set `refreshToken.algorithms` to an explicit HMAC allowlist (for example, `['HS256']`) when the access-token `algorithms` list is asymmetric-only; otherwise, existing configurations continue to derive the refresh policy from the top-level HMAC algorithms. This policy applies only to refresh-token signing and verification, so it does not widen access-token verification.
175
+
176
+ ```typescript
177
+ const options = {
178
+ algorithms: ['RS256'],
179
+ privateKey: '...private PEM...',
180
+ publicKey: '...public PEM...',
181
+ refreshToken: {
182
+ algorithms: ['HS256'],
183
+ secret: 'refresh-secret',
184
+ expiresInSeconds: 300,
185
+ rotation: false,
186
+ store,
187
+ },
188
+ };
189
+ ```
190
+
191
+ 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: after a successful consume, the service saves the replacement through `save(...)`, but only store-owned `rotate(...)` makes those two writes atomic.
192
+
193
+ When reuse is detected, stores that implement the optional `revokeByFamily(family)` capability revoke only the compromised token family. Existing stores remain source-compatible: if `revokeByFamily(...)` is absent, `RefreshTokenService` conservatively falls back to `revokeBySubject(subject)`, which also revokes the subject's independent refresh-token families. Implement `revokeByFamily(...)` in production stores when separate device or session families must remain active after another family is compromised.
194
+
195
+ For single-session logout, pass the compact token that the caller presented to `revokePresentedRefreshToken(...)`:
196
+
197
+ ```typescript
198
+ await refreshTokens.revokePresentedRefreshToken(refreshToken);
199
+ ```
200
+
201
+ This verifies the signature, expiry, `type`, `jti`, `family`, and `sub` claims before revoking the matching record. `revokeRefreshToken(tokenId)` remains available for callers that already hold a trusted record ID; do not pass a raw compact token to that ID-based method.
164
202
 
165
203
  ## Configuration Guardrails
166
204
 
167
- JWT signing and verification require at least one supported algorithm in `algorithms`. The built-in signer supports `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, and `ES512`; configuration with an empty algorithm list fails fast instead of issuing or accepting ambiguous tokens.
205
+ JWT signing and access-token verification require at least one supported algorithm in `algorithms`. Refresh-token signing and verification use `refreshToken.algorithms` when configured, or the top-level HMAC algorithms for backward compatibility. The built-in signer supports `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, and `ES512`; configuration with an empty algorithm list fails fast instead of issuing or accepting ambiguous tokens.
168
206
 
169
207
  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.
170
208
 
171
209
  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.
172
210
 
211
+ ### Node runtime boundary
212
+
173
213
  The root `@fluojs/jwt` import surface is safe to load before selecting a runtime-specific authentication path: Node.js `node:crypto` primitives are loaded lazily only when signing, verification, JWKS key parsing, or refresh-token id generation actually executes. This preserves the existing public exports while avoiding Node-specific crypto work at module import time.
174
214
 
215
+ Lazy loading is an import-time safety property only. It does **not** make signing or verification portable across runtimes. Once a signing, verification, JWKS key-parsing, or refresh-token id-generation path executes, that path requires a Node.js-compatible `node:crypto` implementation (`createHmac`, `createSign`, `createVerify`, `createPublicKey`, `timingSafeEqual`, `randomUUID`). Bun satisfies this through its Node compatibility layer; Deno and Cloudflare Workers do not provide a compatible `node:crypto` surface for these operations and are not supported JWT signing/verification runtimes. Treat `@fluojs/jwt` as a Node-runtime auth package: keep import-time loading lazy, but do not assume the package can sign or verify tokens on non-Node runtimes without a compatible `node:crypto` polyfill.
216
+
217
+ ### `decode()` trust boundary
218
+
219
+ `JwtService.decode(token)` reads the JWT payload segment without verifying the signature, `alg`, `exp`, `nbf`, `iss`, `aud`, or any other claim. The returned object is **unverified input** and must never be used for authorization decisions, identity resolution, or any code path that grants access. Use `JwtService.verify(token, options)` to obtain verified claims. To obtain a normalized `JwtPrincipal`, use `DefaultJwtVerifier.verifyAccessToken(token)` without per-call overrides, or `DefaultJwtVerifier.verifyAccessTokenWithOverrides(token, options)` when preserving per-call `algorithms`, `audience`, `issuer`, `clockSkewSeconds`, `maxAge`, or `requireExp`.
220
+
221
+ `decode()` exists for diagnostics and non-authoritative inspection only, such as reading token metadata for logging or selecting a verification key before calling `verify()`. Any claim value read from `decode()` — including `sub`, `roles`, `scopes`, `iss`, `aud`, and `exp` — must be treated as attacker-controlled until `verify()` succeeds. Never branch on `decode()` output to allow or deny a request, and never expose decoded claims to downstream code as if they were verified.
222
+
175
223
  ## Public API Overview
176
224
 
177
225
  ### Core Classes
@@ -180,22 +228,24 @@ The root `@fluojs/jwt` import surface is safe to load before selecting a runtime
180
228
  - `DefaultJwtVerifier`: Handles token validation and normalization.
181
229
  - `JwtService`: A convenience facade combining signing and verification.
182
230
  - `JwksClient`: Fetches and caches remote JWKS keys with bounded request timeouts.
183
- - `RefreshTokenService`: Issues, rotates, and revokes refresh tokens when `refreshToken` options are configured.
231
+ - `RefreshTokenService`: Issues, rotates, and revokes refresh tokens when `refreshToken` options are configured. `revokePresentedRefreshToken(...)` verifies a compact refresh token before revoking its record; `revokeRefreshToken(tokenId)` is the trusted-ID alternative.
184
232
 
185
233
  ### Types
186
234
  - `JwtPrincipal`: The normalized identity object (`subject`, `roles`, `scopes`, `claims`).
187
235
  - `JwtVerifierOptions`: Configuration for algorithms, keys, and validation policy.
188
236
  - `SignOptions` and `VerifyOptions`: Per-call signing and verification overrides.
189
237
  - `JwtClaims`, `JwtSigner`, `JwtVerifier`, `JwtKeyEntry`, `JwtAlgorithm`: Public signing and verification contracts.
190
- - `RefreshTokenOptions`, `RefreshTokenStore`, `RefreshTokenRecord`, `RefreshTokenConsumeInput`, `RefreshTokenRotateInput`, and `RefreshTokenConsumeResult`: Refresh-token storage, rotation, and replay-detection contracts.
238
+ - `RefreshTokenOptions`, `RefreshTokenStore`, `RefreshTokenRecord`, `RefreshTokenConsumeInput`, `RefreshTokenRotateInput`, and `RefreshTokenConsumeResult`: Refresh-token storage, rotation, family-scoped revocation, and replay-detection contracts. `RefreshTokenStore.revokeByFamily(...)` is optional for compatibility with subject-revocation stores.
239
+ - `JwtPlatformStatusSnapshot` and `JwtStatusAdapterInput`: Status snapshot and adapter input types exported with the platform diagnostic helpers.
191
240
 
192
241
  ### Errors and diagnostics
193
242
  - `JwtVerificationError`, `JwtInvalidTokenError`, `JwtExpiredTokenError`, `JwtConfigurationError`: Typed JWT failures.
194
243
  - `createJwtPlatformStatusSnapshot(...)` and `createJwtPlatformDiagnosticIssues(...)`: Status and diagnostic helpers.
195
- - `JWT_OPTIONS`, `HMAC_HASH`, `ASYMMETRIC_HASH`: Exported tokens/constants used by the module and verification layer.
244
+ - `JWT_OPTIONS`, `HMAC_HASH`, `ASYMMETRIC_HASH`: Exported tokens/constants used by the module and verification layer. `HMAC_HASH` and `ASYMMETRIC_HASH` are readonly lookup values; do not mutate them.
196
245
 
197
246
  ### Deprecated compatibility helpers
198
247
  - `normalizeRefreshTokenOptions(...)`: Retained only for root-import compatibility with existing callers. Prefer `JwtModule.forRoot(...)` / `JwtModule.forRootAsync(...)` plus `RefreshTokenService` instead of calling package normalization internals.
248
+ - `createJwtCoreProviders(...)`: Retained only for root-import compatibility with existing direct module composition callers. Prefer `JwtModule.forRoot(...)` / `JwtModule.forRootAsync(...)` so registration stays aligned with the published module surface.
199
249
 
200
250
  ## Related Packages
201
251
 
package/dist/module.d.ts CHANGED
@@ -1,6 +1,15 @@
1
1
  import { type AsyncModuleOptions, type Constructor } from '@fluojs/core';
2
+ import type { Provider } from '@fluojs/di';
2
3
  import type { JwtVerifierOptions } from './types.js';
3
4
  type ModuleType = Constructor;
5
+ /**
6
+ * Creates the core JWT providers for advanced direct module composition.
7
+ *
8
+ * @deprecated Prefer {@link JwtModule.forRoot} or {@link JwtModule.forRootAsync} so JWT registration stays aligned with the published module surface.
9
+ * @param options JWT verification and signing options used for provider registration.
10
+ * @returns Providers for the JWT verifier, signer, facade, and optional refresh-token service.
11
+ */
12
+ export declare function createJwtCoreProviders(options: JwtVerifierOptions): Provider[];
4
13
  /**
5
14
  * Registers JWT services and optional refresh-token support for an application module.
6
15
  */
@@ -1 +1 @@
1
- {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAsD,MAAM,cAAc,CAAC;AAOrI,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIrD,KAAK,UAAU,GAAG,WAAW,CAAC;AAyE9B;;GAEG;AACH,qBAAa,SAAS;IACpB,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,kBAAkB,GAAG,UAAU;IAQvD,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,kBAAkB,CAAC,kBAAkB,CAAC,GAAG;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,UAAU;IASvG,OAAO,CAAC,MAAM,CAAC,YAAY;CAkB5B"}
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAA0C,MAAM,cAAc,CAAC;AAEzH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAK3C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIrD,KAAK,UAAU,GAAG,WAAW,CAAC;AAyE9B;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,kBAAkB,GAAG,QAAQ,EAAE,CAM9E;AAED;;GAEG;AACH,qBAAa,SAAS;IACpB,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,kBAAkB,GAAG,UAAU;IAQvD,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,kBAAkB,CAAC,kBAAkB,CAAC,GAAG;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,UAAU;IASvG,OAAO,CAAC,MAAM,CAAC,YAAY;CAkB5B"}
package/dist/module.js CHANGED
@@ -55,6 +55,21 @@ function createJwtModuleProviders(optionsProvider, includeRefreshTokenService, r
55
55
  return providers;
56
56
  }
57
57
 
58
+ /**
59
+ * Creates the core JWT providers for advanced direct module composition.
60
+ *
61
+ * @deprecated Prefer {@link JwtModule.forRoot} or {@link JwtModule.forRootAsync} so JWT registration stays aligned with the published module surface.
62
+ * @param options JWT verification and signing options used for provider registration.
63
+ * @returns Providers for the JWT verifier, signer, facade, and optional refresh-token service.
64
+ */
65
+ export function createJwtCoreProviders(options) {
66
+ return createJwtModuleProviders({
67
+ provide: JWT_OPTIONS,
68
+ scope: 'singleton',
69
+ useValue: options
70
+ }, Boolean(options.refreshToken), 'singleton');
71
+ }
72
+
58
73
  /**
59
74
  * Registers JWT services and optional refresh-token support for an application module.
60
75
  */
@@ -64,7 +79,7 @@ export class JwtModule {
64
79
  provide: JWT_OPTIONS,
65
80
  scope: 'singleton',
66
81
  useValue: options
67
- }, Boolean(options.refreshToken), Boolean(options.refreshToken), 'singleton', false, options.global ?? false);
82
+ }, true, true, options.refreshToken ? 'singleton' : 'transient', false, options.global ?? false);
68
83
  }
69
84
  static forRootAsync(options) {
70
85
  return this.createModule({
@@ -0,0 +1,22 @@
1
+ import type { DefaultJwtVerifier } from '../signing/verifier.js';
2
+ import type { JwtClaims } from '../types.js';
3
+ /**
4
+ * Describes the claims required for a verified refresh token.
5
+ */
6
+ export interface RefreshTokenClaims extends JwtClaims {
7
+ family: string;
8
+ jti: string;
9
+ type: 'refresh';
10
+ }
11
+ /**
12
+ * Verifies a compact refresh token and returns its required claims.
13
+ *
14
+ * @param verifier Configured verifier for the refresh-token policy.
15
+ * @param token Compact refresh token to verify.
16
+ * @returns Verified refresh-token claims with non-empty required identifiers.
17
+ * @throws {JwtInvalidTokenError} When the token is not a refresh token or lacks required claims.
18
+ */
19
+ export declare function verifyRefreshTokenClaims(verifier: DefaultJwtVerifier, token: string): Promise<RefreshTokenClaims & {
20
+ sub: string;
21
+ }>;
22
+ //# sourceMappingURL=refresh-token-claims.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh-token-claims.d.ts","sourceRoot":"","sources":["../../src/refresh/refresh-token-claims.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,SAAS;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,SAAS,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,kBAAkB,EAC5B,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,kBAAkB,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CA2B/C"}
@@ -0,0 +1,37 @@
1
+ import { JwtInvalidTokenError } from '../errors.js';
2
+
3
+ /**
4
+ * Describes the claims required for a verified refresh token.
5
+ */
6
+
7
+ /**
8
+ * Verifies a compact refresh token and returns its required claims.
9
+ *
10
+ * @param verifier Configured verifier for the refresh-token policy.
11
+ * @param token Compact refresh token to verify.
12
+ * @returns Verified refresh-token claims with non-empty required identifiers.
13
+ * @throws {JwtInvalidTokenError} When the token is not a refresh token or lacks required claims.
14
+ */
15
+ export async function verifyRefreshTokenClaims(verifier, token) {
16
+ const principal = await verifier.verifyRefreshToken(token);
17
+ const claims = principal.claims;
18
+ if (claims.type !== 'refresh') {
19
+ throw new JwtInvalidTokenError('JWT is not a refresh token.');
20
+ }
21
+ if (typeof claims.jti !== 'string' || claims.jti.length === 0) {
22
+ throw new JwtInvalidTokenError('Refresh token is missing jti.');
23
+ }
24
+ if (typeof claims.family !== 'string' || claims.family.length === 0) {
25
+ throw new JwtInvalidTokenError('Refresh token is missing family.');
26
+ }
27
+ if (typeof claims.sub !== 'string' || claims.sub.length === 0) {
28
+ throw new JwtInvalidTokenError('Refresh token is missing sub.');
29
+ }
30
+ return {
31
+ ...claims,
32
+ family: claims.family,
33
+ jti: claims.jti,
34
+ sub: claims.sub,
35
+ type: 'refresh'
36
+ };
37
+ }
@@ -1,5 +1,6 @@
1
1
  import type { DefaultJwtSigner } from '../signing/signer.js';
2
2
  import type { DefaultJwtVerifier } from '../signing/verifier.js';
3
+ import type { JwtAlgorithm } from '../types.js';
3
4
  /**
4
5
  * Describes the refresh token store contract.
5
6
  */
@@ -8,6 +9,7 @@ export interface RefreshTokenStore {
8
9
  find(tokenId: string): Promise<RefreshTokenRecord | undefined>;
9
10
  revoke(tokenId: string): Promise<void>;
10
11
  revokeBySubject(subject: string): Promise<void>;
12
+ revokeByFamily?(family: string): Promise<void>;
11
13
  consume?(input: RefreshTokenConsumeInput): Promise<RefreshTokenConsumeResult>;
12
14
  rotate?(input: RefreshTokenRotateInput): Promise<RefreshTokenConsumeResult>;
13
15
  }
@@ -45,11 +47,13 @@ export interface RefreshTokenRecord {
45
47
  * Describes the refresh token options contract.
46
48
  */
47
49
  export interface RefreshTokenOptions {
48
- secret: string;
49
- expiresInSeconds: number;
50
- verifyMaxAgeSeconds?: number;
51
- rotation: boolean;
52
- store: RefreshTokenStore;
50
+ /** HMAC algorithms allowed for refresh-token signing and verification. Defaults to HMAC algorithms from the top-level policy. */
51
+ readonly algorithms?: readonly Extract<JwtAlgorithm, 'HS256' | 'HS384' | 'HS512'>[];
52
+ readonly secret: string;
53
+ readonly expiresInSeconds: number;
54
+ readonly verifyMaxAgeSeconds?: number;
55
+ readonly rotation: boolean;
56
+ readonly store: RefreshTokenStore;
53
57
  }
54
58
  /**
55
59
  * Normalize refresh token options for legacy root-import callers.
@@ -77,10 +81,19 @@ export declare class RefreshTokenService {
77
81
  refreshToken: string;
78
82
  }>;
79
83
  revokeRefreshToken(tokenId: string): Promise<void>;
84
+ /**
85
+ * Revokes the record identified by a verified presented refresh token.
86
+ *
87
+ * @param token Compact refresh token to verify before its record is revoked.
88
+ * @returns A promise that resolves after the verified refresh-token record is revoked.
89
+ * @throws {JwtInvalidTokenError} When the token is malformed or lacks required refresh claims.
90
+ * @throws {JwtExpiredTokenError} When the refresh token has expired.
91
+ */
92
+ revokePresentedRefreshToken(token: string): Promise<void>;
80
93
  revokeAllForSubject(subject: string): Promise<void>;
81
94
  private issueRefreshTokenWithFamily;
82
95
  private consumeRefreshToken;
96
+ private revokeCompromisedFamily;
83
97
  private createRefreshTokenWithFamily;
84
- private verifyRefreshClaims;
85
98
  }
86
99
  //# sourceMappingURL=refresh-token.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"refresh-token.d.ts","sourceRoot":"","sources":["../../src/refresh/refresh-token.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAE7D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAAC;IAC/D,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,OAAO,CAAC,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAC9E,MAAM,CAAC,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC;CAC7E;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,IAAI,CAAC;CACX;AAED;;GAEG;AACH,MAAM,WAAW,uBAAwB,SAAQ,wBAAwB;IACvE,WAAW,EAAE,kBAAkB,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,UAAU,GAAG,cAAc,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAEvH;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,iBAAiB,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,mBAAmB,GAAG,SAAS,GAAG,mBAAmB,CA6B1G;AAQD;;GAEG;AACH,qBAAa,mBAAmB;IAK5B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAL3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;gBAG5C,OAAO,EAAE,mBAAmB,EACX,MAAM,EAAE,gBAAgB,EACxB,QAAQ,EAAE,kBAAkB;IAKzC,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAOnD,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;IA0EhG,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAI3C,2BAA2B;YAQ3B,mBAAmB;YAUnB,4BAA4B;YA+B5B,mBAAmB;CA4BlC"}
1
+ {"version":3,"file":"refresh-token.d.ts","sourceRoot":"","sources":["../../src/refresh/refresh-token.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAAC;IAC/D,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,cAAc,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,OAAO,CAAC,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAC9E,MAAM,CAAC,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC;CAC7E;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,IAAI,CAAC;CACX;AAED;;GAEG;AACH,MAAM,WAAW,uBAAwB,SAAQ,wBAAwB;IACvE,WAAW,EAAE,kBAAkB,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,UAAU,GAAG,cAAc,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAEvH;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,iIAAiI;IACjI,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,OAAO,CAAC,YAAY,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC,EAAE,CAAC;IACpF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;CACnC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,mBAAmB,GAAG,SAAS,GAAG,mBAAmB,CA2C1G;AAED;;GAEG;AACH,qBAAa,mBAAmB;IAK5B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAL3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;gBAG5C,OAAO,EAAE,mBAAmB,EACX,MAAM,EAAE,gBAAgB,EACxB,QAAQ,EAAE,kBAAkB;IAKzC,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAOnD,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;IA0EhG,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxD;;;;;;;OAOG;IACG,2BAA2B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMzD,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAI3C,2BAA2B;YAQ3B,mBAAmB;YAUnB,uBAAuB;YASvB,4BAA4B;CA8B3C"}
@@ -1,4 +1,6 @@
1
1
  import { JwtConfigurationError, JwtExpiredTokenError, JwtInvalidTokenError } from '../errors.js';
2
+ import { SUPPORTED_HMAC_HASH } from '../signing/algorithm-policy.js';
3
+ import { verifyRefreshTokenClaims } from './refresh-token-claims.js';
2
4
 
3
5
  /**
4
6
  * Describes the refresh token store contract.
@@ -39,6 +41,16 @@ export function normalizeRefreshTokenOptions(options) {
39
41
  if (!options) {
40
42
  throw new JwtConfigurationError('JWT refresh token options are not configured.');
41
43
  }
44
+ if (options.algorithms !== undefined) {
45
+ if (!Array.isArray(options.algorithms) || options.algorithms.length === 0) {
46
+ throw new JwtConfigurationError('JWT refresh token algorithms must contain at least one HMAC algorithm.');
47
+ }
48
+ for (const algorithm of options.algorithms) {
49
+ if (typeof algorithm !== 'string' || !Object.hasOwn(SUPPORTED_HMAC_HASH, algorithm)) {
50
+ throw new JwtConfigurationError(`JWT refresh token received unsupported algorithm "${String(algorithm)}"; only HS256, HS384, and HS512 are allowed.`);
51
+ }
52
+ }
53
+ }
42
54
  if (typeof options.secret !== 'string' || options.secret.length === 0) {
43
55
  throw new JwtConfigurationError('JWT refresh token secret must be a non-empty string.');
44
56
  }
@@ -55,6 +67,7 @@ export function normalizeRefreshTokenOptions(options) {
55
67
  ...options
56
68
  };
57
69
  }
70
+
58
71
  /**
59
72
  * Represents the refresh token service.
60
73
  */
@@ -73,7 +86,7 @@ export class RefreshTokenService {
73
86
  return this.issueRefreshTokenWithFamily(subject, family);
74
87
  }
75
88
  async rotateRefreshToken(currentToken) {
76
- const claims = await this.verifyRefreshClaims(currentToken);
89
+ const claims = await verifyRefreshTokenClaims(this.verifier, currentToken);
77
90
  if (this.options.rotation) {
78
91
  if (!this.options.store.rotate && !this.options.store.consume) {
79
92
  throw new JwtConfigurationError('Refresh token rotation requires an atomic store.rotate() or store.consume() implementation.');
@@ -104,7 +117,7 @@ export class RefreshTokenService {
104
117
  };
105
118
  }
106
119
  if (consumeResult === 'already_used') {
107
- await this.options.store.revokeBySubject(claims.sub);
120
+ await this.revokeCompromisedFamily(claims.sub, claims.family);
108
121
  throw new JwtInvalidTokenError('Refresh token reuse detected.');
109
122
  }
110
123
  if (consumeResult === 'expired') {
@@ -126,7 +139,7 @@ export class RefreshTokenService {
126
139
  throw new JwtExpiredTokenError('Refresh token has expired.');
127
140
  }
128
141
  if (record.used) {
129
- await this.options.store.revokeBySubject(record.subject);
142
+ await this.revokeCompromisedFamily(record.subject, record.family);
130
143
  throw new JwtInvalidTokenError('Refresh token reuse detected.');
131
144
  }
132
145
  const accessToken = await this.signer.signAccessToken({
@@ -140,6 +153,19 @@ export class RefreshTokenService {
140
153
  async revokeRefreshToken(tokenId) {
141
154
  await this.options.store.revoke(tokenId);
142
155
  }
156
+
157
+ /**
158
+ * Revokes the record identified by a verified presented refresh token.
159
+ *
160
+ * @param token Compact refresh token to verify before its record is revoked.
161
+ * @returns A promise that resolves after the verified refresh-token record is revoked.
162
+ * @throws {JwtInvalidTokenError} When the token is malformed or lacks required refresh claims.
163
+ * @throws {JwtExpiredTokenError} When the refresh token has expired.
164
+ */
165
+ async revokePresentedRefreshToken(token) {
166
+ const claims = await verifyRefreshTokenClaims(this.verifier, token);
167
+ await this.options.store.revoke(claims.jti);
168
+ }
143
169
  async revokeAllForSubject(subject) {
144
170
  await this.options.store.revokeBySubject(subject);
145
171
  }
@@ -157,6 +183,13 @@ export class RefreshTokenService {
157
183
  }
158
184
  return this.options.store.consume(input);
159
185
  }
186
+ async revokeCompromisedFamily(subject, family) {
187
+ if (this.options.store.revokeByFamily) {
188
+ await this.options.store.revokeByFamily(family);
189
+ return;
190
+ }
191
+ await this.options.store.revokeBySubject(subject);
192
+ }
160
193
  async createRefreshTokenWithFamily(subject, family) {
161
194
  const now = Math.floor(Date.now() / 1000);
162
195
  const {
@@ -186,27 +219,4 @@ export class RefreshTokenService {
186
219
  token
187
220
  };
188
221
  }
189
- async verifyRefreshClaims(token) {
190
- const principal = await this.verifier.verifyRefreshToken(token);
191
- const claims = principal.claims;
192
- if (claims.type !== 'refresh') {
193
- throw new JwtInvalidTokenError('JWT is not a refresh token.');
194
- }
195
- if (typeof claims.jti !== 'string' || claims.jti.length === 0) {
196
- throw new JwtInvalidTokenError('Refresh token is missing jti.');
197
- }
198
- if (typeof claims.family !== 'string' || claims.family.length === 0) {
199
- throw new JwtInvalidTokenError('Refresh token is missing family.');
200
- }
201
- if (typeof claims.sub !== 'string' || claims.sub.length === 0) {
202
- throw new JwtInvalidTokenError('Refresh token is missing sub.');
203
- }
204
- return {
205
- ...claims,
206
- family: claims.family,
207
- jti: claims.jti,
208
- sub: claims.sub,
209
- type: 'refresh'
210
- };
211
- }
212
222
  }
@@ -0,0 +1,4 @@
1
+ import type { JwtAlgorithm } from '../types.js';
2
+ export declare const SUPPORTED_HMAC_HASH: Readonly<Partial<Record<JwtAlgorithm, string>>>;
3
+ export declare const SUPPORTED_ASYMMETRIC_HASH: Readonly<Partial<Record<JwtAlgorithm, string>>>;
4
+ //# sourceMappingURL=algorithm-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"algorithm-policy.d.ts","sourceRoot":"","sources":["../../src/signing/algorithm-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAI9E,CAAC;AAEH,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAOpF,CAAC"}
@@ -0,0 +1,13 @@
1
+ export const SUPPORTED_HMAC_HASH = Object.freeze({
2
+ HS256: 'sha256',
3
+ HS384: 'sha384',
4
+ HS512: 'sha512'
5
+ });
6
+ export const SUPPORTED_ASYMMETRIC_HASH = Object.freeze({
7
+ RS256: 'sha256',
8
+ RS384: 'sha384',
9
+ RS512: 'sha512',
10
+ ES256: 'sha256',
11
+ ES384: 'sha384',
12
+ ES512: 'sha512'
13
+ });
@@ -8,6 +8,7 @@ export declare class JwksClient {
8
8
  private readonly requestTimeoutMs;
9
9
  private readonly cacheMaxEntries;
10
10
  private readonly cache;
11
+ private readonly activeFetchControllers;
11
12
  private lifecycleGeneration;
12
13
  constructor(uri: string, cacheTtl?: number, requestTimeoutMs?: number, cacheMaxEntries?: number);
13
14
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"jwks.d.ts","sourceRoot":"","sources":["../../src/signing/jwks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAiC7C;;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;IA2CpD,OAAO,CAAC,wBAAwB;IAQhC,OAAO,CAAC,uBAAuB;YAYjB,SAAS;CAsCxB"}
1
+ {"version":3,"file":"jwks.d.ts","sourceRoot":"","sources":["../../src/signing/jwks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAiC7C;;GAEG;AACH,qBAAa,UAAU;IAMnB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe;IARlC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4D;IAClF,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAA8B;IACrE,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;IAWf,OAAO,CAAC,YAAY;IAId,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IA2CpD,OAAO,CAAC,wBAAwB;IAQhC,OAAO,CAAC,uBAAuB;YAYjB,SAAS;CAwDxB"}
@@ -21,6 +21,7 @@ function assertPositiveInteger(value, label) {
21
21
  */
22
22
  export class JwksClient {
23
23
  cache = new Map();
24
+ activeFetchControllers = new Set();
24
25
  lifecycleGeneration = 0;
25
26
  constructor(uri, cacheTtl = 600_000, requestTimeoutMs = 5_000, cacheMaxEntries = DEFAULT_JWKS_CACHE_MAX_ENTRIES) {
26
27
  this.uri = uri;
@@ -41,6 +42,10 @@ export class JwksClient {
41
42
  dispose() {
42
43
  this.lifecycleGeneration += 1;
43
44
  this.cache.clear();
45
+ for (const controller of this.activeFetchControllers) {
46
+ controller.abort();
47
+ }
48
+ this.activeFetchControllers.clear();
44
49
  }
45
50
  isAbortError(error) {
46
51
  return error instanceof Error && error.name === 'AbortError';
@@ -99,36 +104,51 @@ export class JwksClient {
99
104
  }
100
105
  }
101
106
  async fetchKeys() {
102
- let response;
107
+ const fetchGeneration = this.lifecycleGeneration;
103
108
  const controller = new AbortController();
109
+ this.activeFetchControllers.add(controller);
110
+ let timedOut = false;
104
111
  const timeout = setTimeout(() => {
112
+ timedOut = true;
105
113
  controller.abort();
106
114
  }, this.requestTimeoutMs);
107
115
  timeout.unref?.();
108
116
  try {
109
- response = await fetch(this.uri, {
117
+ const response = await fetch(this.uri, {
110
118
  signal: controller.signal
111
119
  });
120
+ if (!response.ok) {
121
+ throw new JwtConfigurationError(`JWKS endpoint returned HTTP ${response.status}.`);
122
+ }
123
+ let body;
124
+ try {
125
+ body = await response.json();
126
+ } catch (error) {
127
+ if (this.isAbortError(error)) {
128
+ throw error;
129
+ }
130
+ throw new JwtConfigurationError('JWKS endpoint did not return valid JSON.');
131
+ }
132
+ if (!Array.isArray(body.keys)) {
133
+ throw new JwtConfigurationError('JWKS endpoint did not return a keys array.');
134
+ }
135
+ return body.keys;
112
136
  } catch (error) {
137
+ if (error instanceof JwtConfigurationError) {
138
+ throw error;
139
+ }
113
140
  if (this.isAbortError(error)) {
114
- throw new JwtConfigurationError(`JWKS fetch timed out after ${String(this.requestTimeoutMs)}ms.`);
141
+ if (timedOut) {
142
+ throw new JwtConfigurationError(`JWKS fetch timed out after ${String(this.requestTimeoutMs)}ms.`);
143
+ }
144
+ if (fetchGeneration !== this.lifecycleGeneration) {
145
+ throw new JwtConfigurationError('JWKS client was disposed while fetching keys.');
146
+ }
115
147
  }
116
148
  throw new JwtConfigurationError(`Failed to fetch JWKS from "${this.uri}".`);
117
149
  } finally {
118
150
  clearTimeout(timeout);
151
+ this.activeFetchControllers.delete(controller);
119
152
  }
120
- if (!response.ok) {
121
- throw new JwtConfigurationError(`JWKS endpoint returned HTTP ${response.status}.`);
122
- }
123
- let body;
124
- try {
125
- body = await response.json();
126
- } catch {
127
- throw new JwtConfigurationError('JWKS endpoint did not return valid JSON.');
128
- }
129
- if (!Array.isArray(body.keys)) {
130
- throw new JwtConfigurationError('JWKS endpoint did not return a keys array.');
131
- }
132
- return body.keys;
133
153
  }
134
154
  }
@@ -0,0 +1,8 @@
1
+ import type { JwtKeyEntry } from '../types.js';
2
+ /**
3
+ * Rejects static key entries whose IDs cannot unambiguously select one key.
4
+ *
5
+ * @param keys Static JWT key entries to validate.
6
+ */
7
+ export declare function assertJwtKeyEntries(keys: JwtKeyEntry[] | undefined): void;
8
+ //# sourceMappingURL=key-entries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-entries.d.ts","sourceRoot":"","sources":["../../src/signing/key-entries.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,SAAS,GAAG,IAAI,CAczE"}
@@ -0,0 +1,18 @@
1
+ import { JwtConfigurationError } from '../errors.js';
2
+ /**
3
+ * Rejects static key entries whose IDs cannot unambiguously select one key.
4
+ *
5
+ * @param keys Static JWT key entries to validate.
6
+ */
7
+ export function assertJwtKeyEntries(keys) {
8
+ if (!Array.isArray(keys)) {
9
+ return;
10
+ }
11
+ const keyIds = new Set();
12
+ for (const entry of keys) {
13
+ if (typeof entry.kid !== 'string' || entry.kid.length === 0 || keyIds.has(entry.kid)) {
14
+ throw new JwtConfigurationError('JWT key entries require non-empty unique kid values.');
15
+ }
16
+ keyIds.add(entry.kid);
17
+ }
18
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"signer.d.ts","sourceRoot":"","sources":["../../src/signing/signer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAgB,SAAS,EAAe,kBAAkB,EAAE,MAAM,aAAa,CAAC;AA0D5F;;GAEG;AACH,qBACa,gBAAgB;IAGf,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAiB;gBAEtB,OAAO,EAAE,kBAAkB;IAOlD,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;IAInD,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;IAK1D,OAAO,CAAC,4BAA4B;YAYtB,SAAS;CAoFxB"}
1
+ {"version":3,"file":"signer.d.ts","sourceRoot":"","sources":["../../src/signing/signer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAgB,SAAS,EAAe,kBAAkB,EAAE,MAAM,aAAa,CAAC;AA4D5F;;GAEG;AACH,qBACa,gBAAgB;IAGf,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAiB;gBAEtB,OAAO,EAAE,kBAAkB;IAYlD,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;IAInD,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;IAK1D,OAAO,CAAC,4BAA4B;YAYtB,SAAS;CAoFxB"}
@@ -7,7 +7,9 @@ function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side
7
7
  import { Inject } from '@fluojs/core';
8
8
  import { JwtConfigurationError } from '../errors.js';
9
9
  import { normalizeRefreshTokenOptions } from '../refresh/refresh-token.js';
10
- import { ASYMMETRIC_HASH, HMAC_HASH, JWT_OPTIONS } from './verifier.js';
10
+ import { SUPPORTED_ASYMMETRIC_HASH, SUPPORTED_HMAC_HASH } from './algorithm-policy.js';
11
+ import { assertJwtKeyEntries } from './key-entries.js';
12
+ import { JWT_OPTIONS } from './verifier.js';
11
13
  function encodeBase64Url(value) {
12
14
  return Buffer.from(value).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
13
15
  }
@@ -16,7 +18,7 @@ function resolveSigningKeyEntry(options, algorithm) {
16
18
  if (!Array.isArray(keys) || keys.length === 0) {
17
19
  return undefined;
18
20
  }
19
- if (hasOwnAlgorithmMapping(HMAC_HASH, algorithm)) {
21
+ if (hasOwnAlgorithmMapping(SUPPORTED_HMAC_HASH, algorithm)) {
20
22
  return keys.find(entry => typeof entry.secret === 'string' && entry.secret.length > 0);
21
23
  }
22
24
  return keys.find(entry => entry.privateKey !== undefined);
@@ -25,7 +27,7 @@ function hasOwnAlgorithmMapping(mappings, algorithm) {
25
27
  return typeof algorithm === 'string' && Object.hasOwn(mappings, algorithm);
26
28
  }
27
29
  function isSupportedSigningAlgorithm(algorithm) {
28
- return hasOwnAlgorithmMapping(HMAC_HASH, algorithm) || hasOwnAlgorithmMapping(ASYMMETRIC_HASH, algorithm);
30
+ return hasOwnAlgorithmMapping(SUPPORTED_HMAC_HASH, algorithm) || hasOwnAlgorithmMapping(SUPPORTED_ASYMMETRIC_HASH, algorithm);
29
31
  }
30
32
  function assertSigningAlgorithms(algorithms) {
31
33
  if (!Array.isArray(algorithms) || algorithms.length === 0) {
@@ -57,7 +59,10 @@ class DefaultJwtSigner {
57
59
  constructor(options) {
58
60
  this.options = options;
59
61
  assertSigningAlgorithms(options.algorithms);
60
- this.refreshAlgorithms = this.options.algorithms.filter(algorithm => hasOwnAlgorithmMapping(HMAC_HASH, algorithm));
62
+ assertJwtKeyEntries(options.keys);
63
+ const refreshToken = this.options.refreshToken ? normalizeRefreshTokenOptions(this.options.refreshToken) : undefined;
64
+ const configuredRefreshAlgorithms = refreshToken?.algorithms;
65
+ this.refreshAlgorithms = (configuredRefreshAlgorithms ?? this.options.algorithms).filter(algorithm => hasOwnAlgorithmMapping(SUPPORTED_HMAC_HASH, algorithm));
61
66
  }
62
67
  async signAccessToken(claims) {
63
68
  return this.signToken(claims, this.options, false);
@@ -79,7 +84,7 @@ class DefaultJwtSigner {
79
84
  async signToken(claims, options, hmacOnly) {
80
85
  const algorithm = options.algorithms.find(alg => {
81
86
  if (hmacOnly) {
82
- return hasOwnAlgorithmMapping(HMAC_HASH, alg);
87
+ return hasOwnAlgorithmMapping(SUPPORTED_HMAC_HASH, alg);
83
88
  }
84
89
  return isSupportedSigningAlgorithm(alg);
85
90
  });
@@ -89,7 +94,7 @@ class DefaultJwtSigner {
89
94
  }
90
95
  throw new JwtConfigurationError('JWT signer requires at least one supported algorithm (HS256/HS384/HS512/RS256/RS384/RS512/ES256/ES384/ES512) in the allowed algorithms list.');
91
96
  }
92
- const isAsymmetric = hasOwnAlgorithmMapping(ASYMMETRIC_HASH, algorithm);
97
+ const isAsymmetric = hasOwnAlgorithmMapping(SUPPORTED_ASYMMETRIC_HASH, algorithm);
93
98
  const now = Math.floor(Date.now() / 1000);
94
99
  const ttl = resolveAccessTokenTtlSeconds(options);
95
100
  const payload = {
@@ -116,7 +121,7 @@ class DefaultJwtSigner {
116
121
  if (!privateKey) {
117
122
  throw new JwtConfigurationError('JWT private key is not configured.');
118
123
  }
119
- const hash = ASYMMETRIC_HASH[algorithm];
124
+ const hash = SUPPORTED_ASYMMETRIC_HASH[algorithm];
120
125
  if (!hash) {
121
126
  throw new JwtConfigurationError(`No hash mapping for asymmetric algorithm "${algorithm}".`);
122
127
  }
@@ -135,7 +140,7 @@ class DefaultJwtSigner {
135
140
  if (!secret) {
136
141
  throw new JwtConfigurationError('JWT secret is not configured.');
137
142
  }
138
- const hash = HMAC_HASH[algorithm];
143
+ const hash = SUPPORTED_HMAC_HASH[algorithm];
139
144
  if (!hash) {
140
145
  throw new JwtConfigurationError(`No hash mapping for HMAC algorithm "${algorithm}".`);
141
146
  }
@@ -1,22 +1,27 @@
1
- import type { OnModuleDestroy } from '@fluojs/runtime';
2
1
  import type { JwtAlgorithm, JwtPrincipal, JwtVerifierOptions } from '../types.js';
3
2
  /**
4
3
  * Provides the resolved JWT verifier options through dependency injection.
5
4
  */
6
5
  export declare const JWT_OPTIONS: unique symbol;
6
+ /**
7
+ * Describes the lifecycle hook called when a JWT module is destroyed.
8
+ */
9
+ export interface JwtModuleDestroyLifecycle {
10
+ onModuleDestroy(): void | Promise<void>;
11
+ }
7
12
  /**
8
13
  * Maps supported HMAC JWT algorithms to their Node.js hash names.
9
14
  */
10
- export declare const HMAC_HASH: Partial<Record<JwtAlgorithm, string>>;
15
+ export declare const HMAC_HASH: Readonly<Partial<Record<JwtAlgorithm, string>>>;
11
16
  /**
12
17
  * Maps supported asymmetric JWT algorithms to their Node.js hash names.
13
18
  */
14
- export declare const ASYMMETRIC_HASH: Partial<Record<JwtAlgorithm, string>>;
19
+ export declare const ASYMMETRIC_HASH: Readonly<Partial<Record<JwtAlgorithm, string>>>;
15
20
  type AccessTokenVerificationOverrides = Pick<JwtVerifierOptions, 'algorithms' | 'audience' | 'clockSkewSeconds' | 'issuer' | 'maxAge' | 'requireExp'>;
16
21
  /**
17
22
  * Verifies JWT access and refresh tokens against the configured key sources.
18
23
  */
19
- export declare class DefaultJwtVerifier implements OnModuleDestroy {
24
+ export declare class DefaultJwtVerifier implements JwtModuleDestroyLifecycle {
20
25
  private readonly options;
21
26
  private readonly jwksClient;
22
27
  private readonly keyResolutionState;
@@ -1 +1 @@
1
- {"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/signing/verifier.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAIvD,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;AAiMF;;GAEG;AACH,qBACa,kBAAmB,YAAW,eAAe;IAM5C,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;;OAEG;IACH,eAAe,IAAI,IAAI;IAIvB;;;;;;;;;;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":"AAMA,OAAO,KAAK,EAAE,YAAY,EAA0B,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAK1G;;GAEG;AACH,eAAO,MAAM,WAAW,eAAiC,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,eAAe,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC;AAED;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAA6C,CAAC;AAEpH;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAmD,CAAC;AAmDhI,KAAK,gCAAgC,GAAG,IAAI,CAC1C,kBAAkB,EAClB,YAAY,GAAG,UAAU,GAAG,kBAAkB,GAAG,QAAQ,GAAG,QAAQ,GAAG,YAAY,CACpF,CAAC;AA2MF;;GAEG;AACH,qBACa,kBAAmB,YAAW,yBAAyB;IAMtD,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;IAclD,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAI7D;;;;;;OAMG;IACH,OAAO,IAAI,IAAI;IAIf;;OAEG;IACH,eAAe,IAAI,IAAI;IAIvB;;;;;;;;;;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;YAyB1B,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"}
@@ -7,38 +7,37 @@ function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side
7
7
  import { Inject } from '@fluojs/core';
8
8
  import { JwtConfigurationError, JwtExpiredTokenError, JwtInvalidTokenError } from '../errors.js';
9
9
  import { normalizeRefreshTokenOptions } from '../refresh/refresh-token.js';
10
+ import { SUPPORTED_ASYMMETRIC_HASH, SUPPORTED_HMAC_HASH } from './algorithm-policy.js';
10
11
  import { JwksClient } from './jwks.js';
12
+ import { assertJwtKeyEntries } from './key-entries.js';
11
13
 
12
14
  /**
13
15
  * Provides the resolved JWT verifier options through dependency injection.
14
16
  */
15
17
  export const JWT_OPTIONS = Symbol.for('fluo.jwt.options');
16
18
 
19
+ /**
20
+ * Describes the lifecycle hook called when a JWT module is destroyed.
21
+ */
22
+
17
23
  /**
18
24
  * Maps supported HMAC JWT algorithms to their Node.js hash names.
19
25
  */
20
- export const HMAC_HASH = {
21
- HS256: 'sha256',
22
- HS384: 'sha384',
23
- HS512: 'sha512'
24
- };
26
+ export const HMAC_HASH = Object.freeze({
27
+ ...SUPPORTED_HMAC_HASH
28
+ });
25
29
 
26
30
  /**
27
31
  * Maps supported asymmetric JWT algorithms to their Node.js hash names.
28
32
  */
29
- export const ASYMMETRIC_HASH = {
30
- RS256: 'sha256',
31
- RS384: 'sha384',
32
- RS512: 'sha512',
33
- ES256: 'sha256',
34
- ES384: 'sha384',
35
- ES512: 'sha512'
36
- };
33
+ export const ASYMMETRIC_HASH = Object.freeze({
34
+ ...SUPPORTED_ASYMMETRIC_HASH
35
+ });
37
36
  function hasOwnAlgorithmMapping(mappings, alg) {
38
37
  return typeof alg === 'string' && Object.hasOwn(mappings, alg);
39
38
  }
40
39
  function isSupportedAlgorithm(alg) {
41
- return hasOwnAlgorithmMapping(HMAC_HASH, alg) || hasOwnAlgorithmMapping(ASYMMETRIC_HASH, alg);
40
+ return hasOwnAlgorithmMapping(SUPPORTED_HMAC_HASH, alg) || hasOwnAlgorithmMapping(SUPPORTED_ASYMMETRIC_HASH, alg);
42
41
  }
43
42
  function assertJwtAlgorithms(algorithms, context) {
44
43
  if (!Array.isArray(algorithms) || algorithms.length === 0) {
@@ -130,7 +129,7 @@ function resolveStaticPublicKey(options, keyState, kid) {
130
129
  return keyState.defaultPublicKey ?? options.publicKey;
131
130
  }
132
131
  async function verifyHmacSignature(algorithm, secret, signingInput, signatureSegment) {
133
- const hash = HMAC_HASH[algorithm];
132
+ const hash = SUPPORTED_HMAC_HASH[algorithm];
134
133
  if (!hash) {
135
134
  throw new JwtInvalidTokenError();
136
135
  }
@@ -146,7 +145,7 @@ async function verifyHmacSignature(algorithm, secret, signingInput, signatureSeg
146
145
  }
147
146
  }
148
147
  async function verifyAsymmetricSignature(algorithm, publicKey, signingInput, signatureSegment) {
149
- const hash = ASYMMETRIC_HASH[algorithm];
148
+ const hash = SUPPORTED_ASYMMETRIC_HASH[algorithm];
150
149
  if (!hash) {
151
150
  throw new JwtInvalidTokenError();
152
151
  }
@@ -171,8 +170,15 @@ function decodeBase64Url(value) {
171
170
  }
172
171
  function parseJwtPart(value) {
173
172
  try {
174
- return JSON.parse(decodeBase64Url(value).toString('utf8'));
175
- } catch {
173
+ const parsed = JSON.parse(decodeBase64Url(value).toString('utf8'));
174
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
175
+ throw new JwtInvalidTokenError('JWT header and payload must be JSON objects.');
176
+ }
177
+ return parsed;
178
+ } catch (error) {
179
+ if (error instanceof JwtInvalidTokenError) {
180
+ throw error;
181
+ }
176
182
  throw new JwtInvalidTokenError();
177
183
  }
178
184
  }
@@ -212,6 +218,7 @@ class DefaultJwtVerifier {
212
218
  constructor(options) {
213
219
  this.options = options;
214
220
  assertJwtAlgorithms(options.algorithms, 'JWT verifier');
221
+ assertJwtKeyEntries(options.keys);
215
222
  this.jwksClient = options.jwksUri ? new JwksClient(options.jwksUri, options.jwksCacheTtl, options.jwksRequestTimeoutMs, options.jwksCacheMaxEntries) : undefined;
216
223
  this.keyResolutionState = createKeyResolutionState(options.keys);
217
224
  this.refreshVerificationOptions = options.refreshToken ? this.createRefreshVerificationOptions(normalizeRefreshTokenOptions(options.refreshToken)) : undefined;
@@ -270,7 +277,8 @@ class DefaultJwtVerifier {
270
277
  return this.verifyToken(token, this.refreshVerificationOptions, this.refreshKeyResolutionState, undefined);
271
278
  }
272
279
  createRefreshVerificationOptions(refreshToken) {
273
- const algorithms = this.options.algorithms.filter(algorithm => hasOwnAlgorithmMapping(HMAC_HASH, algorithm));
280
+ const configuredRefreshAlgorithms = refreshToken.algorithms;
281
+ const algorithms = (configuredRefreshAlgorithms ?? this.options.algorithms).filter(algorithm => hasOwnAlgorithmMapping(SUPPORTED_HMAC_HASH, algorithm));
274
282
  if (algorithms.length === 0) {
275
283
  throw new JwtConfigurationError('JWT refresh token verifier requires at least one HMAC algorithm (HS256/HS384/HS512) in the allowed algorithms list.');
276
284
  }
@@ -308,7 +316,7 @@ class DefaultJwtVerifier {
308
316
  return segments;
309
317
  }
310
318
  async verifyTokenSignature(header, signingInput, signatureSegment, options, keyResolutionState, jwksClient) {
311
- if (hasOwnAlgorithmMapping(HMAC_HASH, header.alg)) {
319
+ if (hasOwnAlgorithmMapping(SUPPORTED_HMAC_HASH, header.alg)) {
312
320
  await this.verifyHmacTokenSignature(header, signingInput, signatureSegment, options, keyResolutionState);
313
321
  return;
314
322
  }
package/dist/status.d.ts CHANGED
@@ -1,12 +1,56 @@
1
- import type { PlatformDiagnosticIssue, PlatformHealthReport, PlatformReadinessReport, PlatformSnapshot } from '@fluojs/runtime';
1
+ /**
2
+ * Describes the readiness state exposed by JWT.
3
+ */
4
+ export interface JwtPlatformReadinessReport {
5
+ critical: boolean;
6
+ reason?: string;
7
+ status: 'ready' | 'not-ready' | 'degraded';
8
+ checks?: JwtPlatformCheckResult[];
9
+ }
10
+ /**
11
+ * Describes the health state exposed by JWT.
12
+ */
13
+ export interface JwtPlatformHealthReport {
14
+ reason?: string;
15
+ status: 'healthy' | 'unhealthy' | 'degraded';
16
+ checks?: JwtPlatformCheckResult[];
17
+ }
18
+ /**
19
+ * Describes one named readiness or health probe result exposed by JWT.
20
+ */
21
+ export interface JwtPlatformCheckResult {
22
+ name: string;
23
+ status: 'pass' | 'fail' | 'degraded';
24
+ message?: string;
25
+ }
26
+ /**
27
+ * Describes ownership of JWT-managed resources.
28
+ */
29
+ export interface JwtPlatformOwnership {
30
+ externallyManaged: boolean;
31
+ ownsResources: boolean;
32
+ }
33
+ /**
34
+ * Describes a JWT diagnostic issue.
35
+ */
36
+ export interface JwtPlatformDiagnosticIssue {
37
+ cause?: string;
38
+ code: string;
39
+ componentId: string;
40
+ dependsOn?: string[];
41
+ docsUrl?: string;
42
+ fixHint?: string;
43
+ message: string;
44
+ severity: 'error' | 'warning' | 'info';
45
+ }
2
46
  /**
3
47
  * Describes the jwt platform status snapshot contract.
4
48
  */
5
49
  export interface JwtPlatformStatusSnapshot {
6
- readiness: PlatformReadinessReport;
7
- health: PlatformHealthReport;
8
- ownership: PlatformSnapshot['ownership'];
9
50
  details: Record<string, unknown>;
51
+ health: JwtPlatformHealthReport;
52
+ ownership: JwtPlatformOwnership;
53
+ readiness: JwtPlatformReadinessReport;
10
54
  }
11
55
  /**
12
56
  * Describes the jwt status adapter input contract.
@@ -33,5 +77,5 @@ export declare function createJwtPlatformStatusSnapshot(input: JwtStatusAdapterI
33
77
  * @param input The input.
34
78
  * @returns The create jwt platform diagnostic issues result.
35
79
  */
36
- export declare function createJwtPlatformDiagnosticIssues(input: JwtStatusAdapterInput): PlatformDiagnosticIssue[];
80
+ export declare function createJwtPlatformDiagnosticIssues(input: JwtStatusAdapterInput): JwtPlatformDiagnosticIssue[];
37
81
  //# sourceMappingURL=status.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,oBAAoB,EACpB,uBAAuB,EACvB,gBAAgB,EACjB,MAAM,iBAAiB,CAAC;AAEzB;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,uBAAuB,CAAC;IACnC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAC;IACzC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,gBAAgB,CAAC,EAAE,eAAe,GAAG,UAAU,GAAG,MAAM,GAAG,cAAc,CAAC;CAC3E;AAwCD;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,qBAAqB,GAAG,yBAAyB,CA4CvG;AAED;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,qBAAqB,GAAG,uBAAuB,EAAE,CAqBzG"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,GAAG,WAAW,GAAG,UAAU,CAAC;IAC3C,MAAM,CAAC,EAAE,sBAAsB,EAAE,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,UAAU,CAAC;IAC7C,MAAM,CAAC,EAAE,sBAAsB,EAAE,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,aAAa,EAAE,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;CACxC;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,EAAE,uBAAuB,CAAC;IAChC,SAAS,EAAE,oBAAoB,CAAC;IAChC,SAAS,EAAE,0BAA0B,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,gBAAgB,CAAC,EAAE,eAAe,GAAG,UAAU,GAAG,MAAM,GAAG,cAAc,CAAC;CAC3E;AAwCD;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,qBAAqB,GAAG,yBAAyB,CA4CvG;AAED;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,qBAAqB,GAAG,0BAA0B,EAAE,CAqB5G"}
package/dist/status.js CHANGED
@@ -1,3 +1,23 @@
1
+ /**
2
+ * Describes the readiness state exposed by JWT.
3
+ */
4
+
5
+ /**
6
+ * Describes the health state exposed by JWT.
7
+ */
8
+
9
+ /**
10
+ * Describes one named readiness or health probe result exposed by JWT.
11
+ */
12
+
13
+ /**
14
+ * Describes ownership of JWT-managed resources.
15
+ */
16
+
17
+ /**
18
+ * Describes a JWT diagnostic issue.
19
+ */
20
+
1
21
  /**
2
22
  * Describes the jwt platform status snapshot contract.
3
23
  */
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "signing",
10
10
  "verification"
11
11
  ],
12
- "version": "1.0.3",
12
+ "version": "2.0.0",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -18,7 +18,7 @@
18
18
  "directory": "packages/jwt"
19
19
  },
20
20
  "engines": {
21
- "node": ">=20.0.0"
21
+ "node": ">=24.0.0 <27"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "public"
@@ -36,12 +36,12 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
- "@fluojs/core": "^1.0.3",
40
- "@fluojs/di": "^1.1.0",
41
- "@fluojs/runtime": "^1.1.8"
39
+ "@fluojs/core": "^2.0.0",
40
+ "@fluojs/di": "^3.0.0"
42
41
  },
43
42
  "devDependencies": {
44
- "vitest": "^3.2.4"
43
+ "vitest": "^4.1.11",
44
+ "@fluojs/runtime": "^3.0.0"
45
45
  },
46
46
  "scripts": {
47
47
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",