1- import { Result } from "@adviser/cement" ;
2- import { Option } from "@adviser/cement" ;
1+ import { Result , Option } from "@adviser/cement" ;
32import { importJWK } from "jose" ;
43
54// Basic JWKS interfaces
@@ -47,18 +46,22 @@ export interface JWKSValidationResult {
4746}
4847
4948export class JWKSValidationError extends Error {
50- constructor ( message : string , public readonly code : string , public readonly details ?: any ) {
49+ constructor (
50+ message : string ,
51+ public readonly code : string ,
52+ public readonly details ?: unknown ,
53+ ) {
5154 super ( message ) ;
5255 this . name = "JWKSValidationError" ;
5356 }
5457}
5558
5659export class JWKSFetchError extends Error {
5760 constructor (
58- message : string ,
61+ message : string ,
5962 public readonly statusCode ?: number ,
6063 public readonly url ?: string ,
61- public readonly originalError ?: Error
64+ public readonly originalError ?: Error ,
6265 ) {
6366 super ( message ) ;
6467 this . name = "JWKSFetchError" ;
@@ -70,13 +73,13 @@ export function buildJWKSUrl(config: string): string {
7073 if ( config . startsWith ( "http://" ) || config . startsWith ( "https://" ) ) {
7174 return config ;
7275 }
73-
76+
7477 // Handle Clerk-style strings (both "trusted-glowworm-5" and "*.clerk.accounts.dev")
7578 if ( config . includes ( "clerk" ) || ( ! config . includes ( "." ) && config . length > 0 ) ) {
7679 const domain = config . includes ( "." ) ? config : `${ config } .clerk.accounts.dev` ;
7780 return `https://${ domain } /.well-known/jwks.json` ;
7881 }
79-
82+
8083 throw new JWKSValidationError ( "Invalid JWKS configuration" , "INVALID_CONFIG" , { config } ) ;
8184}
8285
@@ -87,163 +90,157 @@ export async function fetchJWKS(
8790 timeout ?: number ;
8891 retries ?: number ;
8992 userAgent ?: string ;
90- }
93+ } ,
9194) : Promise < Result < JWKS , JWKSFetchError > > {
9295 try {
9396 const url = buildJWKSUrl ( config ) ;
9497 const timeout = options ?. timeout ?? 5000 ;
9598 const retries = options ?. retries ?? 3 ;
9699 const userAgent = options ?. userAgent ?? "fireproof-jwks-fetcher/1.0" ;
97-
100+
98101 let lastError : Error | undefined ;
99-
102+
100103 for ( let attempt = 0 ; attempt <= retries ; attempt ++ ) {
101104 try {
102105 const controller = new AbortController ( ) ;
103106 const timeoutId = setTimeout ( ( ) => controller . abort ( ) , timeout ) ;
104-
107+
105108 const response = await fetch ( url , {
106109 signal : controller . signal ,
107110 headers : {
108111 "User-Agent" : userAgent ,
109- " Accept" : "application/json" ,
110- "Cache-Control" : "no-cache"
111- }
112+ Accept : "application/json" ,
113+ "Cache-Control" : "no-cache" ,
114+ } ,
112115 } ) ;
113-
116+
114117 clearTimeout ( timeoutId ) ;
115-
118+
116119 if ( ! response . ok ) {
117120 throw new JWKSFetchError ( `HTTP ${ response . status } : ${ response . statusText } ` , response . status , url ) ;
118121 }
119-
122+
120123 const jsonData = await response . json ( ) ;
121-
124+
122125 if ( ! jsonData ?. keys || ! Array . isArray ( jsonData . keys ) ) {
123126 throw new JWKSFetchError ( "Response does not contain a 'keys' array" , response . status , url ) ;
124127 }
125-
128+
126129 return Result . Ok ( jsonData as JWKS ) ;
127-
128130 } catch ( error ) {
129131 lastError = error instanceof Error ? error : new Error ( String ( error ) ) ;
130-
132+
131133 // Don't retry on client errors
132134 if ( error instanceof JWKSFetchError && error . statusCode && error . statusCode >= 400 && error . statusCode < 500 ) {
133135 throw error ;
134136 }
135-
137+
136138 // Wait before retry
137139 if ( attempt < retries ) {
138- await new Promise ( resolve => setTimeout ( resolve , Math . pow ( 2 , attempt ) * 1000 ) ) ;
140+ await new Promise ( ( resolve ) => setTimeout ( resolve , Math . pow ( 2 , attempt ) * 1000 ) ) ;
139141 }
140142 }
141143 }
142-
144+
143145 throw new JWKSFetchError ( `Failed to fetch JWKS after ${ retries + 1 } attempts` , undefined , url , lastError ) ;
144-
145146 } catch ( error ) {
146147 if ( error instanceof JWKSFetchError ) {
147148 return Result . Err ( error ) ;
148149 }
149- return Result . Err ( new JWKSFetchError (
150- error instanceof Error ? error . message : String ( error ) ,
151- undefined ,
152- undefined ,
153- error instanceof Error ? error : undefined
154- ) ) ;
150+ return Result . Err (
151+ new JWKSFetchError (
152+ error instanceof Error ? error . message : String ( error ) ,
153+ undefined ,
154+ undefined ,
155+ error instanceof Error ? error : undefined ,
156+ ) ,
157+ ) ;
155158 }
156159}
157160
158161// Validate individual key
159- export async function validateJWKSKey (
160- key : JWK ,
161- options : JWKSValidationOptions = { }
162- ) : Promise < KeyValidationResult > {
162+ export async function validateJWKSKey ( key : JWK , options : JWKSValidationOptions = { } ) : Promise < KeyValidationResult > {
163163 const result : KeyValidationResult = {
164164 isValid : false ,
165165 isCurrent : false ,
166166 keyId : key . kid ,
167167 validationErrors : [ ] ,
168168 warningMessages : [ ] ,
169- originalKey : key
169+ originalKey : key ,
170170 } ;
171-
171+
172172 const allowedKeyTypes = options . allowedKeyTypes ?? [ "RSA" , "EC" ] ;
173173 const allowedUse = options . allowedUse ?? [ "sig" ] ;
174174 const requireKeyId = options . requireKeyId ?? true ;
175-
175+
176176 // Basic validations
177177 if ( ! key . kty ) {
178178 result . validationErrors . push ( "Missing required field 'kty'" ) ;
179179 } else if ( ! allowedKeyTypes . includes ( key . kty ) ) {
180180 result . validationErrors . push ( `Unsupported key type: ${ key . kty } ` ) ;
181181 }
182-
182+
183183 if ( requireKeyId && ! key . kid ) {
184184 result . validationErrors . push ( "Missing required field 'kid'" ) ;
185185 }
186-
186+
187187 if ( key . use && ! allowedUse . includes ( key . use ) ) {
188188 result . validationErrors . push ( `Unsupported key use: ${ key . use } ` ) ;
189189 }
190-
190+
191191 // Key-specific validations
192192 if ( key . kty === "RSA" && ( ! key . n || ! key . e ) ) {
193193 result . validationErrors . push ( "RSA key missing n or e parameters" ) ;
194194 }
195-
195+
196196 if ( key . kty === "EC" && ( ! key . crv || ! key . x || ! key . y ) ) {
197197 result . validationErrors . push ( "EC key missing crv, x, or y parameters" ) ;
198198 }
199-
199+
200200 // Try to import the key
201201 try {
202- await importJWK ( key ) ;
203- result . isCurrent = true ;
202+ await importJWK ( key , key . alg as string | undefined ) ;
203+ result . isCurrent = result . validationErrors . length === 0 ;
204204 } catch ( error ) {
205205 result . validationErrors . push ( `Key import failed: ${ error instanceof Error ? error . message : error } ` ) ;
206206 }
207-
207+
208208 result . isValid = result . validationErrors . length === 0 ;
209209 return result ;
210210}
211211
212212// Validate JWKS
213- export async function validateJWKS (
214- jwks : JWKS ,
215- options : JWKSValidationOptions = { }
216- ) : Promise < JWKSValidationResult > {
213+ export async function validateJWKS ( jwks : JWKS , options : JWKSValidationOptions = { } ) : Promise < JWKSValidationResult > {
217214 const result : JWKSValidationResult = {
218215 isValid : false ,
219216 validKeysCount : 0 ,
220217 currentKeysCount : 0 ,
221218 totalKeysCount : jwks . keys . length ,
222219 validationErrors : [ ] ,
223220 warningMessages : [ ] ,
224- keyResults : [ ]
221+ keyResults : [ ] ,
225222 } ;
226-
223+
227224 if ( jwks . keys . length === 0 ) {
228225 result . validationErrors . push ( "JWKS contains no keys" ) ;
229226 return result ;
230227 }
231-
228+
232229 const maxKeys = options . maxKeys ?? 10 ;
233230 if ( jwks . keys . length > maxKeys ) {
234231 result . validationErrors . push ( `Too many keys: ${ jwks . keys . length } (max: ${ maxKeys } )` ) ;
235232 return result ;
236233 }
237-
234+
238235 // Validate each key
239236 for ( const key of jwks . keys ) {
240237 const keyResult = await validateJWKSKey ( key , options ) ;
241238 result . keyResults . push ( keyResult ) ;
242-
239+
243240 if ( keyResult . isValid ) result . validKeysCount ++ ;
244241 if ( keyResult . isCurrent ) result . currentKeysCount ++ ;
245242 }
246-
243+
247244 result . isValid = result . validationErrors . length === 0 && result . validKeysCount > 0 ;
248245 return result ;
249246}
@@ -256,28 +253,25 @@ export async function fetchAndValidateJWKS(
256253 timeout ?: number ;
257254 retries ?: number ;
258255 userAgent ?: string ;
259- }
256+ } ,
260257) : Promise < Result < { jwks : JWKS ; validation : JWKSValidationResult } , JWKSFetchError | JWKSValidationError > > {
261258 const fetchResult = await fetchJWKS ( config , fetchOptions ) ;
262259 if ( fetchResult . is_err ( ) ) {
263260 return Result . Err ( fetchResult . unwrap_err ( ) ) ;
264261 }
265-
262+
266263 const jwks = fetchResult . unwrap ( ) ;
267264 const validation = await validateJWKS ( jwks , validationOptions ) ;
268-
265+
269266 return Result . Ok ( { jwks, validation } ) ;
270267}
271268
272269// Utility functions
273270export function getCurrentKeys ( validationResult : JWKSValidationResult ) : JWK [ ] {
274- return validationResult . keyResults
275- . filter ( result => result . isCurrent )
276- . map ( result => result . originalKey ) ;
271+ return validationResult . keyResults . filter ( ( result ) => result . isCurrent && result . isValid ) . map ( ( result ) => result . originalKey ) ;
277272}
278273
279274export function findKeyById ( jwks : JWKS , keyId : string ) : Option < JWK > {
280- const key = jwks . keys . find ( k => k . kid === keyId ) ;
275+ const key = jwks . keys . find ( ( k ) => k . kid === keyId ) ;
281276 return key ? Option . Some ( key ) : Option . None ( ) ;
282277}
283-
0 commit comments