|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +import { registerBidder } from '../src/adapters/bidderFactory.js'; |
| 4 | +import { BANNER, VIDEO } from '../src/mediaTypes.js'; |
| 5 | +import { deepAccess, logWarn } from '../src/utils.js'; |
| 6 | +/** @import { MagicBidParams } from './magicbidBidAdapter.d.ts' */ |
| 7 | + |
| 8 | +// ───────────────────────────────────────────── |
| 9 | +// MagicBid Prebid.js Bid Adapter |
| 10 | +// Bidder Code : magicbid |
| 11 | +// Supported : Banner, Video |
| 12 | +// ───────────────────────────────────────────── |
| 13 | +// |
| 14 | +// Each publisher receives TWO values from MagicBid: |
| 15 | +// host → e.g. 'ads-2j0kac.rtb-magicbid.ai' (unique per publisher, |
| 16 | +// generated by the Limelight platform when a new publisher |
| 17 | +// endpoint is created) |
| 18 | +// adUnitId → e.g. 631967104 (unique per ad placement) |
| 19 | +// |
| 20 | +// adUnitType is NOT required — the adapter infers it automatically |
| 21 | +// from the mediaTypes object on the ad unit. |
| 22 | +// ───────────────────────────────────────────── |
| 23 | + |
| 24 | +const BIDDER_CODE = 'magicbid'; |
| 25 | + |
| 26 | +// ─── Helpers ──────────────────────────────── |
| 27 | + |
| 28 | +/** |
| 29 | + * Determine adUnitType from the bid's mediaTypes object. |
| 30 | + * VIDEO takes priority if both are present. |
| 31 | + * The host is NOT static — it is unique per publisher and is provided |
| 32 | + * by MagicBid during onboarding (generated by the Limelight platform). |
| 33 | + */ |
| 34 | +function resolveAdUnitType(bid) { |
| 35 | + if (deepAccess(bid, 'mediaTypes.video')) return VIDEO; |
| 36 | + if (deepAccess(bid, 'mediaTypes.banner')) return BANNER; |
| 37 | + // Fall back to explicit param if mediaTypes is not set |
| 38 | + const paramType = (bid.params.adUnitType || '').toLowerCase(); |
| 39 | + if (paramType === VIDEO || paramType === BANNER) return paramType; |
| 40 | + return BANNER; |
| 41 | +} |
| 42 | + |
| 43 | +/** |
| 44 | + * Build the RTB endpoint URL from the publisher-supplied host. |
| 45 | + * The host is publisher-specific and NOT static — each publisher |
| 46 | + * receives a unique host from MagicBid during onboarding. |
| 47 | + * e.g. host = 'ads-2j0kac.rtb-magicbid.ai' (unique per publisher) |
| 48 | + * type = 'banner' |
| 49 | + * → 'https://ads-2j0kac.rtb-magicbid.ai/prebid/banner' |
| 50 | + */ |
| 51 | +function buildEndpointUrl(host, adUnitType) { |
| 52 | + const path = adUnitType === VIDEO ? 'vast' : 'banner'; |
| 53 | + return 'https://' + host + '/prebid/' + path; |
| 54 | +} |
| 55 | + |
| 56 | +/** |
| 57 | + * Validate that a required string parameter exists and is non-empty. |
| 58 | + */ |
| 59 | +function validateStringParam(params, paramName) { |
| 60 | + const val = params[paramName]; |
| 61 | + if (typeof val !== 'string' || val.trim() === '') { |
| 62 | + logWarn('[MagicBid] Missing or invalid required param: "' + paramName + '"'); |
| 63 | + return false; |
| 64 | + } |
| 65 | + return true; |
| 66 | +} |
| 67 | + |
| 68 | +/** |
| 69 | + * Validate that a required integer param is a positive integer. |
| 70 | + */ |
| 71 | +function validateIntParam(params, paramName) { |
| 72 | + const val = params[paramName]; |
| 73 | + if (!Number.isInteger(val) || val <= 0) { |
| 74 | + logWarn('[MagicBid] Missing or invalid required param: "' + paramName + '" (must be a positive integer)'); |
| 75 | + return false; |
| 76 | + } |
| 77 | + return true; |
| 78 | +} |
| 79 | + |
| 80 | +/** |
| 81 | + * Normalize video playerSize — handles both flat [w, h] and |
| 82 | + * nested [[w, h]] forms that publishers may use. |
| 83 | + */ |
| 84 | +function normalizePlayerSize(playerSize) { |
| 85 | + if (!playerSize) return []; |
| 86 | + // Nested: [[640, 480]] → return as-is |
| 87 | + if (Array.isArray(playerSize[0])) return playerSize; |
| 88 | + // Flat: [640, 480] → wrap in array |
| 89 | + return [playerSize]; |
| 90 | +} |
| 91 | + |
| 92 | +// ─── Adapter Definition ────────────────────── |
| 93 | + |
| 94 | +export const spec = { |
| 95 | + code: BIDDER_CODE, |
| 96 | + supportedMediaTypes: [BANNER, VIDEO], |
| 97 | + |
| 98 | + // ─── 1. isBidRequestValid ─────────────────── |
| 99 | + /** |
| 100 | + * Required params: host, adUnitId |
| 101 | + * adUnitType is optional — inferred from mediaTypes automatically |
| 102 | + */ |
| 103 | + isBidRequestValid(bid) { |
| 104 | + const params = bid.params || {}; |
| 105 | + |
| 106 | + if (!validateStringParam(params, 'host')) return false; |
| 107 | + if (!validateIntParam(params, 'adUnitId')) return false; |
| 108 | + |
| 109 | + // If adUnitType is explicitly provided, validate it |
| 110 | + if (params.adUnitType) { |
| 111 | + const t = params.adUnitType.toLowerCase(); |
| 112 | + if (t !== BANNER && t !== VIDEO) { |
| 113 | + logWarn('[MagicBid] adUnitType must be "banner" or "video", got: "' + params.adUnitType + '"'); |
| 114 | + return false; |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + return true; |
| 119 | + }, |
| 120 | + |
| 121 | + // ─── 2. buildRequests ─────────────────────── |
| 122 | + /** |
| 123 | + * Group valid bid requests by host+adUnitType combination and |
| 124 | + * fire one HTTP request per group. |
| 125 | + */ |
| 126 | + buildRequests(validBidRequests, bidderRequest) { |
| 127 | + const groups = {}; |
| 128 | + |
| 129 | + validBidRequests.forEach(function(bid) { |
| 130 | + const params = bid.params; |
| 131 | + const host = params.host; |
| 132 | + const adUnitId = params.adUnitId; |
| 133 | + |
| 134 | + // Infer adUnitType from mediaTypes — no longer required as a param |
| 135 | + const resolvedType = resolveAdUnitType(bid); |
| 136 | + |
| 137 | + const key = host + '__' + resolvedType; |
| 138 | + |
| 139 | + if (!groups[key]) { |
| 140 | + groups[key] = { |
| 141 | + url: buildEndpointUrl(host, resolvedType), |
| 142 | + adUnitType: resolvedType, |
| 143 | + bids: [], |
| 144 | + }; |
| 145 | + } |
| 146 | + |
| 147 | + // Collect ad sizes |
| 148 | + let sizes = []; |
| 149 | + if (resolvedType === BANNER) { |
| 150 | + sizes = deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes || []; |
| 151 | + } else if (resolvedType === VIDEO) { |
| 152 | + const playerSize = deepAccess(bid, 'mediaTypes.video.playerSize'); |
| 153 | + sizes = normalizePlayerSize(playerSize); |
| 154 | + } |
| 155 | + |
| 156 | + const bidData = { |
| 157 | + bidId: bid.bidId, |
| 158 | + transactionId: deepAccess(bid, 'ortb2Imp.ext.tid') || bid.transactionId, |
| 159 | + adUnitCode: bid.adUnitCode, |
| 160 | + adUnitId: adUnitId, |
| 161 | + sizes: sizes, |
| 162 | + // Internal reference — stripped before sending to server |
| 163 | + _sourceBid: bid, |
| 164 | + }; |
| 165 | + |
| 166 | + // Optional params |
| 167 | + if (params.publisherId) bidData.publisherId = params.publisherId; |
| 168 | + if (params.custom1) bidData.custom1 = params.custom1; |
| 169 | + if (params.custom2) bidData.custom2 = params.custom2; |
| 170 | + if (params.custom3) bidData.custom3 = params.custom3; |
| 171 | + if (params.custom4) bidData.custom4 = params.custom4; |
| 172 | + if (params.custom5) bidData.custom5 = params.custom5; |
| 173 | + |
| 174 | + groups[key].bids.push(bidData); |
| 175 | + }); |
| 176 | + |
| 177 | + return Object.values(groups).map(function(group) { |
| 178 | + // Use the first bid from THIS group (not the whole auction) |
| 179 | + // to avoid mixing schain/eids from different publishers |
| 180 | + const representativeBid = group.bids[0]._sourceBid; |
| 181 | + |
| 182 | + // Read schain from ORTB source path (preferred) or legacy field |
| 183 | + const schain = deepAccess(representativeBid, 'ortb2.source.ext.schain') || |
| 184 | + deepAccess(bidderRequest, 'ortb2.source.ext.schain') || |
| 185 | + representativeBid.schain || null; |
| 186 | + |
| 187 | + const eids = representativeBid.userIdAsEids || []; |
| 188 | + |
| 189 | + return { |
| 190 | + method: 'POST', |
| 191 | + url: group.url, |
| 192 | + data: JSON.stringify({ |
| 193 | + id: bidderRequest.auctionId, |
| 194 | + site: { |
| 195 | + page: deepAccess(bidderRequest, 'refererInfo.page') || window.location.href, |
| 196 | + ref: deepAccess(bidderRequest, 'refererInfo.ref') || document.referrer, |
| 197 | + }, |
| 198 | + bids: group.bids.map(function(b) { |
| 199 | + const clean = Object.assign({}, b); |
| 200 | + delete clean._sourceBid; |
| 201 | + return clean; |
| 202 | + }), |
| 203 | + gdpr: { |
| 204 | + applies: !!(deepAccess(bidderRequest, 'gdprConsent.gdprApplies')), |
| 205 | + consent: deepAccess(bidderRequest, 'gdprConsent.consentString') || '', |
| 206 | + }, |
| 207 | + usp: bidderRequest.uspConsent || '', |
| 208 | + coppa: !!(deepAccess(bidderRequest, 'ortb2.regs.coppa')), |
| 209 | + gpp: deepAccess(bidderRequest, 'gppConsent.gppString') || '', |
| 210 | + gppSid: deepAccess(bidderRequest, 'gppConsent.applicableSections') || [], |
| 211 | + schain: schain, |
| 212 | + eids: eids, |
| 213 | + }), |
| 214 | + options: { |
| 215 | + // Using text/plain to avoid CORS preflight requests which can |
| 216 | + // increase bidder timeouts. The server must accept text/plain. |
| 217 | + contentType: 'text/plain', |
| 218 | + withCredentials: true, |
| 219 | + }, |
| 220 | + _adUnitType: group.adUnitType, |
| 221 | + }; |
| 222 | + }); |
| 223 | + }, |
| 224 | + |
| 225 | + // ─── 3. interpretResponse ─────────────────── |
| 226 | + interpretResponse(serverResponse, request) { |
| 227 | + const bids = []; |
| 228 | + |
| 229 | + if (!serverResponse || !serverResponse.body) { |
| 230 | + logWarn('[MagicBid] Empty or missing response body'); |
| 231 | + return bids; |
| 232 | + } |
| 233 | + |
| 234 | + const adUnitType = request._adUnitType; |
| 235 | + const rawBids = Array.isArray(serverResponse.body) |
| 236 | + ? serverResponse.body |
| 237 | + : [serverResponse.body]; |
| 238 | + |
| 239 | + rawBids.forEach(function(rawBid) { |
| 240 | + if (!rawBid || !rawBid.cpm || rawBid.cpm <= 0) return; |
| 241 | + |
| 242 | + const bid = { |
| 243 | + requestId: rawBid.bidId, |
| 244 | + cpm: rawBid.cpm, |
| 245 | + currency: rawBid.currency || 'USD', |
| 246 | + width: rawBid.width || 0, |
| 247 | + height: rawBid.height || 0, |
| 248 | + creativeId: rawBid.creativeId || rawBid.bidId, |
| 249 | + dealId: rawBid.dealId || null, |
| 250 | + netRevenue: true, |
| 251 | + ttl: rawBid.ttl || 300, |
| 252 | + // Preserve nurl for win notification in onBidWon |
| 253 | + nurl: rawBid.nurl || null, |
| 254 | + meta: { |
| 255 | + advertiserDomains: rawBid.adomain || [], |
| 256 | + }, |
| 257 | + }; |
| 258 | + |
| 259 | + if (adUnitType === VIDEO) { |
| 260 | + bid.mediaType = VIDEO; |
| 261 | + bid.vastUrl = rawBid.vastUrl || rawBid.ad; |
| 262 | + bid.vastXml = rawBid.vastXml || null; |
| 263 | + } else { |
| 264 | + bid.mediaType = BANNER; |
| 265 | + bid.ad = rawBid.ad; |
| 266 | + } |
| 267 | + |
| 268 | + bids.push(bid); |
| 269 | + }); |
| 270 | + |
| 271 | + return bids; |
| 272 | + }, |
| 273 | + |
| 274 | + // ─── 4. getUserSyncs ──────────────────────── |
| 275 | + getUserSyncs(syncOptions, serverResponses) { |
| 276 | + const syncs = []; |
| 277 | + if (!serverResponses || serverResponses.length === 0) return syncs; |
| 278 | + |
| 279 | + serverResponses.forEach(function(response) { |
| 280 | + const syncData = deepAccess(response, 'body.userSyncs'); |
| 281 | + if (!Array.isArray(syncData)) return; |
| 282 | + |
| 283 | + syncData.forEach(function(sync) { |
| 284 | + if (sync.type === 'image' && syncOptions.pixelEnabled) { |
| 285 | + syncs.push({ type: 'image', url: sync.url }); |
| 286 | + } else if (sync.type === 'iframe' && syncOptions.iframeEnabled) { |
| 287 | + syncs.push({ type: 'iframe', url: sync.url }); |
| 288 | + } |
| 289 | + }); |
| 290 | + }); |
| 291 | + |
| 292 | + return syncs; |
| 293 | + }, |
| 294 | + |
| 295 | + // ─── 5. onBidWon ──────────────────────────── |
| 296 | + onBidWon(bid) { |
| 297 | + if (bid.nurl) { |
| 298 | + fetch(bid.nurl, { method: 'GET', keepalive: true }).catch(function() {}); |
| 299 | + } |
| 300 | + }, |
| 301 | + |
| 302 | + // ─── 6. onTimeout ─────────────────────────── |
| 303 | + onTimeout(timeoutData) { |
| 304 | + logWarn('[MagicBid] Bid timed out:', timeoutData); |
| 305 | + }, |
| 306 | +}; |
| 307 | + |
| 308 | +registerBidder(spec); |
0 commit comments