diff --git a/.github/actions/load/action.yml b/.github/actions/load/action.yml index d8c803a336d..c6460d607d3 100644 --- a/.github/actions/load/action.yml +++ b/.github/actions/load/action.yml @@ -22,17 +22,23 @@ runs: shell: bash run: | rm -r "$(pwd)"/* - - name: Download artifact - uses: Wandalen/wretry.action@v3.8.0 + id: download + continue-on-error: true + uses: actions/download-artifact@v7 with: - action: actions/download-artifact@v7 - attempt_limit: 2 - attempt_delay: 10000 - with: | - path: '${{ runner.temp }}' - name: '${{ inputs.name }}' - + path: '${{ runner.temp }}' + name: '${{ inputs.name }}' + - name: Delay before retrying download + if: steps.download.outcome != 'success' + shell: bash + run: sleep 10 + - name: Retry downloading artifact + if: steps.download.outcome != 'success' + uses: actions/download-artifact@v7 + with: + path: '${{ runner.temp }}' + name: '${{ inputs.name }}' - name: 'Untar working directory' shell: bash run: | diff --git a/.github/codeql/queries/autogen_fpDOMMethod.qll b/.github/codeql/queries/autogen_fpDOMMethod.qll index 44f573b9756..23f12571485 100644 --- a/.github/codeql/queries/autogen_fpDOMMethod.qll +++ b/.github/codeql/queries/autogen_fpDOMMethod.qll @@ -7,9 +7,9 @@ class DOMMethod extends string { DOMMethod() { - ( this = "toDataURL" and weight = 27.52 and type = "HTMLCanvasElement" ) + ( this = "getChannelData" and weight = 1083.37 and type = "AudioBuffer" ) or - ( this = "getChannelData" and weight = 1153.98 and type = "AudioBuffer" ) + ( this = "toDataURL" and weight = 27.64 and type = "HTMLCanvasElement" ) } float getWeight() { diff --git a/.github/codeql/queries/autogen_fpEventProperty.qll b/.github/codeql/queries/autogen_fpEventProperty.qll index d89458530f0..d0a78338953 100644 --- a/.github/codeql/queries/autogen_fpEventProperty.qll +++ b/.github/codeql/queries/autogen_fpEventProperty.qll @@ -7,21 +7,21 @@ class EventProperty extends string { EventProperty() { - ( this = "accelerationIncludingGravity" and weight = 146.7 and event = "devicemotion" ) + ( this = "accelerationIncludingGravity" and weight = 159.98 and event = "devicemotion" ) or - ( this = "beta" and weight = 761.68 and event = "deviceorientation" ) + ( this = "beta" and weight = 851.21 and event = "deviceorientation" ) or - ( this = "gamma" and weight = 285 and event = "deviceorientation" ) + ( this = "gamma" and weight = 303.39 and event = "deviceorientation" ) or - ( this = "alpha" and weight = 719 and event = "deviceorientation" ) + ( this = "alpha" and weight = 768.07 and event = "deviceorientation" ) or - ( this = "candidate" and weight = 44.7 and event = "icecandidate" ) + ( this = "candidate" and weight = 45.34 and event = "icecandidate" ) or - ( this = "acceleration" and weight = 60.81 and event = "devicemotion" ) + ( this = "acceleration" and weight = 63.39 and event = "devicemotion" ) or - ( this = "rotationRate" and weight = 60.9 and event = "devicemotion" ) + ( this = "rotationRate" and weight = 63.13 and event = "devicemotion" ) or - ( this = "absolute" and weight = 428.28 and event = "deviceorientation" ) + ( this = "absolute" and weight = 402.37 and event = "deviceorientation" ) } float getWeight() { diff --git a/.github/codeql/queries/autogen_fpGlobalConstructor.qll b/.github/codeql/queries/autogen_fpGlobalConstructor.qll index a5073e324a8..9c898c369d9 100644 --- a/.github/codeql/queries/autogen_fpGlobalConstructor.qll +++ b/.github/codeql/queries/autogen_fpGlobalConstructor.qll @@ -6,15 +6,15 @@ class GlobalConstructor extends string { GlobalConstructor() { - ( this = "OfflineAudioContext" and weight = 224.73 ) + ( this = "OfflineAudioContext" and weight = 225.31 ) or - ( this = "SharedWorker" and weight = 83.39 ) + ( this = "RTCPeerConnection" and weight = 34.53 ) or - ( this = "RTCPeerConnection" and weight = 33.57 ) + ( this = "SharedWorker" and weight = 75.74 ) or - ( this = "Gyroscope" and weight = 58.22 ) + ( this = "Gyroscope" and weight = 50.8 ) or - ( this = "AudioWorkletNode" and weight = 128.01 ) + ( this = "AudioWorkletNode" and weight = 119.39 ) } float getWeight() { diff --git a/.github/codeql/queries/autogen_fpGlobalObjectProperty0.qll b/.github/codeql/queries/autogen_fpGlobalObjectProperty0.qll index 82695cf2cb4..dfa86fe9d3f 100644 --- a/.github/codeql/queries/autogen_fpGlobalObjectProperty0.qll +++ b/.github/codeql/queries/autogen_fpGlobalObjectProperty0.qll @@ -7,61 +7,57 @@ class GlobalObjectProperty0 extends string { GlobalObjectProperty0() { - ( this = "cookieEnabled" and weight = 15.03 and global0 = "navigator" ) + ( this = "availHeight" and weight = 89.13 and global0 = "screen" ) or - ( this = "availHeight" and weight = 87.99 and global0 = "screen" ) + ( this = "availWidth" and weight = 84.67 and global0 = "screen" ) or - ( this = "availWidth" and weight = 83.8 and global0 = "screen" ) + ( this = "colorDepth" and weight = 42.61 and global0 = "screen" ) or - ( this = "colorDepth" and weight = 42.63 and global0 = "screen" ) + ( this = "availTop" and weight = 1275.07 and global0 = "screen" ) or - ( this = "deviceMemory" and weight = 62.07 and global0 = "navigator" ) + ( this = "permissions" and weight = 70.37 and global0 = "navigator" ) or - ( this = "availTop" and weight = 1217.02 and global0 = "screen" ) + ( this = "deviceMemory" and weight = 59.45 and global0 = "navigator" ) or - ( this = "plugins" and weight = 15.17 and global0 = "navigator" ) + ( this = "onLine" and weight = 19.95 and global0 = "navigator" ) or - ( this = "getBattery" and weight = 37.48 and global0 = "navigator" ) + ( this = "mediaCapabilities" and weight = 33.73 and global0 = "navigator" ) or - ( this = "webdriver" and weight = 26.24 and global0 = "navigator" ) + ( this = "pixelDepth" and weight = 49.23 and global0 = "screen" ) or - ( this = "permission" and weight = 24.97 and global0 = "Notification" ) + ( this = "permission" and weight = 24.75 and global0 = "Notification" ) or - ( this = "storage" and weight = 41.56 and global0 = "navigator" ) + ( this = "webdriver" and weight = 26.06 and global0 = "navigator" ) or - ( this = "orientation" and weight = 35.51 and global0 = "screen" ) + ( this = "getBattery" and weight = 36.56 and global0 = "navigator" ) or - ( this = "onLine" and weight = 19.45 and global0 = "navigator" ) + ( this = "availLeft" and weight = 653.43 and global0 = "screen" ) or - ( this = "pixelDepth" and weight = 47.8 and global0 = "screen" ) + ( this = "mediaDevices" and weight = 148 and global0 = "navigator" ) or - ( this = "availLeft" and weight = 621.32 and global0 = "screen" ) + ( this = "hardwareConcurrency" and weight = 69.85 and global0 = "navigator" ) or - ( this = "vendorSub" and weight = 2499.81 and global0 = "navigator" ) + ( this = "keyboard" and weight = 2171.11 and global0 = "navigator" ) or - ( this = "productSub" and weight = 358.84 and global0 = "navigator" ) + ( this = "appCodeName" and weight = 167.15 and global0 = "navigator" ) or - ( this = "webkitTemporaryStorage" and weight = 39.44 and global0 = "navigator" ) + ( this = "webkitTemporaryStorage" and weight = 38.32 and global0 = "navigator" ) or - ( this = "hardwareConcurrency" and weight = 71.06 and global0 = "navigator" ) + ( this = "vendorSub" and weight = 2404.06 and global0 = "navigator" ) or - ( this = "appCodeName" and weight = 163.35 and global0 = "navigator" ) + ( this = "productSub" and weight = 372.86 and global0 = "navigator" ) or - ( this = "keyboard" and weight = 2868.74 and global0 = "navigator" ) + ( this = "webkitPersistentStorage" and weight = 129.8 and global0 = "navigator" ) or - ( this = "mediaDevices" and weight = 154.67 and global0 = "navigator" ) + ( this = "storage" and weight = 39.68 and global0 = "navigator" ) or - ( this = "mediaCapabilities" and weight = 32.35 and global0 = "navigator" ) + ( this = "presentation" and weight = 30.64 and global0 = "navigator" ) or - ( this = "permissions" and weight = 71.6 and global0 = "navigator" ) + ( this = "orientation" and weight = 35.14 and global0 = "screen" ) or - ( this = "presentation" and weight = 28.44 and global0 = "navigator" ) + ( this = "getGamepads" and weight = 214.18 and global0 = "navigator" ) or - ( this = "requestMediaKeySystemAccess" and weight = 32.15 and global0 = "navigator" ) - or - ( this = "webkitPersistentStorage" and weight = 141.94 and global0 = "navigator" ) - or - ( this = "getGamepads" and weight = 241.28 and global0 = "navigator" ) + ( this = "requestMediaKeySystemAccess" and weight = 35.23 and global0 = "navigator" ) } float getWeight() { diff --git a/.github/codeql/queries/autogen_fpGlobalObjectProperty1.qll b/.github/codeql/queries/autogen_fpGlobalObjectProperty1.qll index 726c13b21df..55bf499c839 100644 --- a/.github/codeql/queries/autogen_fpGlobalObjectProperty1.qll +++ b/.github/codeql/queries/autogen_fpGlobalObjectProperty1.qll @@ -8,7 +8,7 @@ class GlobalObjectProperty1 extends string { GlobalObjectProperty1() { - ( this = "enumerateDevices" and weight = 636.49 and global0 = "navigator" and global1 = "mediaDevices" ) + ( this = "enumerateDevices" and weight = 652.16 and global0 = "navigator" and global1 = "mediaDevices" ) } float getWeight() { diff --git a/.github/codeql/queries/autogen_fpGlobalTypeProperty0.qll b/.github/codeql/queries/autogen_fpGlobalTypeProperty0.qll index 217b532bd28..2336aed35f0 100644 --- a/.github/codeql/queries/autogen_fpGlobalTypeProperty0.qll +++ b/.github/codeql/queries/autogen_fpGlobalTypeProperty0.qll @@ -7,11 +7,11 @@ class GlobalTypeProperty0 extends string { GlobalTypeProperty0() { - ( this = "x" and weight = 5481.05 and global0 = "Gyroscope" ) + ( this = "x" and weight = 4676.46 and global0 = "Gyroscope" ) or - ( this = "y" and weight = 5481.05 and global0 = "Gyroscope" ) + ( this = "y" and weight = 4676.46 and global0 = "Gyroscope" ) or - ( this = "z" and weight = 5481.05 and global0 = "Gyroscope" ) + ( this = "z" and weight = 4676.46 and global0 = "Gyroscope" ) } float getWeight() { diff --git a/.github/codeql/queries/autogen_fpGlobalTypeProperty1.qll b/.github/codeql/queries/autogen_fpGlobalTypeProperty1.qll index 69f86d0386d..aa09218f981 100644 --- a/.github/codeql/queries/autogen_fpGlobalTypeProperty1.qll +++ b/.github/codeql/queries/autogen_fpGlobalTypeProperty1.qll @@ -8,7 +8,7 @@ class GlobalTypeProperty1 extends string { GlobalTypeProperty1() { - ( this = "resolvedOptions" and weight = 20.7 and global0 = "Intl" and global1 = "DateTimeFormat" ) + ( this = "resolvedOptions" and weight = 21.29 and global0 = "Intl" and global1 = "DateTimeFormat" ) } float getWeight() { diff --git a/.github/codeql/queries/autogen_fpGlobalVar.qll b/.github/codeql/queries/autogen_fpGlobalVar.qll index a9e7206e98a..dfa18ca9f42 100644 --- a/.github/codeql/queries/autogen_fpGlobalVar.qll +++ b/.github/codeql/queries/autogen_fpGlobalVar.qll @@ -6,23 +6,23 @@ class GlobalVar extends string { GlobalVar() { - ( this = "devicePixelRatio" and weight = 16.17 ) + ( this = "devicePixelRatio" and weight = 16.11 ) or - ( this = "screenX" and weight = 380.94 ) + ( this = "screenX" and weight = 378.78 ) or - ( this = "screenY" and weight = 342.21 ) + ( this = "screenY" and weight = 349.03 ) or - ( this = "outerWidth" and weight = 105.88 ) + ( this = "outerWidth" and weight = 110.41 ) or - ( this = "outerHeight" and weight = 154.3 ) + ( this = "outerHeight" and weight = 160.29 ) or - ( this = "screenLeft" and weight = 389.67 ) + ( this = "screenLeft" and weight = 380.11 ) or - ( this = "screenTop" and weight = 388.77 ) + ( this = "screenTop" and weight = 381.44 ) or - ( this = "indexedDB" and weight = 22.27 ) + ( this = "indexedDB" and weight = 22.05 ) or - ( this = "openDatabase" and weight = 37.78 ) + ( this = "openDatabase" and weight = 32.51 ) } float getWeight() { diff --git a/.github/codeql/queries/autogen_fpRenderingContextProperty.qll b/.github/codeql/queries/autogen_fpRenderingContextProperty.qll index 37b4db42fcf..1a99f02d32e 100644 --- a/.github/codeql/queries/autogen_fpRenderingContextProperty.qll +++ b/.github/codeql/queries/autogen_fpRenderingContextProperty.qll @@ -7,35 +7,35 @@ class RenderingContextProperty extends string { RenderingContextProperty() { - ( this = "getExtension" and weight = 26.62 and contextType = "webgl" ) + ( this = "getExtension" and weight = 27.52 and contextType = "webgl" ) or - ( this = "getParameter" and weight = 29.62 and contextType = "webgl" ) + ( this = "getParameter" and weight = 30.5 and contextType = "webgl" ) or - ( this = "getImageData" and weight = 63.37 and contextType = "2d" ) + ( this = "isPointInPath" and weight = 4676.46 and contextType = "2d" ) or - ( this = "measureText" and weight = 45.55 and contextType = "2d" ) + ( this = "getSupportedExtensions" and weight = 1324.74 and contextType = "webgl" ) or - ( this = "getParameter" and weight = 76.38 and contextType = "webgl2" ) + ( this = "getContextAttributes" and weight = 2060.8 and contextType = "webgl" ) or - ( this = "getShaderPrecisionFormat" and weight = 139.82 and contextType = "webgl2" ) + ( this = "getShaderPrecisionFormat" and weight = 1194.85 and contextType = "webgl" ) or - ( this = "getExtension" and weight = 78.73 and contextType = "webgl2" ) + ( this = "getExtension" and weight = 75.32 and contextType = "webgl2" ) or - ( this = "getContextAttributes" and weight = 193.02 and contextType = "webgl2" ) + ( this = "getParameter" and weight = 74.54 and contextType = "webgl2" ) or - ( this = "getSupportedExtensions" and weight = 347.91 and contextType = "webgl2" ) + ( this = "getSupportedExtensions" and weight = 340.32 and contextType = "webgl2" ) or - ( this = "getShaderPrecisionFormat" and weight = 1534.33 and contextType = "webgl" ) + ( this = "getContextAttributes" and weight = 176.36 and contextType = "webgl2" ) or - ( this = "getContextAttributes" and weight = 3155.81 and contextType = "webgl" ) + ( this = "getShaderPrecisionFormat" and weight = 127.49 and contextType = "webgl2" ) or - ( this = "getSupportedExtensions" and weight = 1559.61 and contextType = "webgl" ) + ( this = "getImageData" and weight = 60.04 and contextType = "2d" ) or - ( this = "isPointInPath" and weight = 5481.05 and contextType = "2d" ) + ( this = "readPixels" and weight = 40.63 and contextType = "webgl" ) or - ( this = "readPixels" and weight = 26.87 and contextType = "webgl" ) + ( this = "readPixels" and weight = 805.02 and contextType = "webgl2" ) or - ( this = "readPixels" and weight = 947.57 and contextType = "webgl2" ) + ( this = "measureText" and weight = 37.13 and contextType = "2d" ) } float getWeight() { diff --git a/.github/codeql/queries/autogen_fpSensorProperty.qll b/.github/codeql/queries/autogen_fpSensorProperty.qll index 250f645e681..9984b83b315 100644 --- a/.github/codeql/queries/autogen_fpSensorProperty.qll +++ b/.github/codeql/queries/autogen_fpSensorProperty.qll @@ -6,7 +6,7 @@ class SensorProperty extends string { SensorProperty() { - ( this = "start" and weight = 60.75 ) + ( this = "start" and weight = 52.76 ) } float getWeight() { diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 6c61aaa320a..d89320078ad 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -6,6 +6,9 @@ autolabeler: title: - '/^(?!.*(bug|initial|release|fix)).*$/i' categories: + - title: '🏛 Core PRs' + labels: + - 'core' - title: '🚀 New Features' label: 'feature' - title: '🐛 Bug Fixes' diff --git a/.github/workflows/PR-assignment-deps.yml b/.github/workflows/PR-assignment-deps.yml index 2d7fce88837..e768e3caa5e 100644 --- a/.github/workflows/PR-assignment-deps.yml +++ b/.github/workflows/PR-assignment-deps.yml @@ -1,7 +1,7 @@ name: Compile dependencies.json for PR assignment checks on: pull_request: - types: [opened, synchronize, reopened] + types: [opened, synchronize, reopened, ready_for_review] permissions: contents: read jobs: diff --git a/.github/workflows/PR-assignment.yml b/.github/workflows/PR-assignment.yml index 40167458368..6c4c5d7f537 100644 --- a/.github/workflows/PR-assignment.yml +++ b/.github/workflows/PR-assignment.yml @@ -35,7 +35,7 @@ jobs: npm install @aws-sdk/client-s3 - name: Get PR properties id: get-props - uses: actions/github-script@v8 + uses: actions/github-script@v9 env: AWS_ACCESS_KEY_ID: ${{ vars.PR_BOT_AWS_AK }} AWS_SECRET_ACCESS_KEY: ${{ secrets.PR_BOT_AWS_SAK }} @@ -58,10 +58,25 @@ jobs: return props; - name: Assign reviewers if: ${{ !fromJSON(steps.get-props.outputs.result).review.ok }} - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: github-token: ${{ steps.token.outputs.token }} script: | const assignReviewers = require('./.github/workflows/scripts/assignReviewers.js') const reviewers = await assignReviewers({github, context, prData: ${{ steps.get-props.outputs.result }} }); console.log('Assigned reviewers:', JSON.stringify(reviewers, null, 2)); + - name: Auto-label core PR + if: ${{ fromJSON(steps.get-props.outputs.result).isCoreChange }} + uses: actions/github-script@v8 + with: + github-token: ${{ steps.token.outputs.token }} + script: | + const ghRequester = require('.github/workflows/scripts/ghRequest.js'); + const request = ghRequester(github); + const prData = ${{ steps.get-props.outputs.result }}; + await request('POST /repos/{owner}/{repo}/issues/{issue_number}/labels', { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prData.pr, + labels: ['core'], + }); diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml index 65c7d8d6540..c5b2db44baa 100644 --- a/.github/workflows/browser-tests.yml +++ b/.github/workflows/browser-tests.yml @@ -33,7 +33,7 @@ jobs: with: name: ${{ needs.build.outputs.built-key }} - name: "Define testing strategy" - uses: actions/github-script@v8 + uses: actions/github-script@v9 id: define env: browserstack: ${{ secrets.BROWSERSTACK_USER_NAME }} diff --git a/.github/workflows/code-path-changes.yml b/.github/workflows/code-path-changes.yml index 8552489365f..c4ff492f0df 100644 --- a/.github/workflows/code-path-changes.yml +++ b/.github/workflows/code-path-changes.yml @@ -7,12 +7,11 @@ on: - '**' env: - OAUTH2_CLIENT_ID: ${{ secrets.OAUTH2_CLIENT_ID }} - OAUTH2_CLIENT_SECRET: ${{ secrets.OAUTH2_CLIENT_SECRET }} - OAUTH2_REFRESH_TOKEN: ${{ secrets.OAUTH2_REFRESH_TOKEN }} GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NOTIFICATION_EMAIL: 'prebid-notifications@prebid.org' + NOTIFICATION_PASSWORD: ${{ secrets.NOTIFICATION_PASSWORD }} permissions: contents: read diff --git a/.github/workflows/comment.yml b/.github/workflows/comment.yml index 73fca067c05..ffdecd3b630 100644 --- a/.github/workflows/comment.yml +++ b/.github/workflows/comment.yml @@ -28,7 +28,7 @@ jobs: - name: 'Comment on PR' if: ${{ steps.get-comment.outputs.exists == 'true' }} - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const fs = require('fs'); diff --git a/.github/workflows/jscpd.yml b/.github/workflows/jscpd.yml index e426c9acb58..9c7366c8602 100644 --- a/.github/workflows/jscpd.yml +++ b/.github/workflows/jscpd.yml @@ -96,7 +96,7 @@ jobs: - name: Generate PR comment if: env.filtered_report_exists == 'true' - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const fs = require('fs'); diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index ac64a3a4dfb..2457c36e387 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -51,7 +51,7 @@ jobs: run: npx eslint --no-inline-config --format json $(cat __changed_files.txt | xargs stat --printf '%n\n' 2> /dev/null) > __pr.json || true - name: Compare them and post comment if necessary - uses: actions/github-script@v8 + uses: actions/github-script@v9 id: comment with: result-encoding: string diff --git a/.github/workflows/scripts/getPRProperties.js b/.github/workflows/scripts/getPRProperties.js index 90273e441a1..1d4bb8b4422 100644 --- a/.github/workflows/scripts/getPRProperties.js +++ b/.github/workflows/scripts/getPRProperties.js @@ -11,8 +11,11 @@ const MODULE_PATTERNS = [ const EXCLUDE_PATTERNS = [ /^test\//, - /^integrationExamples\// + /^integrationExamples\//, + /^[^\/]+$/, + /^.github\//, ] + const LIBRARY_PATTERN = /^libraries\/([^\/]+)\//; function extractVendor(chunkName) { @@ -134,6 +137,7 @@ async function getPRProperties({github, context, prNo, reviewerTeam, engTeam, au }); const data = { pr: prNo, + draft: pr.data.draft, author: { login: author, isPrebidMember: await isPrebidMember(author) @@ -145,7 +149,7 @@ async function getPRProperties({github, context, prNo, reviewerTeam, engTeam, au review, } data.review.requires = reviewRequirements(data); - data.review.ok = satisfiesReviewRequirements(data.review); + data.review.ok = data.draft || satisfiesReviewRequirements(data.review); return data; } diff --git a/.github/workflows/scripts/send-notification-on-change.js b/.github/workflows/scripts/send-notification-on-change.js index 463481b0230..0d1ab40eab9 100644 --- a/.github/workflows/scripts/send-notification-on-change.js +++ b/.github/workflows/scripts/send-notification-on-change.js @@ -9,43 +9,16 @@ const fs = require('fs'); const path = require('path'); const nodemailer = require('nodemailer'); -async function getAccessToken(clientId, clientSecret, refreshToken) { - try { - const response = await fetch('https://oauth2.googleapis.com/token', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - client_id: clientId, - client_secret: clientSecret, - refresh_token: refreshToken, - grant_type: 'refresh_token', - }), - }); - if (!response.ok) { - const errorBody = await response.text().catch(() => ''); - throw new Error(`OAuth token request failed: ${response.status} ${response.statusText} - ${errorBody}`); - } - const data = await response.json(); - return data.access_token; - } catch (error) { - console.error('Failed to fetch access token:', error.message); - process.exit(1); - } -} - (async () => { const configFilePath = path.join(__dirname, 'codepath-notification'); const repo = process.env.GITHUB_REPOSITORY; const prNumber = process.env.GITHUB_PR_NUMBER; const token = process.env.GITHUB_TOKEN; - - // Generate OAuth2 access token - const clientId = process.env.OAUTH2_CLIENT_ID; - const clientSecret = process.env.OAUTH2_CLIENT_SECRET; - const refreshToken = process.env.OAUTH2_REFRESH_TOKEN; + const sender = process.env.NOTIFICATION_EMAIL; + const pass = process.env.NOTIFICATION_PASSWORD; // validate params - if (!repo || !prNumber || !token || !clientId || !clientSecret || !refreshToken) { + if (!repo || !prNumber || !token || !sender || !pass) { console.error('Missing required environment variables.'); process.exit(1); } @@ -107,22 +80,11 @@ async function getAccessToken(clientId, clientSecret, refreshToken) { console.log('Grouped matches by email:', matchesByEmail); - // get ready to email the changes - const accessToken = await getAccessToken(clientId, clientSecret, refreshToken); - - // Configure Nodemailer with OAuth2 - // service: 'Gmail', const transporter = nodemailer.createTransport({ - host: "smtp.gmail.com", - port: 465, - secure: true, + service: 'gmail', auth: { - type: 'OAuth2', - user: 'info@prebid.org', - clientId: clientId, - clientSecret: clientSecret, - refreshToken: refreshToken, - accessToken: accessToken + user: sender, + pass }, }); @@ -139,7 +101,7 @@ async function getAccessToken(clientId, clientSecret, refreshToken) { try { await transporter.sendMail({ - from: `"Prebid Info" `, + from: `"Prebid Notifications" <${sender}>`, to: email, subject: `Files have been changed in open source ${repo}`, html: emailBody, diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a174a1f8a09..87b9d5cf64d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,11 +35,14 @@ jobs: uses: actions/checkout@v6 with: ref: refs/pull/${{ github.event.pull_request.number }}/head + fetch-depth: 0 - name: Checkout code (push) id: checkout-push if: ${{ github.event_name == 'push' }} uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@v6 @@ -49,12 +52,20 @@ jobs: - name: Commit info id: info run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + BASE_BRANCH="${{ github.event.pull_request.base.ref }}" + git fetch origin "$BASE_BRANCH" + BASE_COMMIT="$(git merge-base HEAD "origin/$BASE_BRANCH")" + else + BASE_BRANCH="${{ github.ref }}" + BASE_COMMIT="${{ github.event.before }}" + fi echo ref="${{ steps.checkout-pr.outputs.ref || steps.checkout-push.outputs.ref }}" >> $GITHUB_OUTPUT echo commit="${{ steps.checkout-pr.outputs.commit || steps.checkout-push.outputs.commit }}" >> $GITHUB_OUTPUT echo branch="${{ github.head_ref || github.ref }}" >> $GITHUB_OUTPUT echo fork="${{ (github.event.pull_request && github.event.pull_request.head.repo.owner.login != github.repository_owner) && github.event.pull_request.head.repo.owner.login || null }}" >> $GITHUB_OUTPUT - echo base-branch="${{ github.event.pull_request.base.ref || github.ref }}" >> $GITHUB_OUTPUT - echo base-commit="${{ github.event.pull_request.base.sha || github.event.before }}" >> $GITHUB_OUTPUT + echo base-branch="$BASE_BRANCH" >> $GITHUB_OUTPUT + echo base-commit="$BASE_COMMIT" >> $GITHUB_OUTPUT - name: Install dependencies uses: ./.github/actions/npm-ci @@ -112,9 +123,17 @@ jobs: with: name: ${{ needs.test.outputs.coverage }} + - name: Combine coverage results + shell: bash + run: | + sudo apt-get update + sudo apt-get install lcov -y + find build/coverage/chunks -name 'lcov.info' -printf ' -a %p' | xargs lcov -o build/coverage/combined.info + - name: Coveralls uses: coverallsapp/github-action@v2 with: + file: build/coverage/combined.info git-branch: ${{ needs.checkout.outputs.fork && format('{0}:{1}', needs.checkout.outputs.fork, needs.checkout.outputs.branch) || needs.checkout.outputs.branch }} git-commit: ${{ needs.checkout.outputs.commit }} compare-ref: ${{ needs.checkout.outputs.base-branch }} diff --git a/eslint.config.js b/eslint.config.js index a5310749559..731287247ad 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -93,6 +93,10 @@ module.exports = [ tagNamePreference: { return: 'return' } + }, + 'import/resolver': { + [path.resolve('./plugins/eslint/resolver')]: true, + node: true, } }, languageOptions: { @@ -114,7 +118,13 @@ module.exports = [ 'no-console': 'error', 'space-before-function-paren': 'off', '@stylistic/space-before-function-paren': 'off', - 'import/extensions': ['error', 'ignorePackages'], + 'import/no-unresolved': 'error', + 'import/named': 'error', + 'import/default': 'error', + 'import/export': 'error', + 'import/no-named-as-default': 'warn', + 'import/no-named-as-default-member': 'warn', + 'import/no-duplicates': 'warn', 'no-restricted-syntax': [ 'error', { diff --git a/gulp.precompilation.js b/gulp.precompilation.js index f8b533d1635..d46674078b8 100644 --- a/gulp.precompilation.js +++ b/gulp.precompilation.js @@ -2,12 +2,10 @@ const webpackStream = require('webpack-stream'); const gulp = require('gulp'); const helpers = require('./gulpHelpers.js'); const {argv} = require('yargs'); -const sourcemaps = require('gulp-sourcemaps'); const babel = require('gulp-babel'); const {glob} = require('glob'); const path = require('path'); const tap = require('gulp-tap'); -const wrap = require('gulp-wrap') const _ = require('lodash'); const fs = require('fs'); const filter = import('gulp-filter'); @@ -28,13 +26,21 @@ const babelPrecomp = _.memoize( function ({distUrlBase = null, disableFeatures = null, dev = false} = {}) { const babelConfig = require('./babelConfig.js')(getDefaults({distUrlBase, disableFeatures, dev})); return function () { - return gulp.src(helpers.getSourcePatterns(), {base: '.', since: gulp.lastRun(babelPrecomp({distUrlBase, disableFeatures, dev}))}) - .pipe(sourcemaps.init()) + const sourceRoot = path.resolve('.'); + const relativeSourceRoot = path.relative(helpers.getPrecompiledPath(), sourceRoot); + return gulp.src(helpers.getSourcePatterns(), { + base: '.', + since: gulp.lastRun(babelPrecomp({distUrlBase, disableFeatures, dev})), + sourcemaps: true + }) .pipe(babel(babelConfig)) - .pipe(sourcemaps.write('.', { - sourceRoot: path.relative(helpers.getPrecompiledPath(), path.resolve('.')) + .pipe(tap(file => { + file.sourceMap.file = file.basename; + file.sourceMap.sourceRoot = path.join(relativeSourceRoot, path.relative(file.dirname, sourceRoot)) })) - .pipe(gulp.dest(helpers.getPrecompiledPath())); + .pipe(gulp.dest(helpers.getPrecompiledPath(), { + sourcemaps: '.' + })); } }, ({dev, distUrlBase, disableFeatures} = {}) => `${dev}::${distUrlBase ?? ''}::${(disableFeatures ?? []).join(':')}` @@ -192,7 +198,7 @@ const buildCreative = _.memoize( function buildCreative({dev = false} = {}) { const opts = { mode: dev ? 'development' : 'production', - devtool: false + devtool: dev ? 'inline-source-map': false }; return function() { return gulp.src(['creative/**/*'], {since: gulp.lastRun(buildCreative({dev}))}) @@ -204,8 +210,11 @@ const buildCreative = _.memoize( ) function generateCreativeRenderers() { + const tpl = _.template('// this file is autogenerated, see creative/README.md\nexport const RENDERER = <%= JSON.stringify(contents.toString()) %>'); return gulp.src(['build/creative/renderers/**/*.js'], {since: gulp.lastRun(generateCreativeRenderers)}) - .pipe(wrap('// this file is autogenerated, see creative/README.md\nexport const RENDERER = <%= JSON.stringify(contents.toString()) %>')) + .pipe(tap((file) => { + file.contents = Buffer.from(tpl({contents: file.contents})); + })) .pipe(gulp.dest(helpers.getCreativeRendererPath())) } @@ -220,13 +229,16 @@ function precompile(options = {}) { generateCoreSummary, generateModuleSummary, generateGlobalDef(options), - ]) - ]); + ]), + ].concat(options.dev ? [] : [ + 'ts-strict' + ])); } gulp.task('ts', helpers.execaTask('tsc')); -gulp.task('ts-dev', helpers.execaTask('tsc --incremental')) +gulp.task('ts-dev', helpers.execaTask('tsc --incremental')); +gulp.task('ts-strict', helpers.execaTask('tsc -p tsconfig-strict.json')); gulp.task('transpile', babelPrecomp()); gulp.task('precompile-dev', precompile({dev: true})); gulp.task('precompile', precompile()); diff --git a/gulpfile.js b/gulpfile.js index d3cc07e4583..b2e2a61ff99 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -17,15 +17,12 @@ const execaTask = helpers.execaTask; var concat = require('gulp-concat'); var replace = require('gulp-replace'); const execaCmd = require('execa'); -var gulpif = require('gulp-if'); -var sourcemaps = require('gulp-sourcemaps'); var through = require('through2'); var fs = require('fs'); var jsEscape = require('gulp-js-escape'); const path = require('path'); const {minify} = require('terser'); const Vinyl = require('vinyl'); -const wrap = require('gulp-wrap'); const rename = require('gulp-rename'); const merge = require('merge-stream'); @@ -153,24 +150,6 @@ function makeWebpackPkg(config = webpackConfig) { } } -function buildCreative(mode = 'production') { - const opts = {mode}; - if (mode === 'development') { - opts.devtool = 'inline-source-map' - } - return function() { - return gulp.src(['creative/**/*']) - .pipe(webpackStream(Object.assign(require('./webpack.creative.js'), opts))) - .pipe(gulp.dest('build/creative')) - } -} - -function updateCreativeRenderers() { - return gulp.src(['build/creative/renderers/**/*']) - .pipe(wrap('// this file is autogenerated, see creative/README.md\nexport const RENDERER = <%= JSON.stringify(contents.toString()) %>')) - .pipe(gulp.dest(helpers.getCreativeRendererPath())) -} - function updateCreativeExample(cb) { const CREATIVE_EXAMPLE = 'integrationExamples/gpt/x-domain/creative.html'; const root = require('node-html-parser').parse(fs.readFileSync(CREATIVE_EXAMPLE)); @@ -188,7 +167,10 @@ function getModulesListToAddInBanner(modules) { } function gulpBundle(dev) { - return bundle(dev).pipe(gulp.dest('build/' + (dev ? 'dev' : 'dist'))); + return bundle(dev) + .pipe(gulp.dest('build/' + (dev ? 'dev' : 'dist'), { + sourcemaps: dev || argv.sourceMaps ? '.' : false + })); } function nodeBundle(modules, dev = false) { @@ -206,16 +188,26 @@ function nodeBundle(modules, dev = false) { }); } -function memoryVinyl(name, contents) { - return new Vinyl({ +function memoryVinyl(name, contents, sourcemaps = false) { + const file = new Vinyl({ cwd: '', base: 'generated', path: name, contents: Buffer.from(contents, 'utf-8') }); + if (sourcemaps) { + file.sourceMap = { + version: 3, + names: [], + mappings: '', + sources: [name], + sourcesContent: [contents] + }; + } + return file; } -function wrapWithHeaderAndFooter(dev, modules) { +function wrapWithHeaderAndFooter(dev, modules, sourcemaps = false) { // NOTE: gulp-header, gulp-footer & gulp-wrap do not play nice with source maps. // gulp-concat does; for that reason we are prepending and appending the source stream with "fake" header & footer files. return function wrap(stream) { @@ -234,10 +226,10 @@ function wrapWithHeaderAndFooter(dev, modules) { throw new Error(`Cannot parse bundle template; it must contain exactly one instance of '${placeholder}'`); } const [header, footer] = parts; - wrapped.push(memoryVinyl('prebid-header.js', header)); + wrapped.push(memoryVinyl('prebid-header.js', header, sourcemaps)); stream.pipe(wrapped, {end: false}); stream.on('end', () => { - wrapped.push(memoryVinyl('prebid-footer.js', footer)); + wrapped.push(memoryVinyl('prebid-footer.js', footer, sourcemaps)); wrapped.push(null); }); }) @@ -308,11 +300,9 @@ function bundle(dev, moduleArr) { fancyLog('Generating bundle:', outputFileName); fancyLog('Generating storage use disclosure summary:', disclosureFile); - const wrap = wrapWithHeaderAndFooter(dev, modules); - const source = wrap(gulp.src(entries)) - .pipe(gulpif(sm, sourcemaps.init({ loadMaps: true }))) - .pipe(concat(outputFileName)) - .pipe(gulpif(sm, sourcemaps.write('.'))); + const wrap = wrapWithHeaderAndFooter(dev, modules, sm); + const source = wrap(gulp.src(entries, {sourcemaps: sm})) + .pipe(concat(outputFileName)); const disclosure = disclosureSummary(['prebid-core'].concat(modules), disclosureFile); return merge(source, disclosure); } diff --git a/integrationExamples/gpt/x-domain/creative.html b/integrationExamples/gpt/x-domain/creative.html index 14deeb4f559..ae8456c19e0 100644 --- a/integrationExamples/gpt/x-domain/creative.html +++ b/integrationExamples/gpt/x-domain/creative.html @@ -2,7 +2,7 @@ // creative will be rendered, e.g. GAM delivering a SafeFrame // this code is autogenerated, also available in 'build/creative/creative.js' - + ' + res.tag, - mediaType: res.mediaType.name || 'banner', + ad: `${res.tag}`, + mediaType: deepAccess(res, 'mediaType.name') || BANNER, meta: { - advertiserDomains: res.adDomains && res.adDomains.length ? res.adDomains : [], - mediaType: res.mediaType.name || 'banner', - } - }; - bidResponses.push(bidResponse); - logInfo('bidResponses', bidResponses); + advertiserDomains: deepAccess(res, 'adDomains') || [], + mediaType: deepAccess(res, 'mediaType.name') || BANNER, + }, + }); + logInfo('bidResponses', bidResponses); return bidResponses; }, @@ -200,6 +191,9 @@ export const spec = { } const copyOfBid = { ...bid } + + const uuidMatch = copyOfBid.ad && typeof copyOfBid.ad === 'string' ? copyOfBid.ad.match(/data-uuid="([^"]*)"/) : null; + copyOfBid.uuid = uuidMatch ? uuidMatch[1] : null; delete copyOfBid.ad const shortBidString = JSON.stringify(copyOfBid); const encodedBuf = window.btoa(shortBidString); @@ -258,10 +252,6 @@ export const spec = { 'ccpa_consent': uspConsent && uspConsent.uspConsent ? uspConsent.uspConsent : '', }; - if (window.qid) { // only for new users (new qid) - syncData.qid = window.qid; - } - const syncUrlObject = { protocol: ADQUERY_BIDDER_DOMAIN_PROTOCOL, hostname: ADQUERY_USER_SYNC_DOMAIN, @@ -289,24 +279,22 @@ export const spec = { function buildRequest(bid, bidderRequest, isVideo = false) { let userId = null; - if (window.qid) { - userId = window.qid; - } - if (bid.userId && bid.userId.qid) { - userId = bid.userId.qid - } + const eids = bid.userIdAsEids; + if (Array.isArray(eids)) { + const adqueryEid = eids.find(eid => eid.source === 'adquery.io'); + userId = adqueryEid?.uids?.[0]?.id; - if (!userId) { - userId = bid.ortb2?.user.ext.eids.find(eid => eid.source === "adquery.io")?.uids[0]?.id; + if (!userId) { + userId = eids[0]?.uids?.[0]?.id; + } } if (!userId) { - // onetime User ID - const ramdomValues = Array.from(window.crypto.getRandomValues(new Uint32Array(4))); - userId = ramdomValues.map(val => val.toString(36)).join('').substring(0, 20); + const randomValues = Array.from(window.crypto.getRandomValues(new Uint32Array(4))); + const randomPart = randomValues.map(val => val.toString(36)).join('').substring(0, 26); + userId = `qd_${randomPart}`; logMessage('generated onetime User ID: ', userId); - window.qid = userId; } let pageUrl = ''; @@ -327,6 +315,14 @@ function buildRequest(bid, bidderRequest, isVideo = false) { deepSetValue(videoRequest, 'site.ext.bidder', bid.params); videoRequest.id = bid.bidId + if (bidderRequest?.gdprConsent?.consentString) { + deepSetValue(videoRequest, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies ? 1 : 0); + deepSetValue(videoRequest, 'user.consent', bidderRequest.gdprConsent.consentString); + } + if (bidderRequest?.uspConsent) { + deepSetValue(videoRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + let currency = bid?.ortb2?.ext?.prebid?.adServerCurrency || "PLN"; videoRequest.cur = [currency] @@ -363,6 +359,9 @@ function buildRequest(bid, bidderRequest, isVideo = false) { bidRequestsCount: bid.bidRequestsCount, bidderRequestsCount: bid.bidderRequestsCount, sizes: parseSizesInput(bid.mediaTypes.banner.sizes).toString(), + gdpr: bidderRequest?.gdprConsent?.gdprApplies ? 1 : 0, + gdpr_consent: bidderRequest?.gdprConsent?.consentString || '', + us_privacy: bidderRequest?.uspConsent || '', }; } diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index b14b0f28d82..2d0e54cb176 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -49,7 +49,6 @@ export const spec = { aliases: [ 'streamkey', 'janet', - { code: 'selectmedia', gvlid: 775 }, { code: 'ocm', gvlid: 1148 }, '9dotsmedia', 'indicue', diff --git a/modules/adtrueBidAdapter.js b/modules/adtrueBidAdapter.js index 9fdb1c051c4..bdb50e98e0d 100644 --- a/modules/adtrueBidAdapter.js +++ b/modules/adtrueBidAdapter.js @@ -220,7 +220,7 @@ function _parseNativeResponse(bid, newBid) { try { adm = JSON.parse(bid.adm.replace(/\\/g, '')); } catch (ex) { - // logWarn(LOG_WARN_PREFIX + 'Error: Cannot parse native reponse for ad response: ' + newBid.adm); + // logWarn(LOG_WARN_PREFIX + 'Error: Cannot parse native response for ad response: ' + newBid.adm); return; } if (adm && adm.native && adm.native.assets && adm.native.assets.length > 0) { @@ -359,7 +359,7 @@ function _checkMediaType(adm, newBid) { newBid.mediaType = NATIVE; } } catch (e) { - logWarn(LOG_WARN_PREFIX + 'Error: Cannot parse native reponse for ad response: ' + adm); + logWarn(LOG_WARN_PREFIX + 'Error: Cannot parse native response for ad response: ' + adm); } } } diff --git a/modules/aidemBidAdapter.md b/modules/aidemBidAdapter.md index dece9f065ee..c8fb750ea34 100644 --- a/modules/aidemBidAdapter.md +++ b/modules/aidemBidAdapter.md @@ -31,7 +31,7 @@ This module is GDPR and CCPA compliant, and no 3rd party userIds are allowed. ### Video Bid Params | Name | Scope | Description | Example | Type | |---------------|----------|-----------------------------------------|-----------------|-----------| -| `context` | required | One of instream, outstream, adpod | `'instream'` | `String` | +| `context` | required | One of instream, outstream | `'instream'` | `String` | | `playerSize` | required | Width and height of the player | `'[640, 480]'` | `Array` | | `maxduration` | required | Maximum video ad duration, in seconds | `30` | `Integer` | | `minduration` | required | Minimum video ad duration, in seconds | `5` | `Integer` | diff --git a/modules/anzuSSPBidAdapter.md b/modules/anzuSSPBidAdapter.md new file mode 100644 index 00000000000..fcc70ca894a --- /dev/null +++ b/modules/anzuSSPBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Anzu SSP Bidder Adapter +Module Type: Anzu SSP Bidder Adapter +Maintainer: prebid@anzu.io +``` + +# Description + +Connects to Anzu SSP exchange for bids. +Anzu SSP bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'anzuSSP', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'anzuSSP', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'anzuSSP', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/modules/anzuSSPBidAdapter.ts b/modules/anzuSSPBidAdapter.ts new file mode 100644 index 00000000000..f72999f58f5 --- /dev/null +++ b/modules/anzuSSPBidAdapter.ts @@ -0,0 +1,17 @@ +import { type BidderSpec, registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { isBidRequestValid, buildRequests, interpretResponse } from '../libraries/teqblazeUtils/bidderUtils.ts'; + +const BIDDER_CODE = 'anzuSSP'; +const AD_URL = 'https://endpoint.anzumarketplace.com/pbjs'; + +export const spec: BidderSpec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, +}; + +registerBidder(spec); diff --git a/modules/apesterBidAdapter.js b/modules/apesterBidAdapter.js index 6d9cedb16f2..fbbb5b9f18e 100644 --- a/modules/apesterBidAdapter.js +++ b/modules/apesterBidAdapter.js @@ -9,6 +9,10 @@ import { createInterpretResponseFn } from '../libraries/vidazooUtils/bidderUtils.js'; +/** + * @typedef {import('./apesterBidAdapterTypes.d.ts').ApesterBidRequestParams} ApesterBidRequestParams + */ + const DEFAULT_SUB_DOMAIN = 'bidder'; const BIDDER_CODE = 'apester'; const BIDDER_VERSION = '1.0.0'; diff --git a/modules/apesterBidAdapterTypes.d.ts b/modules/apesterBidAdapterTypes.d.ts new file mode 100644 index 00000000000..29690dc40ca --- /dev/null +++ b/modules/apesterBidAdapterTypes.d.ts @@ -0,0 +1,9 @@ +import { VidazooBaseBidderParams } from "../libraries/vidazooUtils/vidazooTypes.ts"; + +export type ApesterBidRequestParams = VidazooBaseBidderParams + +declare module '../src/adUnits' { + interface BidderParams { + apester: ApesterBidRequestParams; + } +} diff --git a/modules/asterioBidAdapter.md b/modules/asterioBidAdapter.md new file mode 100644 index 00000000000..f18b7156bd7 --- /dev/null +++ b/modules/asterioBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +``` +Module Name: Asterio Bidder Adapter +Module Type: Bidder Adapter +Maintainer: mnikulin@asteriosoft.com +``` + +# Description + +Connects to Asterio Bidder for bids. +Asterio bid adapter supports Banner and Video ads. + +# Bid Params + +| Name | Scope | Type | Description | +| ---- | ----- | ---- | ----------- | +| `adUnitToken` | required | String | Asterio ad unit token provided by Asterio. | +| `pos` | optional | Number | Ad position override. When omitted, the adapter uses `mediaTypes.banner.pos` or `mediaTypes.video.pos` from the ad unit. | + +# Test Parameters +``` +const adUnits = [ + { + bids: [ + { + bidder: 'asterio', + params: { + adUnitToken: '????????-????-????-????-????????????', // adUnitToken provided by Asterio + } + } + ] + } +]; +``` diff --git a/modules/asterioBidAdapter.ts b/modules/asterioBidAdapter.ts new file mode 100644 index 00000000000..313eb4420b5 --- /dev/null +++ b/modules/asterioBidAdapter.ts @@ -0,0 +1,149 @@ +import { type AdapterRequest, type BidderSpec, type ServerResponse, registerBidder } from '../src/adapters/bidderFactory.js'; +import { deepAccess, deepClone } from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import type { BidRequest } from '../src/adapterManager.js'; +import type { Size } from '../src/types/common.d.ts'; + +const BIDDER_CODE = 'asterio'; +export const ENDPOINT = 'https://bid.asterio.ai/prebid/bid'; + +export type AsterioBidParams = { + adUnitToken: string; + pos?: number; +}; + +declare module '../src/adUnits' { + interface BidderParams { + [BIDDER_CODE]: AsterioBidParams; + } +} + +type AsterioBidPayload = { + bidId: string; + adUnitToken: string; + pos?: number; + sizes: Array<{ width: number; height: number }>; +}; + +type AsterioServerBid = { + ad?: string; + requestId: string; + cpm: string | number; + currency?: string; + width: number; + height: number; + ttl: number; + creativeId: string; + netRevenue?: boolean; + mediaType?: string; + format?: string; + adomain?: string[]; +}; + +export const spec: BidderSpec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + isBidRequestValid: function (bid) { + return !!(bid.params && bid.params.adUnitToken); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const bids: AsterioBidPayload[] = validBidRequests.map(bidRequest => ({ + bidId: bidRequest.bidId, + adUnitToken: bidRequest.params.adUnitToken, + pos: getPosition(bidRequest), + sizes: prepareSizes(bidRequest.sizes) + })); + + const payload: { + requestId: string; + bids: AsterioBidPayload[]; + referer: string; + schain: unknown; + gdprConsent?: { + consentRequired: boolean; + consentString?: string; + }; + } = { + requestId: bidderRequest.bidderRequestId, + bids, + referer: bidderRequest.refererInfo?.page, + schain: validBidRequests[0]?.ortb2?.source?.ext?.schain + }; + + if (bidderRequest?.gdprConsent) { + payload.gdprConsent = { + consentRequired: typeof bidderRequest.gdprConsent.gdprApplies === 'boolean' ? bidderRequest.gdprConsent.gdprApplies : false, + consentString: bidderRequest.gdprConsent.consentString + }; + } + + return { + method: 'POST', + url: ENDPOINT, + data: payload, + options: { + contentType: 'text/plain', + customHeaders: { + 'Rtb-Direct': 'true' + } + } + }; + }, + + interpretResponse: function (serverResponse: ServerResponse, _request: AdapterRequest) { + const serverBody = serverResponse.body; + if (!serverBody || typeof serverBody !== 'object' || !Array.isArray(serverBody.bids)) { + return []; + } + + return serverBody.bids.map((bidResponse: AsterioServerBid) => { + const bid = deepClone(bidResponse); + + bid.cpm = parseFloat(String(bidResponse.cpm)); + bid.requestId = bidResponse.requestId; + bid.ad = bidResponse.ad; + bid.width = bidResponse.width; + bid.height = bidResponse.height; + bid.currency = bidResponse.currency || 'USD'; + bid.netRevenue = typeof bidResponse.netRevenue === 'boolean' ? bidResponse.netRevenue : true; + bid.ttl = bidResponse.ttl; + bid.creativeId = bidResponse.creativeId; + bid.mediaType = bidResponse.mediaType || bidResponse.format || 'banner'; + + if (VIDEO === bid.mediaType && bidResponse.ad) { + bid.vastXml = bidResponse.ad; + } + + bid.meta = {}; + bid.meta.advertiserDomains = bid.adomain || []; + + return bid; + }); + }, + + onBidWon: function (bid: { winUrl?: string; cpm: number }) { + if (bid.winUrl) { + const winUrl = bid.winUrl.replace(/\$\{AUCTION_PRICE}/, String(bid.cpm)); + ajax(winUrl, null, undefined, { keepalive: true }); + return true; + } + return false; + } +}; + +function prepareSizes(sizes: Size | Size[]) { + if (!Array.isArray(sizes) || sizes.length === 0) { + return []; + } + const normalizedSizes = typeof sizes[0] === 'number' ? [sizes] : sizes; + return normalizedSizes.map(size => ({ width: size[0], height: size[1] })); +} + +function getPosition(bidRequest: BidRequest): number | undefined { + return bidRequest.params.pos ?? deepAccess(bidRequest, 'mediaTypes.banner.pos') ?? deepAccess(bidRequest, 'mediaTypes.video.pos'); +} + +registerBidder(spec); diff --git a/modules/bidResponseFilter/index.js b/modules/bidResponseFilter/index.js index 8da669dd8f8..faf29810b84 100644 --- a/modules/bidResponseFilter/index.js +++ b/modules/bidResponseFilter/index.js @@ -11,6 +11,13 @@ export const BID_MEDIA_TYPE_REJECTION_REASON = `Media type is not allowed`; let moduleConfig; let enabled = false; +function isIbvBannerOnMultiFormatAdUnit(metaMediaType, bidRequest) { + const mediaTypes = Object.keys(bidRequest?.mediaTypes || {}); + return metaMediaType === 'banner' && + mediaTypes.length > 1 && + bidRequest?.mediaTypes?.video?.context === 'inbanner'; +} + function init() { config.getConfig(MODULE_NAME, (cfg) => { moduleConfig = cfg[MODULE_NAME]; @@ -36,7 +43,12 @@ export function addBidResponseHook(next, adUnitCode, bid, reject, index = auctio const catConfig = { enforce: true, blockUnknown: true, ...(moduleConfig?.cat || {}) }; const advConfig = { enforce: true, blockUnknown: true, ...(moduleConfig?.adv || {}) }; const attrConfig = { enforce: true, blockUnknown: true, ...(moduleConfig?.attr || {}) }; - const mediaTypesConfig = { enforce: true, blockUnknown: true, ...(moduleConfig?.mediaTypes || {}) }; + const mediaTypesConfig = { + enforce: true, + blockUnknown: true, + rejectIbvBannerOnMultiFormat: false, + ...(moduleConfig?.mediaTypes || {}) + }; const { primaryCatId, secondaryCatIds = [], @@ -50,6 +62,9 @@ export function addBidResponseHook(next, adUnitCode, bid, reject, index = auctio const normalizedMetaCattax = Number(metaCattax); const normalizedRequestCattax = Number(cattax); const isCattaxMatch = normalizedMetaCattax === normalizedRequestCattax; + const allowedMediaTypes = Object.keys(bidRequest?.mediaTypes || {}); + const rejectIbvBannerOnMultiFormat = mediaTypesConfig.rejectIbvBannerOnMultiFormat && + isIbvBannerOnMultiFormatAdUnit(metaMediaType, bidRequest); if ((catConfig.enforce && isCattaxMatch && bcat.some(category => [primaryCatId, ...secondaryCatIds].includes(category))) || (catConfig.blockUnknown && (!isCattaxMatch || !primaryCatId))) { reject(BID_CATEGORY_REJECTION_REASON); @@ -59,7 +74,7 @@ export function addBidResponseHook(next, adUnitCode, bid, reject, index = auctio } else if ((attrConfig.enforce && battr.includes(metaAttr)) || (attrConfig.blockUnknown && !metaAttr)) { reject(BID_ATTR_REJECTION_REASON); - } else if ((mediaTypesConfig.enforce && !Object.keys(bidRequest?.mediaTypes || {}).includes(metaMediaType)) || + } else if ((mediaTypesConfig.enforce && (!allowedMediaTypes.includes(metaMediaType) || rejectIbvBannerOnMultiFormat)) || (mediaTypesConfig.blockUnknown && !metaMediaType)) { reject(BID_MEDIA_TYPE_REJECTION_REASON); } else { diff --git a/modules/billow_rtb25BidAdapter.md b/modules/billow_rtb25BidAdapter.md new file mode 100644 index 00000000000..14f608b7f27 --- /dev/null +++ b/modules/billow_rtb25BidAdapter.md @@ -0,0 +1,155 @@ +# Overview + +``` +Module Name: Billowlink OpenRTB 2.5 Bidder Adapter +Module Type: Bidder Adapter +Maintainer: zepeng.yin@billowlink.com +``` + +**Bidder code:** `billow_rtb25` + +# Description + +The Billowlink adapter connects Prebid.js to Billowlink’s OpenRTB 2.5 HTTP endpoint. It uses Prebid’s [`ortbConverter`](https://github.com/prebid/Prebid.js/tree/master/libraries/ortbConverter) to translate ad units into a standard OpenRTB `BidRequest` and to map the `BidResponse` back into Prebid bid objects. + +**Currency** + +- Billowlink returns prices in **USD**. The adapter always sets `bid.currency` to `USD` and substitutes `${AUCTION_CURRENCY}` with `USD`, regardless of `BidResponse.cur`. + +**Placement mapping** + +- Your server-side placement configuration is keyed off OpenRTB `imp.tagid`. +- The adapter sets `imp.tagid` from `bid.params.placementId` (stringified). This value must match the placement identifier configured on the Billowlink side. + +**Supported media types** + +- Banner (`adm` → `bid.ad`) +- Video (`adm` → `bid.vastXml`; optional `nurl` → `bid.vastUrl`) +- Native (`adm` as OpenRTB Native JSON → `bid.native.ortb`) + OpenRTB 2.5 responses do not include `mtype`; the adapter infers `mediaType` from the **request** impression (`imp.native` / `imp.video` / otherwise banner). + +**OpenRTB macros in markup** + +If `adm` / VAST still contains `${AUCTION_*}` placeholders after your server responds, the adapter replaces them on `bid.ad`, `bid.vastXml`, and `bid.vastUrl` after `fromORTB`: + +| Macro | Value used | +|-------|------------| +| `${AUCTION_ID}` | OpenRTB `BidRequest.id` on the outgoing request (`request.data.id`). | +| `${AUCTION_BID_ID}` | OpenRTB bid `id` → Prebid `seatBidId`. | +| `${AUCTION_IMP_ID}` | OpenRTB `imp.id` from the request → Prebid `requestId`. | +| `${AUCTION_SEAT_ID}` | `seatbid.seat` for the seat entry that contains this bid. | +| `${AUCTION_AD_ID}` | OpenRTB `seatbid[].bid[].adid` returned by backend (matched by bid id). | +| `${AUCTION_PRICE}` | Clearing price: `originalCpm` if set, otherwise `cpm`. | +| `${AUCTION_CURRENCY}` | Always `USD` (Billowlink prices are USD; `BidResponse.cur` is not used). | +| `${AUCTION_MBR}` | Empty string (not derived here). | +| `${AUCTION_LOSS}` | `0` on the **winning** render path. | + +Prefer server-side substitution when possible. + +**Outstream video** + +The adapter does not attach an outstream renderer. For `mediaTypes.video` with `context: 'outstream'`, publishers must supply a renderer (for example `mediaTypes.video.renderer` on the ad unit) or use their standard video / PUC integration, per Prebid documentation. + +**Privacy and first-party data** + +The adapter does not implement custom GDPR/CCPA/COPPA logic. It merges `bidderRequest.ortb2` into the outgoing request (site, user, device, regs, etc.). Configure consent modules and `setConfig` as for any ORTB2-based bidder so that the correct signals are present in `ortb2`. + +**CORS** + +Browser-side calls to the bid endpoint require CORS on the Billowlink server if the origin differs from the API host (e.g. `Access-Control-Allow-Origin`, credentials if needed). + +# Bid Parameters + +| Name | Scope | Type | Description | +|----------------|----------|--------|-------------| +| `placementId` | Required | String or Number | Placement ID on the Billowlink side; sent as OpenRTB `imp.tagid`. | +| `endpoint` | Optional | String | Overrides the default bid URL (e.g. staging or local `https://adx-sg.billowlink.com/api/rtb/adsWeb`). If omitted, the production default below is used. | + +**Default endpoint:** `https://adx-sg.billowlink.com/api/rtb/adsWeb` + +# Example: Banner + +```javascript +var adUnits = [{ + code: 'div-gpt-ad-example', + mediaTypes: { + banner: { sizes: [[300, 250], [728, 90]] } + }, + bids: [{ + bidder: 'billow_rtb25', + params: { + placementId: 'YOUR_PLACEMENT_ID' + // endpoint: 'https://your-staging-host/api/rtb/adsWeb' // optional + } + }] +}]; +``` + +# Example: Video (instream or outstream) + +```javascript +var videoAdUnits = [{ + code: 'div-video-example', + mediaTypes: { + video: { + context: 'instream', // or 'outstream' (renderer required for outstream) + playerSize: [640, 480], + mimes: ['video/mp4'], + minduration: 1, + maxduration: 120, + protocols: [2, 3, 5, 6] + } + }, + bids: [{ + bidder: 'billow_rtb25', + params: { + placementId: 'YOUR_VIDEO_PLACEMENT_ID' + } + }] +}]; +``` + +# Example: Native + +```javascript +var nativeAdUnits = [{ + code: 'div-native-example', + mediaTypes: { + native: { + ortb: { + ver: '1.2', + assets: [ + { id: 1, required: 1, title: { len: 80 } }, + { id: 2, required: 1, img: { type: 3, wmin: 100, hmin: 100 } } + ] + } + } + }, + bids: [{ + bidder: 'billow_rtb25', + params: { + placementId: 'YOUR_NATIVE_PLACEMENT_ID' + } + }] +}]; +``` + +Ensure the bid request includes a valid native ORTB payload (e.g. `nativeOrtbRequest` / `mediaTypes.native.ortb`) so that `imp.native` is populated for the converter. + +# User sync + +`getUserSyncs` currently returns no sync URLs. If cookie or iframe sync is added later, it will be documented here and implemented per Prebid’s user-sync rules. + +# Build + +Include the module in your Prebid bundle, for example: + +```bash +gulp build --modules=billow_rtb25BidAdapter,... +``` + +# Notes for maintainers + +- Responses with no bids should use HTTP 204 or an empty `seatbid` array; the adapter returns `[]` in those cases. +- Request `Content-Type` uses Prebid's default for POST (`text/plain` with JSON body), which avoids triggering CORS preflight. +- Default `ttl` for bids is 30 seconds when the response omits `exp`; `netRevenue` is set to `true` in the converter context. diff --git a/modules/billow_rtb25BidAdapter.ts b/modules/billow_rtb25BidAdapter.ts new file mode 100644 index 00000000000..21a26f4f2df --- /dev/null +++ b/modules/billow_rtb25BidAdapter.ts @@ -0,0 +1,166 @@ +import { deepAccess, replaceMacros } from '../src/utils.js'; +import { BidderSpec, ExtendedResponse, registerBidder } from '../src/adapters/bidderFactory.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'billow_rtb25'; + +interface BillowRtb25BidParams { + placementId: string | number; + endpoint?: string; +} + +declare module '../src/adUnits' { + interface BidderParams { + [BIDDER_CODE]: BillowRtb25BidParams; + } +} + +const DEFAULT_ENDPOINT = 'https://adx-sg.billowlink.com/api/rtb/adsWeb'; +const BILLOW_BID_CURRENCY = 'USD'; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 30, + }, + + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const placementId = deepAccess(bidRequest, 'params.placementId'); + if (placementId) { + imp.tagid = String(placementId); + } + return imp; + }, + + // OpenRTB 2.5 has no mtype; infer mediaType from the request-side imp. + bidResponse(buildBidResponse, bid, context) { + const imp = context && context.imp; + + if (imp && imp.native) { + context.mediaType = NATIVE; + } else if (imp && imp.video) { + context.mediaType = VIDEO; + } else { + context.mediaType = BANNER; + } + + return buildBidResponse(bid, context); + }, +}); + +function findOrtbSeatId(body: any, seatBidId: string): string { + if (!body || !Array.isArray(body.seatbid) || seatBidId == null || seatBidId === '') return ''; + for (const sb of body.seatbid) { + if (!sb || !Array.isArray(sb.bid)) continue; + for (const ortbBid of sb.bid) { + if (ortbBid && ortbBid.id === seatBidId) { + return sb.seat != null && sb.seat !== '' ? String(sb.seat) : ''; + } + } + } + return ''; +} + +function findOrtbAdId(body: any, seatBidId: string): string { + if (!body || !Array.isArray(body.seatbid) || seatBidId == null || seatBidId === '') return ''; + for (const sb of body.seatbid) { + if (!sb || !Array.isArray(sb.bid)) continue; + for (const ortbBid of sb.bid) { + if (ortbBid && ortbBid.id === seatBidId) { + return ortbBid.adid != null ? String(ortbBid.adid) : ''; + } + } + } + return ''; +} + +/** + * Replace unresolved ${AUCTION_*} macros in bid markup so tracking URLs work. + * Prefer server-side substitution when possible. + */ +function applyOpenRtbMacrosToBid(bid: any, body: any, ortbRequest: any): void { + if (!bid) return; + const priceRaw = bid.originalCpm != null ? bid.originalCpm : bid.cpm; + const priceStr = priceRaw != null && !Number.isNaN(Number(priceRaw)) ? String(priceRaw) : ''; + const seatBidId = bid.seatBidId != null ? String(bid.seatBidId) : ''; + const subs: Record = { + AUCTION_ID: ortbRequest && ortbRequest.id != null ? String(ortbRequest.id) : '', + AUCTION_BID_ID: seatBidId, + AUCTION_IMP_ID: bid.requestId != null ? String(bid.requestId) : '', + AUCTION_SEAT_ID: findOrtbSeatId(body, seatBidId), + AUCTION_AD_ID: findOrtbAdId(body, seatBidId), + AUCTION_PRICE: priceStr, + AUCTION_CURRENCY: BILLOW_BID_CURRENCY, + AUCTION_MBR: '', + AUCTION_LOSS: '0', + }; + (['vastXml', 'vastUrl', 'ad'] as const).forEach((key) => { + const val = bid[key]; + if (typeof val === 'string' && val.indexOf('${') !== -1) { + const next = replaceMacros(val, subs); + if (typeof next === 'string') { + bid[key] = next; + } + } + }); +} + +export const spec: BidderSpec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid(bid) { + const placementId = deepAccess(bid, 'params.placementId'); + return !!placementId; + }, + + buildRequests(validBidRequests, bidderRequest) { + const endpointFromBid = deepAccess(validBidRequests, '0.params.endpoint'); + const endpoint = endpointFromBid || DEFAULT_ENDPOINT; + + const ortbRequest = converter.toORTB({ + bidRequests: validBidRequests, + bidderRequest, + }); + + return { + method: 'POST', + url: endpoint, + data: ortbRequest, + }; + }, + + interpretResponse(serverResponse, request) { + const body = serverResponse && serverResponse.body; + if (!body) { + return []; + } + + const seatbid = body.seatbid; + if (!Array.isArray(seatbid) || seatbid.length === 0) { + return []; + } + + const result = converter.fromORTB({ + response: body, + request: request.data, + }) as ExtendedResponse; + + const bids = (result && result.bids) || []; + bids.forEach((b: any) => { + b.currency = BILLOW_BID_CURRENCY; + applyOpenRtbMacrosToBid(b, body, request.data); + }); + return bids; + }, + + getUserSyncs() { + return []; + }, + + alwaysHasCapacity: true, +}; + +registerBidder(spec); diff --git a/modules/conceptxBidAdapter.js b/modules/conceptxBidAdapter.js index eef57e0aaa8..3f5b8c11342 100644 --- a/modules/conceptxBidAdapter.js +++ b/modules/conceptxBidAdapter.js @@ -1,6 +1,20 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapterManager.js').BidderRequest} BidderRequest + */ + +/** + * @typedef {Object} ConceptxBidParams + * @property {string} adunit - Stored request ID used to look up the ad unit configuration on our PBS + * @property {string} [site] - Internal site identifier (e.g. "some_domain.com") used as site.id; + * NOT the publisher domain. The actual domain/page are sourced from ortb2 or refererInfo. + */ + const BIDDER_CODE = 'conceptx'; const ENDPOINT_URL = 'https://cxba-s2s.cncpt.dk/openrtb2/auction'; const GVLID = 1340; @@ -44,7 +58,7 @@ export const spec = { if (bidderRequest.gdprConsent.consentString) { query.push( 'gdpr_consent=' + - encodeURIComponent(bidderRequest.gdprConsent.consentString) + encodeURIComponent(bidderRequest.gdprConsent.consentString) ); } } @@ -54,16 +68,20 @@ export const spec = { url += '?' + query.join('&'); } - // site + // site – params.site is our internal stored-request key, NOT the publisher domain const page = - params.site || (ortb2.site && ortb2.site.page) || ''; + (ortb2.site && ortb2.site.page) || + (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.page) || + ''; const domain = - params.domain || (ortb2.site && ortb2.site.domain) || page; + (ortb2.site && ortb2.site.domain) || + (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.domain) || + ''; const site = { - id: domain || page || adUnitCode, - domain: domain || '', - page: page || '', + id: params.site || domain || adUnitCode, + domain: domain, + page: page, }; // banner sizes from mediaTypes.banner.sizes @@ -213,7 +231,7 @@ export const spec = { referrer = originalReq.site.page; } } - } catch (_) {} + } catch (_) { } for (let i = 0; i < body.seatbid.length; i++) { const seatbid = body.seatbid[i]; diff --git a/modules/connatixBidAdapter.js b/modules/connatixBidAdapter.js index c8364b97daa..1b6c11a385c 100644 --- a/modules/connatixBidAdapter.js +++ b/modules/connatixBidAdapter.js @@ -21,11 +21,11 @@ import { } from '../src/utils.js'; import { - ADPOD, BANNER, VIDEO, } from '../src/mediaTypes.js'; import { getAdUnitElement } from '../src/utils/adUnits.js'; +import { INSTREAM, OUTSTREAM } from '../src/video.js'; const BIDDER_CODE = 'connatix'; @@ -81,7 +81,7 @@ export function validateVideo(mediaTypes) { return true; } - return video.context !== ADPOD; + return video.context === INSTREAM || video.context === OUTSTREAM; } export function _getMinSize(sizes) { diff --git a/modules/consentManagementTcf.ts b/modules/consentManagementTcf.ts index 025c8d93223..a0e780c7c03 100644 --- a/modules/consentManagementTcf.ts +++ b/modules/consentManagementTcf.ts @@ -45,7 +45,7 @@ export type TCFConsentData = { /** * The response from the CMP. */ - vendorData: Record; + vendorData: Record; /** * Additional consent string, if provided by the CMP. * @see https://support.google.com/admanager/answer/9681920?hl=en diff --git a/modules/cortexBidAdapter.md b/modules/cortexBidAdapter.md new file mode 100644 index 00000000000..db33acd3d8d --- /dev/null +++ b/modules/cortexBidAdapter.md @@ -0,0 +1,88 @@ +# Overview + +``` +Module Name: Cortex Bidder Adapter +Module Type: Cortex Bidder Adapter +Maintainer: dev@cortextech.it +``` + +# Description + +Connects to Cortex exchange for bids. +Cortex bid adapter supports Banner, Video (instream and outstream) and Native. + +# Bid Params + +| Name | Scope | Description | Example | Type | +|---|---|---|---|---| +| placementId | optional* | Placement ID from the Cortex platform. Required when `endpointId` is not set. | `'testBanner'` | `string` | +| endpointId | optional* | Endpoint ID from the Cortex platform. Required when `placementId` is not set. | `'testEndpoint'` | `string` | + +\* At least one of `placementId` or `endpointId` must be provided. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'cortex', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'cortex', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'cortex', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/modules/cortexBidAdapter.ts b/modules/cortexBidAdapter.ts new file mode 100644 index 00000000000..8e042eeb52c --- /dev/null +++ b/modules/cortexBidAdapter.ts @@ -0,0 +1,25 @@ +import { type BidderSpec, registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs, type TeqBlazeBidParams } from '../libraries/teqblazeUtils/bidderUtils.ts'; + +declare module '../src/adUnits' { + interface BidderParams { + [BIDDER_CODE]: TeqBlazeBidParams; + } +} + +const BIDDER_CODE = 'cortex'; +const AD_URL = 'https://eu.targetadserver.com/pbjs'; +const SYNC_URL = 'https://sync.targetadserver.com'; + +export const spec: BidderSpec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) +}; + +registerBidder(spec); diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 56fc19ef7be..1b06d8fe51f 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -92,8 +92,6 @@ function imp(buildImp, bidRequest, context) { context: bidRequest.mediaTypes.video.context, playersizes: parseSizes(bidRequest?.mediaTypes?.video?.playerSize, parseSize), plcmt: bidRequest.mediaTypes.video.plcmt, - poddur: bidRequest.mediaTypes.video.adPodDurationSec, - rqddurs: bidRequest.mediaTypes.video.durationRangeSec, }) } @@ -671,7 +669,9 @@ function createOutstreamVideoRenderer(bid) { }; const renderer = Renderer.install({ url: PUBLISHER_TAG_OUTSTREAM_SRC, config: config }); - renderer.setRender(render); + renderer.setRender( + (renderBid, renderDocument) => renderBid.renderer.push(() => render(renderBid, renderDocument)) + ); return renderer; } diff --git a/modules/criteoIdSystem.d.ts b/modules/criteoIdSystem.d.ts new file mode 100644 index 00000000000..a67ff0f5ade --- /dev/null +++ b/modules/criteoIdSystem.d.ts @@ -0,0 +1,17 @@ +export type CriteoIdSystemModuleName = 'criteo'; + +declare module './userId/spec' { + interface UserId { + criteoId: string; + } + + interface ProvidersToId { + criteoId: 'criteoId'; + } + + interface ProviderParams { + criteo: never + } +} + +export {} diff --git a/modules/criteoIdSystem.js b/modules/criteoIdSystem.js index 544e5a9ea31..7b7207e9d6d 100644 --- a/modules/criteoIdSystem.js +++ b/modules/criteoIdSystem.js @@ -17,9 +17,14 @@ import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterM * @typedef {import('../modules/userId/index.js').Submodule} Submodule * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/spec.js').IdProviderSpec} IdProviderSpec + * @typedef {import('./criteoIdSystem.d.ts').CriteoIdSystemModuleName} CriteoIdSystemModuleName */ const gvlid = 91; +/** + * @typedef CriteoIdSystemModuleName + */ const bidderCode = 'criteo'; export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: bidderCode }); @@ -205,11 +210,11 @@ function callCriteoUserSync(submoduleConfig, parsedCriteoData, callback) { ajax(url, callbacks, undefined, { method: 'GET', contentType: 'application/json', withCredentials: true }); } -/** @type {Submodule} */ +/** @type {IdProviderSpec} */ export const criteoIdSubmodule = { /** * used to link submodule with config - * @type {string} + * @type {CriteoIdSystemModuleName} */ name: bidderCode, gvlid: gvlid, diff --git a/modules/currency.ts b/modules/currency.ts index a45c79ef9ec..5517916a6b7 100644 --- a/modules/currency.ts +++ b/modules/currency.ts @@ -28,6 +28,7 @@ export var currencySupportEnabled = false; export var currencyRates = {} as any; let bidderCurrencyDefault = {}; let defaultRates; +let shouldUseDefaults = true; export let responseReady = defer(); @@ -93,11 +94,12 @@ export function setConfig(config: CurrencyConfig) { if (config.rates !== null && typeof config.rates === 'object') { currencyRates.conversions = config.rates; + shouldUseDefaults = false; currencyRatesLoaded = true; needToCallForCurrencyFile = false; // don't call if rates are already specified } - if (config.defaultRates !== null && typeof config.defaultRates === 'object') { + if (shouldUseDefaults && config.defaultRates !== null && typeof config.defaultRates === 'object') { defaultRates = config.defaultRates; // set up the default rates to be used if the rate file doesn't get loaded in time @@ -167,6 +169,7 @@ function loadRates() { logInfo('currencyRates set to ' + JSON.stringify(currencyRates)); conversionCache = {}; currencyRatesLoaded = true; + shouldUseDefaults = false; processBidResponseQueue(); delayedAuctions.resume(); } catch (e) { @@ -231,6 +234,7 @@ export function resetCurrency() { currencySupportEnabled = false; currencyRatesLoaded = false; needToCallForCurrencyFile = true; + shouldUseDefaults = true; currencyRates = {}; bidderCurrencyDefault = {}; responseReady = defer(); diff --git a/modules/datawrkzBidAdapter.js b/modules/datawrkzBidAdapter.js index f09f824486a..da1989fb5eb 100644 --- a/modules/datawrkzBidAdapter.js +++ b/modules/datawrkzBidAdapter.js @@ -27,6 +27,7 @@ const RENDERER_URL = 'https://js.datawrkz.com/prebid/osRenderer.min.js'; const OUTSTREAM_TYPES = ['inline', 'slider_top_left', 'slider_top_right', 'slider_bottom_left', 'slider_bottom_right', 'interstitial_close', 'listicle'] const OUTSTREAM_MIMES = ['video/mp4'] const SUPPORTED_AD_TYPES = [BANNER, NATIVE, VIDEO]; +const SUPPORTED_VIDEO_CONTEXTS = [INSTREAM, OUTSTREAM]; export const spec = { code: BIDDER_CODE, @@ -39,7 +40,7 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function(bid) { - return !!(bid.params && bid.params.site_id && (deepAccess(bid, 'mediaTypes.video.context') !== 'adpod')); + return !!(bid.params && bid.params.site_id && isValidVideoMediaTypeContext(deepAccess(bid, 'mediaTypes.video.context'))); }, /** @@ -96,6 +97,11 @@ export const spec = { }, } +/* Checks whether the video media type context is supported */ +function isValidVideoMediaTypeContext(context) { + return context == null || SUPPORTED_VIDEO_CONTEXTS.some(c => context === c); +} + /* Generate bid request for banner adunit */ function buildBannerRequest(bidRequest, bidderRequest) { const bidFloor = getBidFloor(bidRequest); @@ -241,7 +247,7 @@ function buildVideoRequest(bidRequest, bidderRequest) { deals = bidRequest.params.deals; } - if (context !== 'adpod') { + if (isValidVideoMediaTypeContext(context)) { imp.push({ id: bidRequest.bidId, video: video, diff --git a/modules/debugging/debugging.js b/modules/debugging/debugging.js index 022d901264d..5ae3f790457 100644 --- a/modules/debugging/debugging.js +++ b/modules/debugging/debugging.js @@ -2,10 +2,17 @@ import { makebidInterceptor } from './bidInterceptor.js'; import { makePbsInterceptor } from './pbsInterceptor.js'; import { addHooks, removeHooks } from './legacy.js'; +/** + * @typedef {import('./debuggingModule.d.ts').DebugModuleConfiguration} DebugModuleConfiguration + */ + const interceptorHooks = []; let bidInterceptor; let enabled = false; +/** + * @param {DebugModuleConfiguration} debugConfig Configuration of the debug module + */ function enableDebugging(debugConfig, { fromSession = false, config, hook, logger }) { config.setConfig({ debug: true }); bidInterceptor.updateConfig(debugConfig); diff --git a/modules/debugging/debuggingModule.d.ts b/modules/debugging/debuggingModule.d.ts new file mode 100644 index 00000000000..d92c4217f01 --- /dev/null +++ b/modules/debugging/debuggingModule.d.ts @@ -0,0 +1,80 @@ +import type { BidRequest } from "../../src/adapterManager"; +import type { Bid } from "../../src/bidfactory"; +import type { BidderCode } from "../../src/types/common"; + +export type DebugModuleConfiguration = { + enabled?: boolean; + /** + * Rules are evaluated on each bid in the order they are provided: the first one that has a matching when definition takes the bid out of the normal auction flow and replaces it according to its then definition. + */ + intercept?: InterceptRule[] +} + +export type InterceptRule = { + /** + * Decides which bids should be intercepted by this rule + */ + when: MatchRule; + /** + * Decides the contents of the bids that are intercepted by this rule + */ + then?: ReplaceRule; + options?: RuleOptions; +} + + type MatchRule = + /** + * The match rule can be provided as a function that takes the bid request as its only argument and returns `true` if the bid should be intercepted, `false` otherwise. + */ + | ((bidRequest: BidRequest) => boolean) + /** + * Alternatively, the rule can be expressed as an `object`, and it matches if for each key-value pair: + * - `bidRequest[key] === value`, or + * - `value` is a function and `value(bidRequest[key])` is `true`, or + * - `value` is a regular expression and it matches `bidRequest[key]`. + */ + | { + [K in keyof BidRequest]?: BidRequest[K] | ((value: BidRequest[K]) => boolean) | RegExp + }; + + type ReplaceRule = + /** + * The replace rule can be provided as a function that takes the bid request as its only argument and returns an object with the desired response properties. + * The function can return `null` to indicate that there is no bid. + */ + | ((bidRequest: BidRequest) => Partial | null) + /** + * Alternatively, the rule can be expressed as an `object`, and its key-value pairs will appear in the response as follows: + * - if `value` is a function, then `bidResponse[key]` will be set to `value(bidRequest)`; + * - otherwise, `bidResponse[key]` will be set to `value`. + */ + | { + [K in keyof Bid]?: Bid[K] | ((request: BidRequest) => Bid[K]); + } + /** + * Indicates no bid. + */ + | null; + + type RuleOptions = { + /** + * Delay (in milliseconds) before intercepted bids are injected into the auction. + * Can be used to simulate network latency. + * + * Defaults to zero. + */ + delay?: number + } + +declare module '../../src/config' { + interface Config { + /** + * This module allows to “intercept” bids and replace their contents with arbitrary data for the purposes of testing and development. + * + * Bids intercepted in this way are never seen by bid adapters or their backend SSPs, but they are nonetheless injected into the auction as if they originated from them. + */ + debugging?: DebugModuleConfiguration; + } +} + +export {} diff --git a/modules/encypherRtdProvider.md b/modules/encypherRtdProvider.md new file mode 100644 index 00000000000..1a73e45cd5b --- /dev/null +++ b/modules/encypherRtdProvider.md @@ -0,0 +1,106 @@ +# Overview + +Module Name: Encypher RTD Provider +Module Type: Rtd Provider +Maintainer: engineering@encypher.com + +# Description + +This module injects C2PA content provenance signals into OpenRTB bid requests at `site.ext.data.c2pa`. It enables DSPs to factor verified publisher identity and content integrity into bidding decisions. + +The module runs once per page load through three paths in strict priority: + +1. **Manifest Shortcut (Path A):** If a `` tag or `params.manifestUrl` is present, fetches the manifest directly without calling the signing API. This is an optional optimization for publishers who already expose manifest URLs. + +2. **Cache (Path B):** Serves previously obtained provenance from localStorage (30-day TTL, keyed by canonical URL hash). No network call. + +3. **API Signing and Verification (Path C):** Extracts article text from the DOM and sends it to the Encypher API. The API detects whether the content already contains embedded C2PA provenance markers. If markers are present, it verifies them and returns the existing provenance data (including the original signer tier). If no markers are found, it signs the content fresh and returns a new manifest. The result is cached and injected into the bid request. + +**Key behavior:** Content signed at the CMS or CDN layer carries invisible provenance markers that survive DOM rendering. When the module sends this text to the API, the markers are detected and verified server-side. Publishers who sign at publish time receive their authenticated `signer_tier` (e.g., `connected` or `byok`) in the bid request, which is a stronger signal than the `encypher_free` tier assigned to auto-signed content. + +No external JavaScript is loaded. The module uses only Prebid.js core imports. Every code path, including all error branches, calls `callback()`. A 2-second safety timeout ensures the module never blocks an auction. Path C requires GDPR consent before transmitting page content and validates the API endpoint against an allowlist of permitted hosts. + +Free tier: 1,000 unique content signatures per publisher domain per month. Re-requests for the same content (deduped by content hash) do not count against quota. Verification of already-signed content does not count against quota. Quota exceeded returns gracefully with no provenance data (fail-open). + +# Integration + +```bash +gulp build -modules=rtdModule,encypherRtdProvider +``` + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 300, + dataProviders: [{ + name: 'encypher', + waitForIt: true, + params: { + // All optional. Free tier works with zero config. + apiBase: 'https://api.encypher.com', // override for staging/dev + manifestUrl: 'https://...' // manual manifest URL (skips API call) + } + }] + } +}); +``` + +No configuration is required. The module extracts article text from the page and sends it to the Encypher API, which handles both verification of existing provenance and fresh signing. + +For publishers who prefer to skip the API call entirely, output a meta tag pointing to the manifest URL: + +```html + +``` + +# Data Injected + +The following object is placed at `ortb2Fragments.global.site.ext.data.c2pa`: + +```json +{ + "manifest_url": "https://api.encypher.com/api/v1/public/prebid/manifest/abc123", + "verified": true, + "signer_tier": "connected", + "signed_at": "2026-04-01T10:00:00Z", + "content_hash": "a1b2c3d4e5f6", + "source": "auto", + "extraction_method": "json-ld" +} +``` + +| Field | Type | Description | +|-|-|-| +| `manifest_url` | string | URL to retrieve the C2PA manifest | +| `verified` | boolean | `true` if the content's provenance was successfully verified or signed | +| `signer_tier` | string | Signing identity tier: `local`, `encypher_free`, `connected`, `byok`. Content signed at CMS/CDN level returns the publisher's authenticated tier. | +| `signed_at` | string | ISO 8601 timestamp of signing | +| `content_hash` | string | SHA-256 hash of article text | +| `source` | string | How provenance was obtained: `cms` (Path A), `cache` (Path B), or `auto` (Path C) | +| `extraction_method` | string | DOM extraction method used (Path C): `json-ld`, `article-element`, or `role-main` | +| `action` | string | First C2PA action from manifest (Path A only, e.g., `c2pa.created`) | + +# Signer Tiers + +The `signer_tier` field tells DSPs how the content was authenticated: + +| Tier | Meaning | +|-|-| +| `byok` | Publisher signed with their own key (strongest identity) | +| `connected` | Publisher authenticated with Encypher and signed at publish time | +| `encypher_free` | Content was auto-signed by Encypher at first pageview (no publisher authentication) | +| `local` | Manifest fetched from a local/self-hosted endpoint (Path A) | + +DSPs can use this field for differential bidding: content signed by authenticated publishers carries stronger brand-safety guarantees than content attested by a third party at pageview time. + +# Content Extraction + +The module extracts article text from the DOM in this priority order: + +1. **JSON-LD structured data:** Looks for `application/ld+json` scripts containing schema.org types `Article`, `NewsArticle`, `BlogPosting`, or `Report`. Uses `articleBody` or `text` field. Handles `@graph` arrays. +2. **`
` element:** Uses `textContent` of the first `
` element. +3. **`[role="main"]` element:** Uses `textContent` of the first element with `role="main"`. + +Content shorter than 50 characters is skipped. Content longer than 50,000 characters is truncated. If no usable content is found, the module calls callback without injecting provenance data. + +When extracting from JSON-LD, the module also collects article metadata (author, datePublished, dateModified, section, wordCount, keywords, publisher, language) and includes it in the signing request. This metadata is used for analytics only and is not injected into the bid request. diff --git a/modules/encypherRtdProvider.ts b/modules/encypherRtdProvider.ts new file mode 100644 index 00000000000..64758d10c0d --- /dev/null +++ b/modules/encypherRtdProvider.ts @@ -0,0 +1,434 @@ +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { submodule } from '../src/hook.js'; +import { ajax } from '../src/ajax.js'; +import { deepSetValue, logError, logInfo, logWarn } from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { getCanonicalUrl, hashUrl } from '../libraries/encypherUtils/encypherUtils.ts'; +import type { AllConsentData } from '../src/consentHandler.ts'; +import type { RTDProviderConfig, RtdProviderSpec } from './rtdModule/spec.ts'; +import type { StartAuctionOptions } from '../src/prebid.ts'; + +const REAL_TIME_MODULE = 'realTimeData'; +export const MODULE_NAME = 'encypher'; +const LOG_PREFIX = '[EncypherRTD]: '; +const STORAGE_KEY = 'encypher_provenance_v1'; +const CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days +const DEFAULT_API_BASE = 'https://api.encypher.com'; +const ALLOWED_API_HOSTS = ['api.encypher.com', 'staging-api.encypher.com']; +const MAX_CONTENT_LENGTH = 50000; +const MIN_CONTENT_LENGTH = 50; +const AJAX_TIMEOUT_MS = 2000; + +// --------------------------------------------------------------------------- +// Public interface types +// --------------------------------------------------------------------------- + +export interface EncypherRtdParams { + /** Override API base URL (default: https://api.encypher.com). */ + apiBase?: string; + /** Manual manifest URL; skips the signing API call (Path A). */ + manifestUrl?: string; +} + +declare module './rtdModule/spec.ts' { + interface ProviderConfig { + encypher: { + params?: EncypherRtdParams; + }; + } +} + +export interface C2paPayload { + manifest_url: string; + verified: boolean; + signer_tier: string; + signed_at?: string; + content_hash?: string; + source: 'cms' | 'cache' | 'auto'; + extraction_method?: 'json-ld' | 'article-element' | 'role-main'; + action?: string; +} + +interface ContentExtraction { + text: string; + source: 'json-ld' | 'article-element' | 'role-main'; + metadata: Record | null; +} + +// --------------------------------------------------------------------------- +// Storage +// --------------------------------------------------------------------------- + +export const storage = getStorageManager({ + moduleType: MODULE_TYPE_RTD, + moduleName: MODULE_NAME, +}); + +// --------------------------------------------------------------------------- +// Cache (localStorage via Prebid storageManager for consent enforcement) +// --------------------------------------------------------------------------- + +export function readCache(urlHash: string): Record | null { + if (!storage.localStorageIsEnabled()) return null; + try { + const raw = storage.getDataFromLocalStorage(STORAGE_KEY); + if (!raw) return null; + const store = JSON.parse(raw); + const entry = store[urlHash]; + if (!entry) return null; + if (Date.now() > entry.expires_at) { + delete store[urlHash]; + storage.setDataInLocalStorage(STORAGE_KEY, JSON.stringify(store)); + return null; + } + return entry.payload; + } catch (e) { + logWarn(LOG_PREFIX, 'Cache read error', e); + return null; + } +} + +export function writeCache(urlHash: string, payload: Record): void { + if (!storage.localStorageIsEnabled()) return; + try { + const raw = storage.getDataFromLocalStorage(STORAGE_KEY); + const store = raw ? JSON.parse(raw) : {}; + store[urlHash] = { payload, expires_at: Date.now() + CACHE_TTL_MS }; + storage.setDataInLocalStorage(STORAGE_KEY, JSON.stringify(store)); + } catch (e) { + logWarn(LOG_PREFIX, 'Cache write error', e); + } +} + +// --------------------------------------------------------------------------- +// Content extraction +// --------------------------------------------------------------------------- + +function extractMetadata(item: Record): Record | null { + const meta: Record = {}; + + const author = item.author; + if (author) { + if (typeof author === 'string') { + meta.author = author; + } else if (author.name) { + meta.author = author.name; + } else if (Array.isArray(author) && author[0]) { + meta.author = author[0].name || (typeof author[0] === 'string' ? author[0] : undefined); + } + } + + if (item.datePublished) meta.datePublished = item.datePublished; + if (item.dateModified) meta.dateModified = item.dateModified; + if (item.articleSection) meta.section = item.articleSection; + if (item.wordCount) meta.wordCount = Number(item.wordCount) || undefined; + + if (item.keywords) { + meta.keywords = Array.isArray(item.keywords) + ? item.keywords.join(',') + : String(item.keywords); + } + + const pub = item.publisher; + if (pub && pub.name) meta.publisher = pub.name; + + if (item.inLanguage) meta.language = item.inLanguage; + + return Object.keys(meta).length > 0 ? meta : null; +} + +export function extractContent(): ContentExtraction | null { + // 1. JSON-LD structured data + const scripts = document.querySelectorAll('script[type="application/ld+json"]'); + for (let i = 0; i < scripts.length; i++) { + try { + let data = JSON.parse(scripts[i].textContent || ''); + if (data['@graph'] && Array.isArray(data['@graph'])) { + data = data['@graph']; + } + const items = Array.isArray(data) ? data : [data]; + for (let j = 0; j < items.length; j++) { + const item = items[j]; + const type = item['@type'] || ''; + if (/Article|NewsArticle|BlogPosting|Report/i.test(type)) { + const body = item.articleBody || item.text || ''; + if (body.length >= MIN_CONTENT_LENGTH) { + return { + text: body.slice(0, MAX_CONTENT_LENGTH), + source: 'json-ld', + metadata: extractMetadata(item), + }; + } + } + } + } catch (_) { /* malformed JSON-LD, skip */ } + } + + // 2.
element + const article = document.querySelector('article'); + if (article) { + const text = (article.textContent || '').trim(); + if (text.length >= MIN_CONTENT_LENGTH) { + return { text: text.slice(0, MAX_CONTENT_LENGTH), source: 'article-element', metadata: null }; + } + } + + // 3. [role="main"] + const main = document.querySelector('[role="main"]'); + if (main) { + const text = (main.textContent || '').trim(); + if (text.length >= MIN_CONTENT_LENGTH) { + return { text: text.slice(0, MAX_CONTENT_LENGTH), source: 'role-main', metadata: null }; + } + } + + return null; +} + +// --------------------------------------------------------------------------- +// Path A helpers +// --------------------------------------------------------------------------- + +function buildManifestPayload(manifest: Record, manifestUrl: string): Partial { + return { + manifest_url: manifestUrl, + verified: manifest.status === 'ok', + signer_tier: manifest.signerTier || 'local', + action: (manifest.actions && manifest.actions[0] && manifest.actions[0].action) || undefined, + signed_at: manifest.signedAt || undefined, + source: 'cms', + }; +} + +// --------------------------------------------------------------------------- +// Path C helpers +// --------------------------------------------------------------------------- + +function signContent( + text: string, + apiBase: string, + pageUrl: string, + metadata: Record | null, + cb: (err: any, resp: any) => void +): void { + const payload: Record = { + text, + page_url: pageUrl, + document_title: document.title || undefined, + }; + if (metadata) { + payload.metadata = metadata; + } + const body = JSON.stringify(payload); + + ajax( + apiBase + '/api/v1/public/prebid/sign', + { + success(responseText: string) { + try { + cb(null, JSON.parse(responseText)); + } catch (e) { + cb(e, null); + } + }, + error(error: any) { + cb(error, null); + }, + }, + body, + { + method: 'POST', + } + ); +} + +// --------------------------------------------------------------------------- +// Submodule interface +// --------------------------------------------------------------------------- + +function isAllowedApiUrl(url: string): boolean { + try { + const parsed = new URL(url); + if (parsed.protocol !== 'https:') return false; + return ALLOWED_API_HOSTS.indexOf(parsed.hostname) !== -1; + } catch (_) { + return false; + } +} + +/** + * Check if privacy signals allow data transmission. + * Returns false when any applicable regulation indicates opt-out or restriction. + */ +function hasConsentForDataTransmission(userConsent: AllConsentData | null | undefined): boolean { + if (!userConsent) return true; + + // COPPA: children's data must never be transmitted + if (userConsent.coppa === true) return false; + + // USP/CCPA: position 2 is the opt-out-sale flag; 'Y' means user opted out + if (userConsent.usp && typeof userConsent.usp === 'string') { + if (userConsent.usp[2] === 'Y') return false; + } + + // GDPR: require a consent string when GDPR applies + if (userConsent.gdpr) { + if (userConsent.gdpr.gdprApplies && !userConsent.gdpr.consentString) return false; + } + + return true; +} + +const init = ( + _config: RTDProviderConfig<'encypher'>, + _userConsent: AllConsentData +): boolean => { + return true; +}; + +/** + * Three execution paths in strict priority: + * + * Path A (CMS): or params.manifestUrl + * Path B (Cache): localStorage hit for canonical URL hash + * Path C (Auto-sign): Extract article text from DOM, POST to Encypher API + * + * Every path and every error branch calls callback(). + */ +const getBidRequestData = ( + reqBidsConfigObj: StartAuctionOptions, + callback: () => void, + moduleConfig: RTDProviderConfig<'encypher'>, + userConsent: AllConsentData +): void => { + const params = (moduleConfig && moduleConfig.params) || {}; + const apiBase = params.apiBase || DEFAULT_API_BASE; + const canonicalUrl = getCanonicalUrl(); + const urlHash = hashUrl(canonicalUrl); + + let callbackFired = false; + function done() { + if (callbackFired) return; + callbackFired = true; + callback(); + } + + const timer = setTimeout(() => { + if (!callbackFired) { + logWarn(LOG_PREFIX, 'Timeout reached, continuing without provenance'); + done(); + } + }, AJAX_TIMEOUT_MS); + + function finish() { + clearTimeout(timer); + done(); + } + + // -- Path A: CMS meta tag or manual manifestUrl override ----------------- + const metaTag = document.querySelector('meta[name="c2pa-manifest-url"]') as HTMLMetaElement | null; + const manifestUrl = (metaTag && metaTag.content) || params.manifestUrl || null; + + if (manifestUrl) { + try { + if (new URL(manifestUrl).protocol !== 'https:') { + logWarn(LOG_PREFIX, 'Path A: manifestUrl must use HTTPS, skipping'); + finish(); + return; + } + } catch (_) { + logWarn(LOG_PREFIX, 'Path A: invalid manifestUrl, skipping'); + finish(); + return; + } + logInfo(LOG_PREFIX, 'Path A: fetching manifest from', manifestUrl); + ajax( + manifestUrl, + { + success(responseText: string) { + try { + const manifest = JSON.parse(responseText); + const payload = buildManifestPayload(manifest, manifestUrl); + deepSetValue(reqBidsConfigObj.ortb2Fragments.global, 'site.ext.data.c2pa', payload); + logInfo(LOG_PREFIX, 'Path A: provenance injected'); + } catch (e) { + logError(LOG_PREFIX, 'Path A: manifest parse error', e); + } + finish(); + }, + error(e: any) { + logError(LOG_PREFIX, 'Path A: manifest fetch error', e); + finish(); + }, + }, + null, + { method: 'GET' } + ); + return; + } + + // -- Path B: localStorage cache hit -------------------------------------- + const cached = readCache(urlHash); + if (cached) { + logInfo(LOG_PREFIX, 'Path B: cache hit for', canonicalUrl); + const payload = Object.assign({}, cached, { source: 'cache' }); + deepSetValue(reqBidsConfigObj.ortb2Fragments.global, 'site.ext.data.c2pa', payload); + finish(); + return; + } + + // -- Path C: auto-sign via Encypher API ---------------------------------- + + if (!hasConsentForDataTransmission(userConsent)) { + logInfo(LOG_PREFIX, 'Path C: skipping, no consent for data transmission'); + finish(); + return; + } + + if (!isAllowedApiUrl(apiBase + '/api/v1/public/prebid/sign')) { + logWarn(LOG_PREFIX, 'Path C: apiBase not in allowed hosts, skipping'); + finish(); + return; + } + + const content = extractContent(); + if (!content) { + logInfo(LOG_PREFIX, 'Path C: no extractable content, skipping'); + finish(); + return; + } + + logInfo(LOG_PREFIX, 'Path C: signing content via', content.source); + signContent(content.text, apiBase, canonicalUrl, content.metadata, (err, resp) => { + if (err || !resp || !resp.success) { + logWarn(LOG_PREFIX, 'Path C: sign API error, continuing without provenance', err); + finish(); + return; + } + + const payload: C2paPayload = { + manifest_url: resp.manifest_url, + verified: true, + signer_tier: resp.signer_tier || 'encypher_free', + signed_at: resp.signed_at, + content_hash: resp.content_hash, + extraction_method: content.source, + source: 'auto', + }; + + writeCache(urlHash, payload); + deepSetValue(reqBidsConfigObj.ortb2Fragments.global, 'site.ext.data.c2pa', payload); + logInfo(LOG_PREFIX, 'Path C: provenance signed and injected'); + finish(); + }); +}; + +export const encypherSubmodule: RtdProviderSpec<'encypher'> = { + name: MODULE_NAME as 'encypher', + init, + getBidRequestData, +}; + +submodule(REAL_TIME_MODULE, encypherSubmodule); + +export { getCanonicalUrl, hashUrl }; diff --git a/modules/equativBidAdapter.js b/modules/equativBidAdapter.js index 683b10f711a..a83101add94 100644 --- a/modules/equativBidAdapter.js +++ b/modules/equativBidAdapter.js @@ -200,6 +200,8 @@ export const converter = ortbConverter({ imp.secure = 1; imp.tagid = bidRequest.adUnitCode; + imp.displaymanager ||= 'Prebid.js'; + imp.displaymanagerver ||= '$prebid.version$'; if (!deepAccess(bidRequest, 'ortb2Imp.rwdd') && deepAccess(bidRequest, 'mediaTypes.video.ext.rewarded')) { mergeDeep(imp, { rwdd: bidRequest.mediaTypes.video.ext.rewarded }); diff --git a/modules/fluctBidAdapter.js b/modules/fluctBidAdapter.js index 3ba1356acd6..0281e47c95c 100644 --- a/modules/fluctBidAdapter.js +++ b/modules/fluctBidAdapter.js @@ -10,7 +10,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'fluct'; const END_POINT = 'https://hb.adingo.jp/prebid/'; -const VERSION = '1.3'; +const VERSION = '1.5'; const NET_REVENUE = true; const TTL = 300; const DEFAULT_CURRENCY = 'JPY'; @@ -118,6 +118,20 @@ export const spec = { const data = {}; data.page = page; + + const ortb2Site = bidderRequest.ortb2?.site; + if (ortb2Site) { + data.site = {}; + if (ortb2Site.cat) data.site.cat = ortb2Site.cat; + if (ortb2Site.sectioncat) data.site.sectioncat = ortb2Site.sectioncat; + if (ortb2Site.pagecat) data.site.pagecat = ortb2Site.pagecat; + if (ortb2Site.keywords) data.site.keywords = ortb2Site.keywords; + if (ortb2Site.content) data.site.content = ortb2Site.content; + if (ortb2Site.domain) data.site.domain = ortb2Site.domain; + if (ortb2Site.ref) data.site.ref = ortb2Site.ref; + if (ortb2Site.ext?.data) data.site.ext = { data: ortb2Site.ext.data }; + } + data.adUnitCode = request.adUnitCode; data.bidId = request.bidId; data.user = { @@ -131,6 +145,9 @@ export const spec = { if (impExt) { data.transactionId = impExt.tid; data.gpid = impExt.gpid ?? impExt.data?.adserver?.adslot; + if (impExt.data) { + deepSetValue(data, 'imp.ext.data', impExt.data); + } } if (bidderRequest.gdprConsent) { deepSetValue(data, 'regs.gdpr', { @@ -187,6 +204,22 @@ export const spec = { data.instl = deepAccess(request, 'ortb2Imp.instl') === 1 || request.params.instl === 1 ? 1 : 0; + if (deepAccess(request, 'ortb2Imp.rwdd') === 1) data.rwdd = 1; + + const pos = deepAccess(request, 'mediaTypes.banner.pos') ?? deepAccess(request, 'ortb2Imp.ext.data.pos'); + if (pos != null) data.pos = pos; + + const ortb2Device = bidderRequest.ortb2?.device; + if (ortb2Device) { + data.device = {}; + if (ortb2Device.sua) data.device.sua = ortb2Device.sua; + if (ortb2Device.ua) data.device.ua = ortb2Device.ua; + if (ortb2Device.w) data.device.w = ortb2Device.w; + if (ortb2Device.h) data.device.h = ortb2Device.h; + if (ortb2Device.language) data.device.language = ortb2Device.language; + if (ortb2Device.devicetype) data.device.devicetype = ortb2Device.devicetype; + } + // Set top-level bidfloor to the highest floor across all sizes const highestFloorData = getHighestBidFloor(request); if (highestFloorData) { diff --git a/modules/gemiusIdSystem.ts b/modules/gemiusIdSystem.ts index 2894eeaab9b..6ac8138ea65 100644 --- a/modules/gemiusIdSystem.ts +++ b/modules/gemiusIdSystem.ts @@ -25,6 +25,9 @@ declare module './userId/spec' { interface ProvidersToId { gemiusId: 'gemiusId'; } + interface ProviderParams { + gemiusId: never + } } function getTopAccessibleWindow(): Window { diff --git a/modules/goadserverBidAdapter.md b/modules/goadserverBidAdapter.md new file mode 100644 index 00000000000..36610dd7408 --- /dev/null +++ b/modules/goadserverBidAdapter.md @@ -0,0 +1,163 @@ +# Overview + +``` +Module Name: goadserver Bidder Adapter +Module Type: Bidder Adapter +Maintainer: jan@goadserver.com +``` + +# Description + +Prebid.js adapter for the goadserver platform — a self-hosted, multi-tenant ad serving stack with a built-in OpenRTB 2.5 Prebid Server endpoint. + +One adapter serves every goadserver deployment. The specific ad server to route bids to is passed per-ad-unit via `params.host`, and the publisher's SSP campaign authentication token (generated in the goadserver panel) is passed via `params.token`. Publishers running multiple goadserver instances can mix and match them in a single Prebid.js config by setting different `params.host` values on different ad units. + +Supported media types: `banner`, `video`, `native`. + +# Bid Parameters + +| Name | Scope | Description | Example | Type | +| -------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------- | -------- | +| `host` | required | The goadserver deployment's public domain. The adapter POSTs to `https://{host}/openrtb2/auction`. | `"ads.example.com"` | `string` | +| `token` | required | SSP campaign authentication token from the publisher's goadserver panel. Goes into `site.publisher.id`. | `"a1b2c3d4..."` | `string` | +| `floor` | optional | Per-bid CPM floor (USD). Honored only if the Price Floors module hasn't already set `imp.bidfloor`. | `0.50` | `number` | +| `subid` | optional | Per-impression sub-identifier for stats attribution (page section, article bucket, A/B test group, etc.). Emitted as `imp.ext.goadserver.subid` and logged against the bid in goadserver's reporting. Normalized server-side (stripped of `,\|"'` and capped at 1024 chars). | `"article_page"` | `string` | +| `deals` | optional | Array of private marketplace deal objects attached to this impression. Each entry maps to OpenRTB `imp.pmp.deals[]`: `id` (required), `bidfloor`, `bidfloorcur`, `at` (auction type), `wseat[]` (whitelisted seats), `wadomain[]` (whitelisted advertiser domains). Forwarded verbatim to downstream DSPs; winning bids return with `bid.dealid` which Prebid.js surfaces in `bid.dealId` for GAM line-item targeting. | see below | `Object[]` | +| `outstreamRendererUrl` | optional | Override URL for the outstream video renderer script. Defaults to `https://{host}/prebid-outstream.js`, which every goadserver deployment hosts. Set this if you want to self-host or bundle a custom player script. | `"https://cdn.pub.example.com/my-outstream.js"` | `string` | + +## Deals / Private Marketplace + +```js +bids: [{ + bidder: 'goadserver', + params: { + host: 'ads.example.com', + token: 'your-sspcampaigns-hash', + deals: [ + { id: 'DEAL_XYZ', bidfloor: 2.50, bidfloorcur: 'USD' }, + { id: 'DEAL_ABC', bidfloor: 1.00, at: 1, wseat: ['agency-42'] } + ] + } +}] +``` + +## Outstream Video + +Outstream is supported via the standard `mediaTypes.video.context: 'outstream'` setting. When a video bid is returned for an outstream ad unit, the adapter attaches a Prebid.js `Renderer` that loads the goadserver-hosted outstream player (served at `https://{params.host}/prebid-outstream.js`). The player parses the VAST XML, injects a muted auto-playing `