@@ -59,6 +59,24 @@ const SYN_FIELD = Symbol();
5959export const allowedFields = [ SYN_FIELD , 'gptSlot' , 'adUnitCode' , 'size' , 'domain' , 'mediaType' ] as const ;
6060type DefaultField = { [ K in ( typeof allowedFields ) [ number ] ] : K extends string ? K : never } [ ( typeof allowedFields ) [ number ] ] ;
6161
62+ /**
63+ * @summary Global set to track valid userId tier fields
64+ */
65+ const validUserIdTierFields = new Set < string > ( ) ;
66+
67+ /**
68+ * @summary Checks if a field is a valid user ID tier field (userId.tierName)
69+ * A field is only considered valid if it appears in the validUserIdTierFields set,
70+ * which is populated during config validation based on explicitly configured userIds.
71+ * Fields will be rejected if they're not in the configured set, even if they follow the userId.tierName format.
72+ */
73+ function isUserIdTierField ( field : string ) : boolean {
74+ if ( typeof field !== 'string' ) return false ;
75+
76+ // Simply check if the field exists in our configured userId tier fields set
77+ return validUserIdTierFields . has ( field ) ;
78+ }
79+
6280/**
6381 * @summary This is a flag to indicate if a AJAX call is processing for a floors request
6482 */
@@ -103,7 +121,33 @@ const getHostname = (() => {
103121 }
104122} ) ( ) ;
105123
106- // First look into bidRequest!
124+ /**
125+ * @summary Check if a bidRequest contains any user IDs from the specified tiers
126+ * Returns an object with keys like 'userId.tierName' with boolean values (0/1)
127+ */
128+ export function resolveTierUserIds ( tiers , bidRequest ) {
129+ if ( ! tiers || ! bidRequest ?. userIdAsEid ?. length ) {
130+ return { } ;
131+ }
132+
133+ // Get all available EID sources from the bidRequest (single pass)
134+ const availableSources = bidRequest . userIdAsEid . reduce ( ( acc : Set < string > , eid : { source ?: string } ) => {
135+ if ( eid ?. source ) {
136+ acc . add ( eid . source ) ;
137+ }
138+ return acc ;
139+ } , new Set ( ) ) ;
140+
141+ // For each tier, check if any of its sources are available
142+ return Object . entries ( tiers ) . reduce ( ( result , [ tierName , sources ] ) => {
143+ const hasAnyIdFromTier = Array . isArray ( sources ) &&
144+ sources . some ( source => availableSources . has ( source ) ) ;
145+
146+ result [ `userId.${ tierName } ` ] = hasAnyIdFromTier ? 1 : 0 ;
147+ return result ;
148+ } , { } ) ;
149+ }
150+
107151function getGptSlotFromAdUnit ( adUnitId , { index = auctionManager . index } = { } ) {
108152 const adUnit = index . getAdUnit ( { adUnitId} ) ;
109153 const isGam = deepAccess ( adUnit , 'ortb2Imp.ext.data.adserver.name' ) === 'gam' ;
@@ -133,9 +177,25 @@ export const fieldMatchingFunctions = {
133177 */
134178function enumeratePossibleFieldValues ( floorFields , bidObject , responseObject ) {
135179 if ( ! floorFields . length ) return [ ] ;
180+
181+ // Get userId tier values if needed
182+ let userIdTierValues = { } ;
183+ const userIdFields = floorFields . filter ( isUserIdTierField ) ;
184+ if ( userIdFields . length > 0 && _floorsConfig . userIds ) {
185+ userIdTierValues = resolveTierUserIds ( _floorsConfig . userIds , bidObject ) ;
186+ }
187+
136188 // generate combination of all exact matches and catch all for each field type
137189 return floorFields . reduce ( ( accum , field ) => {
138- const exactMatch = fieldMatchingFunctions [ field ] ( bidObject , responseObject ) || '*' ;
190+ let exactMatch : string ;
191+ // Handle userId tier fields
192+ if ( isUserIdTierField ( field ) ) {
193+ exactMatch = String ( userIdTierValues [ field ] ?? '*' ) ;
194+ } else {
195+ // Standard fields use the field matching functions
196+ exactMatch = fieldMatchingFunctions [ field ] ( bidObject , responseObject ) || '*' ;
197+ }
198+
139199 // storing exact matches as lowerCase since we want to compare case insensitively
140200 accum . push ( exactMatch === '*' ? [ '*' ] : [ exactMatch . toLowerCase ( ) , '*' ] ) ;
141201 return accum ;
@@ -481,7 +541,7 @@ export function continueAuction(hookConfig) {
481541
482542function validateSchemaFields ( fields ) {
483543 if ( Array . isArray ( fields ) && fields . length > 0 ) {
484- if ( fields . every ( field => allowedFields . includes ( field ) ) ) {
544+ if ( fields . every ( field => allowedFields . includes ( field ) || isUserIdTierField ( field ) ) ) {
485545 return true ;
486546 } else {
487547 logError ( `${ MODULE_NAME } : Fields received do not match allowed fields` ) ;
@@ -768,6 +828,13 @@ export type FloorsConfig = Pick<Schema1FloorData, 'skipRate' | 'floorProvider'>
768828 * The Price Floors Module will take the greater of floorMin and the matched rule CPM when evaluating getFloor() and enforcing floors.
769829 */
770830 floorMin ?: number ;
831+ /**
832+ * Configuration for user ID tiers. Each tier is an array of EID sources
833+ * that will be matched against available EIDs in the bid request.
834+ */
835+ userIds ?: {
836+ [ tierName : string ] : string [ ] ;
837+ } ;
771838 enforcement ?: Pick < Schema2FloorData [ 'modelGroups' ] [ 0 ] , 'noFloorSignalBidders' > & {
772839 /**
773840 * If set to true (the default), the Price Floors Module will provide floors to bid adapters for bid request
@@ -830,6 +897,7 @@ export function handleSetFloorsConfig(config) {
830897 'floorProvider' , floorProvider => deepAccess ( config , 'data.floorProvider' , floorProvider ) ,
831898 'endpoint' , endpoint => endpoint || { } ,
832899 'skipRate' , ( ) => ! isNaN ( deepAccess ( config , 'data.skipRate' ) ) ? config . data . skipRate : config . skipRate || 0 ,
900+ 'userIds' , validateUserIdsConfig ,
833901 'enforcement' , enforcement => pick ( enforcement || { } , [
834902 'enforceJS' , enforceJS => enforceJS !== false , // defaults to true
835903 'enforcePBS' , enforcePBS => enforcePBS === true , // defaults to false
@@ -1085,3 +1153,31 @@ registerOrtbProcessor({type: IMP, name: 'bidfloor', fn: setOrtbImpBidFloor});
10851153registerOrtbProcessor ( { type : IMP , name : 'extBidfloor' , fn : setGranularBidfloors , priority : - 10 } )
10861154registerOrtbProcessor ( { type : IMP , name : 'extPrebidFloors' , fn : setImpExtPrebidFloors , dialects : [ PBS ] , priority : - 1 } ) ;
10871155registerOrtbProcessor ( { type : REQUEST , name : 'extPrebidFloors' , fn : setOrtbExtPrebidFloors , dialects : [ PBS ] } ) ;
1156+
1157+ /**
1158+ * Validate userIds config: must be an object with array values
1159+ * Also populates the validUserIdTierFields set with field names in the format "userId.tierName"
1160+ */
1161+ function validateUserIdsConfig ( userIds : Record < string , unknown > ) : Record < string , unknown > {
1162+ if ( ! userIds || typeof userIds !== 'object' ) return { } ;
1163+
1164+ // Clear the previous set of valid tier fields
1165+ validUserIdTierFields . clear ( ) ;
1166+
1167+ // Check if userIds is an object with array values
1168+ const invalidKey = Object . entries ( userIds ) . some ( ( [ tierName , value ] ) => {
1169+ if ( ! Array . isArray ( value ) ) {
1170+ return true ;
1171+ }
1172+ // Add the tier field to the validUserIdTierFields set
1173+ validUserIdTierFields . add ( `userId.${ tierName } ` ) ;
1174+ return false ;
1175+ } ) ;
1176+
1177+ if ( invalidKey ) {
1178+ validUserIdTierFields . clear ( ) ;
1179+ return { } ;
1180+ }
1181+
1182+ return userIds ;
1183+ }
0 commit comments