🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@fluojs/jwt

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@fluojs/jwt - npm Package Compare versions

Comparing version
1.0.3
to
1.1.0
+1
-1
dist/module.js

@@ -67,3 +67,3 @@ let _initClass;

useValue: options
}, Boolean(options.refreshToken), Boolean(options.refreshToken), 'singleton', false, options.global ?? false);
}, true, true, options.refreshToken ? 'singleton' : 'transient', false, options.global ?? false);
}

@@ -70,0 +70,0 @@ static forRootAsync(options) {

@@ -11,2 +11,3 @@ import type { DefaultJwtSigner } from '../signing/signer.js';

revokeBySubject(subject: string): Promise<void>;
revokeByFamily?(family: string): Promise<void>;
consume?(input: RefreshTokenConsumeInput): Promise<RefreshTokenConsumeResult>;

@@ -84,2 +85,3 @@ rotate?(input: RefreshTokenRotateInput): Promise<RefreshTokenConsumeResult>;

private consumeRefreshToken;
private revokeCompromisedFamily;
private createRefreshTokenWithFamily;

@@ -86,0 +88,0 @@ private verifyRefreshClaims;

@@ -1,1 +0,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"}
{"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;AAC7D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAGjE;;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,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,uBAAuB;YASvB,4BAA4B;YA+B5B,mBAAmB;CA4BlC"}

@@ -107,3 +107,3 @@ import { JwtConfigurationError, JwtExpiredTokenError, JwtInvalidTokenError } from '../errors.js';

if (consumeResult === 'already_used') {
await this.options.store.revokeBySubject(claims.sub);
await this.revokeCompromisedFamily(claims.sub, claims.family);
throw new JwtInvalidTokenError('Refresh token reuse detected.');

@@ -130,3 +130,3 @@ }

if (record.used) {
await this.options.store.revokeBySubject(record.subject);
await this.revokeCompromisedFamily(record.subject, record.family);
throw new JwtInvalidTokenError('Refresh token reuse detected.');

@@ -162,2 +162,9 @@ }

}
async revokeCompromisedFamily(subject, family) {
if (this.options.store.revokeByFamily) {
await this.options.store.revokeByFamily(family);
return;
}
await this.options.store.revokeBySubject(subject);
}
async createRefreshTokenWithFamily(subject, family) {

@@ -164,0 +171,0 @@ const now = Math.floor(Date.now() / 1000);

@@ -11,2 +11,3 @@ import type { KeyObject } from 'node:crypto';

private readonly cache;
private readonly activeFetchControllers;
private lifecycleGeneration;

@@ -13,0 +14,0 @@ constructor(uri: string, cacheTtl?: number, requestTimeoutMs?: number, cacheMaxEntries?: number);

@@ -1,1 +0,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"}
{"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"}

@@ -24,2 +24,3 @@ import { JwtConfigurationError, JwtInvalidTokenError } from '../errors.js';

cache = new Map();
activeFetchControllers = new Set();
lifecycleGeneration = 0;

@@ -45,2 +46,6 @@ constructor(uri, cacheTtl = 600_000, requestTimeoutMs = 5_000, cacheMaxEntries = DEFAULT_JWKS_CACHE_MAX_ENTRIES) {

this.cache.clear();
for (const controller of this.activeFetchControllers) {
controller.abort();
}
this.activeFetchControllers.clear();
}

@@ -104,5 +109,8 @@ isAbortError(error) {

async fetchKeys() {
let response;
const fetchGeneration = this.lifecycleGeneration;
const controller = new AbortController();
this.activeFetchControllers.add(controller);
let timedOut = false;
const timeout = setTimeout(() => {
timedOut = true;
controller.abort();

@@ -112,8 +120,32 @@ }, this.requestTimeoutMs);

try {
response = await fetch(this.uri, {
const response = await fetch(this.uri, {
signal: controller.signal
});
if (!response.ok) {
throw new JwtConfigurationError(`JWKS endpoint returned HTTP ${response.status}.`);
}
let body;
try {
body = await response.json();
} catch (error) {
if (this.isAbortError(error)) {
throw error;
}
throw new JwtConfigurationError('JWKS endpoint did not return valid JSON.');
}
if (!Array.isArray(body.keys)) {
throw new JwtConfigurationError('JWKS endpoint did not return a keys array.');
}
return body.keys;
} catch (error) {
if (error instanceof JwtConfigurationError) {
throw error;
}
if (this.isAbortError(error)) {
throw new JwtConfigurationError(`JWKS fetch timed out after ${String(this.requestTimeoutMs)}ms.`);
if (timedOut) {
throw new JwtConfigurationError(`JWKS fetch timed out after ${String(this.requestTimeoutMs)}ms.`);
}
if (fetchGeneration !== this.lifecycleGeneration) {
throw new JwtConfigurationError('JWKS client was disposed while fetching keys.');
}
}

@@ -123,17 +155,5 @@ throw new JwtConfigurationError(`Failed to fetch JWKS from "${this.uri}".`);

clearTimeout(timeout);
this.activeFetchControllers.delete(controller);
}
if (!response.ok) {
throw new JwtConfigurationError(`JWKS endpoint returned HTTP ${response.status}.`);
}
let body;
try {
body = await response.json();
} catch {
throw new JwtConfigurationError('JWKS endpoint did not return valid JSON.');
}
if (!Array.isArray(body.keys)) {
throw new JwtConfigurationError('JWKS endpoint did not return a keys array.');
}
return body.keys;
}
}

@@ -12,3 +12,3 @@ {

],
"version": "1.0.3",
"version": "1.1.0",
"private": false,

@@ -40,5 +40,5 @@ "license": "MIT",

"dependencies": {
"@fluojs/core": "^1.0.3",
"@fluojs/di": "^1.1.0",
"@fluojs/runtime": "^1.1.8"
"@fluojs/core": "^1.1.0",
"@fluojs/di": "^2.0.0",
"@fluojs/runtime": "^2.0.1"
},

@@ -45,0 +45,0 @@ "devDependencies": {

@@ -153,3 +153,3 @@ # @fluojs/jwt

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 보관만 비활성화합니다.
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 보관만 비활성화합니다.

@@ -164,4 +164,6 @@ `JwtService.verify(token, options)`는 호출 단위의 알고리즘/클레임 정책 재정의(`issuer`, `audience`, `clockSkewSeconds`, `maxAge`, `requireExp`)를 적용하더라도, 내부 JWKS client나 정적 key-resolution cache를 다시 만들지 않습니다. 호출 단위 검증은 `jwksUri`, `keys[]`, `publicKey`, `secret`, `secretOrKeyProvider` 같은 구성된 key source 자체를 교체하지는 않습니다.

`RefreshTokenService`는 전용 HMAC refresh-token 경로를 사용합니다. `refreshToken.secret`은 access-token 서명 키와 별도로 설정하세요. Rotation은 `RefreshTokenStore.rotate(...)`를 사용해 현재 토큰을 소비 처리하고 대체 토큰을 같은 durable store 작업 안에서 저장할 수 있습니다. 따라서 성공한 rotation은 저장된 후속 토큰 없이 기존 토큰만 소비하지 않습니다. 기존 atomic `consume(...)` hook만 구현한 store도 계속 지원하지만, 대체 토큰 저장의 내구성은 store가 소유한 `rotate(...)` 작업에 달려 있습니다.
`RefreshTokenService`는 전용 HMAC refresh-token 경로를 사용합니다. `refreshToken.secret`은 access-token 서명 키와 별도로 설정하세요. Rotation은 `RefreshTokenStore.rotate(...)`를 사용해 현재 토큰을 소비 처리하고 대체 토큰을 같은 durable store 작업 안에서 저장할 수 있습니다. 따라서 성공한 rotation은 저장된 후속 토큰 없이 기존 토큰만 소비하지 않습니다. 기존 atomic `consume(...)` hook만 구현한 store도 계속 지원합니다. Consume이 성공하면 service가 `save(...)`로 대체 토큰을 저장하지만, 두 쓰기를 원자적으로 만드는 경로는 store 소유의 `rotate(...)`뿐입니다.
재사용을 감지하면 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(...)`를 구현하세요.
## 설정 가드레일

@@ -175,4 +177,14 @@

### Node 런타임 경계
루트 `@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 작업을 피합니다.
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에서 토큰을 서명하거나 검증할 수 있다고 가정하지 마세요.
### `decode()` trust boundary
`JwtService.decode(token)`는 서명, `alg`, `exp`, `nbf`, `iss`, `aud` 또는 기타 클레임을 검증하지 않고 JWT payload segment를 읽습니다. 반환된 객체는 **검증되지 않은 입력(unverified input)**이며, 권한 결정(authorization decisions), 신원 확인(identity resolution), 또는 접근을 허가하는 모든 코드 경로에 사용해서는 안 됩니다. 먼저 `JwtService.verify(token, options)`(또는 `DefaultJwtVerifier.verifyAccessToken(token)`)를 호출하고, 검증이 반환하는 정규화된 `JwtPrincipal`에서 신원을 읽으세요.
`decode()`는 진단(diagnostics) 및 비권위적 검사(non-authoritative inspection)에만 사용됩니다. 예를 들어 로깅을 위해 토큰 메타데이터를 읽거나 `verify()` 호출 전에 검증 키를 선택할 때 사용할 수 있습니다. `decode()` 출력에서 읽은 모든 클레임 값 — `sub`, `roles`, `scopes`, `iss`, `aud`, `exp` 포함 — 은 `verify()`가 성공하기 전까지 공격자가 제어한 값으로 취급해야 합니다. `decode()` 출력을 기준으로 요청을 허가하거나 거부하는 분기를 만들지 말고, 검증되지 않은 클레임을 검증된 것처럼 downstream 코드에 노출하지 마세요.
## 공개 API 개요

@@ -193,3 +205,4 @@

- `JwtClaims`, `JwtSigner`, `JwtVerifier`, `JwtKeyEntry`, `JwtAlgorithm`: 공개 서명 및 검증 계약입니다.
- `RefreshTokenOptions`, `RefreshTokenStore`, `RefreshTokenRecord`, `RefreshTokenConsumeInput`, `RefreshTokenRotateInput`, `RefreshTokenConsumeResult`: refresh-token 저장, rotation, replay detection 계약입니다.
- `RefreshTokenOptions`, `RefreshTokenStore`, `RefreshTokenRecord`, `RefreshTokenConsumeInput`, `RefreshTokenRotateInput`, `RefreshTokenConsumeResult`: refresh-token 저장, rotation, family-scoped revocation, replay detection 계약입니다. `RefreshTokenStore.revokeByFamily(...)`는 subject-revocation store와의 호환성을 위해 optional입니다.
- `JwtPlatformStatusSnapshot`, `JwtStatusAdapterInput`: platform diagnostic helper와 함께 export되는 status snapshot 및 adapter input 타입입니다.

@@ -196,0 +209,0 @@ ### 에러와 diagnostics

@@ -153,3 +153,3 @@ # @fluojs/jwt

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.
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.

@@ -164,4 +164,6 @@ `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`.

`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.
`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: after a successful consume, the service saves the replacement through `save(...)`, but only store-owned `rotate(...)` makes those two writes atomic.
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.
## Configuration Guardrails

@@ -175,4 +177,14 @@

### Node runtime boundary
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.
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.
### `decode()` trust boundary
`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)` (or `DefaultJwtVerifier.verifyAccessToken(token)`) first, and read identity from the normalized `JwtPrincipal` that verification returns.
`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.
## Public API Overview

@@ -193,3 +205,4 @@

- `JwtClaims`, `JwtSigner`, `JwtVerifier`, `JwtKeyEntry`, `JwtAlgorithm`: Public signing and verification contracts.
- `RefreshTokenOptions`, `RefreshTokenStore`, `RefreshTokenRecord`, `RefreshTokenConsumeInput`, `RefreshTokenRotateInput`, and `RefreshTokenConsumeResult`: Refresh-token storage, rotation, and replay-detection contracts.
- `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.
- `JwtPlatformStatusSnapshot` and `JwtStatusAdapterInput`: Status snapshot and adapter input types exported with the platform diagnostic helpers.

@@ -196,0 +209,0 @@ ### Errors and diagnostics