@@ -4,6 +4,7 @@ import * as CatboxMemory from '@hapi/catbox-memory';
44
55import {
66 Plugin ,
7+ ReqRef ,
78 Request ,
89 RequestRoute ,
910 ResponseToolkit ,
@@ -68,20 +69,20 @@ interface RequestDecorations {
6869 prefix : string [ ] ;
6970 } ,
7071 AuthUser : {
71- id : string ,
72- name : string
73- email : string
72+ id : string ;
73+ name : string ;
74+ email : string ;
7475 } ,
7576 AuthCredentialsExtra : {
76- test : number
77- }
77+ test : number ;
78+ } ,
7879 AuthApp : {
79- key : string
80- name : string
80+ key : string ;
81+ name : string ;
8182 } ,
8283 AuthArtifactsExtra : {
83- some : string
84- thing : number
84+ some : string ;
85+ thing : number ;
8586 }
8687}
8788
@@ -287,3 +288,230 @@ server.decorate('server', 'obj3_1', { func5: theFunc });
287288// Error when extending on server with objects
288289// @ts -expect-error Lab does not support overload errors
289290check . error ( ( ) => server . decorate ( 'server' , 'obj3_1' , { func5 : theFunc } , { apply : true , extend : true } ) ) ;
291+
292+ // Issue #4561 - Generic Request<Refs> should resolve augmented ReqRefDefaults auth properties
293+
294+ interface ExtraCred {
295+ extra_id : string ;
296+ }
297+
298+ interface UserProfile {
299+ id : string ;
300+ }
301+
302+ declare module '../..' {
303+ interface ReqRefDefaults {
304+ AuthCredentialsExtra : Partial < ExtraCred > ;
305+ }
306+ }
307+
308+ // Generic route (no custom refs) should see augmented UserCredentials
309+ const genericAuthRoute : ServerRoute = {
310+ method : 'GET' ,
311+ path : '/auth-check' ,
312+ handler : ( request , h ) => {
313+
314+ check . type < string > ( request . auth . credentials . user ! . someId ) ;
315+ check . type < string > ( request . auth . credentials . user ! . someName ) ;
316+
317+ const credIsAny : IsAny < typeof request . auth . credentials > = false ;
318+
319+ return 'ok' ;
320+ }
321+ } ;
322+
323+ // Generic function should see augmented credentials from ReqRefDefaults
324+ export function processAuthGeneric < Refs extends ReqRef > ( req : Request < Refs > ) : void {
325+
326+ if ( req . auth . isAuthenticated && req . auth . credentials . extra_id ) {
327+ check . type < string | undefined > ( req . auth . credentials . extra_id ) ;
328+ }
329+ }
330+
331+ // Non-generic Request should also see augmented credentials
332+ export function processAuthConcrete ( req : Request ) : void {
333+
334+ if ( req . auth . isAuthenticated && req . auth . credentials . extra_id ) {
335+ check . type < string | undefined > ( req . auth . credentials . extra_id ) ;
336+ }
337+
338+ // credentials should NOT resolve to `any`
339+ const credIsAny : IsAny < typeof req . auth . credentials > = false ;
340+ const artifactsIsAny : IsAny < typeof req . auth . artifacts > = false ;
341+ }
342+
343+ // Generic function should accept Request with specific route refs
344+ interface SpecificRouteRefs {
345+ Params : { id : string } ;
346+ }
347+
348+ export function callWithSpecificRefs ( req : Request < SpecificRouteRefs > ) : void {
349+
350+ processAuthGeneric ( req ) ;
351+ }
352+
353+ // =============================================================================
354+ // ReqRef System Issue Tests
355+ // Each section demonstrates a specific weakness in the current type system.
356+ // These tests produce VISIBLE compiler errors to demonstrate each problem.
357+ // =============================================================================
358+
359+ // -----------------------------------------------------------------------------
360+ // ISSUE 1: Direct Refs['Key'] access bypasses MergeRefs (route.d.ts:361)
361+ //
362+ // RouteOptionsPreObject.assign uses `keyof Refs['Pres']` instead of
363+ // `keyof MergeRefs<Refs>['Pres']`. When the user doesn't explicitly provide
364+ // `Pres` in their Refs, `Refs['Pres']` is `unknown` (from ReqRef's
365+ // Partial<Record<..., unknown>>), so `keyof unknown` is `never`.
366+ // This means `assign` is impossible unless Pres is explicitly provided.
367+ // -----------------------------------------------------------------------------
368+
369+ // This should compile — the user only customizes Params, and the default
370+ // Pres (Record<string, any>) should allow any string for `assign`.
371+ // ERROR: Type '"user"' is not assignable to type 'never'.
372+ const issuePreAssign : ServerRoute < { Params : { id : string } } > = {
373+ method : 'GET' ,
374+ path : '/users/{id}' ,
375+ options : {
376+ pre : [
377+ {
378+ method : ( request , h ) => ( { name : 'test' } ) ,
379+ assign : 'user' // TS ERROR — should work
380+ }
381+ ] ,
382+ handler : ( request , h ) => 'ok'
383+ }
384+ } ;
385+
386+ // -----------------------------------------------------------------------------
387+ // ISSUE 2: Params defaults to Record<string, any> — allows unsafe access
388+ //
389+ // URL path params are ALWAYS strings at runtime (before Joi validation), but
390+ // the default type Record<string, any> means TypeScript allows anything.
391+ // These assignments should all be errors but none are.
392+ // -----------------------------------------------------------------------------
393+
394+ const issueParamsAny : ServerRoute = {
395+ method : 'GET' ,
396+ path : '/items/{id}' ,
397+ handler : ( request , h ) => {
398+
399+ // FIXED: Params now correctly typed as Record<string, string>
400+ // @ts -expect-error - params are strings, not numbers
401+ const id : number = request . params . id ;
402+ // @ts -expect-error - params are strings, not boolean[]
403+ const wat : boolean [ ] = request . params . id ;
404+
405+ // FIXED: params is no longer `any`
406+ const paramsIsAny : IsAny < typeof request . params . id > = false ;
407+
408+ return 'ok' ;
409+ }
410+ } ;
411+
412+ // -----------------------------------------------------------------------------
413+ // ISSUE 3: Headers defaults to Record<string, any>
414+ //
415+ // Node's http.IncomingHttpHeaders types headers as string | string[] | undefined.
416+ // The Record<string, any> default loses this.
417+ // -----------------------------------------------------------------------------
418+
419+ const issueHeadersAny : ServerRoute = {
420+ method : 'GET' ,
421+ path : '/headers' ,
422+ handler : ( request , h ) => {
423+
424+ // FIXED: Headers now correctly typed as Record<string, string | string[] | undefined>
425+ // @ts -expect-error - headers are string | string[] | undefined, not number
426+ const auth : number = request . headers . authorization ;
427+
428+ // FIXED: headers is no longer `any`
429+ const headersIsAny : IsAny < typeof request . headers . authorization > = false ;
430+
431+ return 'ok' ;
432+ }
433+ } ;
434+
435+ // -----------------------------------------------------------------------------
436+ // ISSUE 4: Default RequestQuery has [key: string]: any index signature
437+ //
438+ // Without a Query override, any access on request.query is `any`.
439+ // -----------------------------------------------------------------------------
440+
441+ const issueQueryAny : ServerRoute = {
442+ method : 'GET' ,
443+ path : '/search' ,
444+ handler : ( request , h ) => {
445+
446+ // FIXED: Query now correctly typed as Record<string, string | string[] | undefined>
447+ // @ts -expect-error - query values are string | string[] | undefined, not number
448+ const page : number = request . query . page ;
449+ // @ts -expect-error - query values are string | string[] | undefined, not boolean[]
450+ const wat : boolean [ ] = request . query . anything ;
451+
452+ // FIXED: query is no longer `any`
453+ const queryIsAny : IsAny < typeof request . query . page > = false ;
454+
455+ return 'ok' ;
456+ }
457+ } ;
458+
459+ // -----------------------------------------------------------------------------
460+ // ISSUE 5: Request<CustomRefs> not assignable to Request<ReqRefDefaults>
461+ //
462+ // A function taking Request (no generic) can't accept Request<{ Params: ... }>
463+ // even though the custom refs only NARROW a property. Users are forced to
464+ // choose between generic (accepts all) or concrete (sees defaults).
465+ // -----------------------------------------------------------------------------
466+
467+ export function concreteHelper ( req : Request ) : string | undefined {
468+
469+ if ( req . auth . credentials . extra_id ) {
470+ return req . auth . credentials . extra_id ;
471+ }
472+
473+ return undefined ;
474+ }
475+
476+ interface MyRouteRefs {
477+ Params : { id : string } ;
478+ Query : { expand : string } ;
479+ }
480+
481+ // KNOWN LIMITATION: Request<MyRouteRefs> is not assignable to Request<ReqRefDefaults>
482+ // because TypeScript checks generic interface compatibility invariantly when
483+ // the generic appears in contravariant positions (e.g. lifecycle method parameters).
484+ // Workaround: use a generic function like processAuthGeneric<Refs> above instead
485+ // of concrete Request (no generic) for helper functions that need to accept
486+ // requests with different Refs.
487+ export function issueConcreteVsGeneric ( req : Request < MyRouteRefs > ) : void {
488+
489+ // @ts -expect-error - Known TS limitation: Request<CustomRefs> not assignable to Request<ReqRefDefaults>
490+ concreteHelper ( req ) ;
491+ }
492+
493+ // -----------------------------------------------------------------------------
494+ // ISSUE 6: state and preResponses are not extensible through ReqRef
495+ //
496+ // These properties use hardcoded Record<string, any> and are NOT wired
497+ // through InternalRequestDefaults/ReqRef, so users can't type them.
498+ // -----------------------------------------------------------------------------
499+
500+ const issueStateAny : ServerRoute = {
501+ method : 'GET' ,
502+ path : '/state' ,
503+ handler : ( request , h ) => {
504+
505+ // FIXED: state is now Record<string, unknown> — requires type narrowing
506+ // @ts -expect-error - state values are unknown, not directly assignable to number
507+ const session : number = request . state . session ;
508+
509+ // FIXED: state is no longer `any`
510+ const stateIsAny : IsAny < typeof request . state . session > = false ;
511+
512+ // FIXED: preResponses is no longer `any`
513+ const preRespIsAny : IsAny < typeof request . preResponses . myPre > = false ;
514+
515+ return 'ok' ;
516+ }
517+ } ;
0 commit comments