diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 8eb8b28c570..00000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2.1 -jobs: - noop: - docker: - - image: cimg/base:stable - steps: - - run: echo "CircleCI build skipped - using GitHub Actions. This job can be removed once 9.x is no longer supported." -workflows: - version: 2 - default: - jobs: - - noop diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh index 7e14a2d200d..257b4905952 100644 --- a/.devcontainer/postCreate.sh +++ b/.devcontainer/postCreate.sh @@ -1,5 +1,8 @@ echo "Post Create Starting" +export NVM_DIR="/usr/local/share/nvm" +[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + nvm install nvm use npm install gulp-cli -g diff --git a/.github/actions/install-deb/action.yml b/.github/actions/install-deb/action.yml new file mode 100644 index 00000000000..c33bfb220ba --- /dev/null +++ b/.github/actions/install-deb/action.yml @@ -0,0 +1,35 @@ +name: Install deb +description: Download and install a .deb package +inputs: + url: + description: URL to the .deb file + required: true + name: + description: A local name for the package. Required if using this action multiple times in the same context. + default: package.deb + required: false + +runs: + using: 'composite' + steps: + - name: Restore deb + id: deb-restore + uses: actions/cache/restore@v4 + with: + path: "${{ runner.temp }}/${{ inputs.name }}" + key: ${{ inputs.url }} + - name: Download deb + if: ${{ steps.deb-restore.outputs.cache-hit != 'true' }} + shell: bash + run: | + wget --no-verbose "${{ inputs.url }}" -O "${{ runner.temp }}/${{ inputs.name }}" + - name: Cache deb + if: ${{ steps.deb-restore.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v4 + with: + path: "${{ runner.temp }}/${{ inputs.name }}" + key: ${{ inputs.url }} + - name: Install deb + shell: bash + run: | + sudo apt-get install -y --allow-downgrades "${{ runner.temp }}/${{ inputs.name }}" diff --git a/.github/actions/load/action.yml b/.github/actions/load/action.yml new file mode 100644 index 00000000000..0102608dbd1 --- /dev/null +++ b/.github/actions/load/action.yml @@ -0,0 +1,38 @@ +name: Load working directory +description: Load working directory saved with "actions/save" +inputs: + name: + description: The name used with actions/save + +runs: + using: 'composite' + steps: + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + - uses: actions/github-script@v8 + id: platform + with: + result-encoding: string + script: | + const os = require('os'); + return os.platform(); + - name: 'Clear working directory' + shell: bash + run: | + rm -r "$(pwd)"/* + + - name: Download artifact + uses: actions/download-artifact@v5 + with: + path: '${{ runner.temp }}' + name: '${{ inputs.name }}' + + - name: 'Untar working directory' + shell: bash + run: | + wdir="$(pwd)" + parent="$(dirname "$wdir")" + target="$(basename "$wdir")" + tar ${{ steps.platform.outputs.result == 'win32' && '--force-local' || '' }} -C "$parent" -xf '${{ runner.temp }}/${{ inputs.name }}.tar' "$target" diff --git a/.github/actions/npm-ci/action.yml b/.github/actions/npm-ci/action.yml new file mode 100644 index 00000000000..c23b3f455d6 --- /dev/null +++ b/.github/actions/npm-ci/action.yml @@ -0,0 +1,23 @@ +name: NPM install +description: Run npm install and cache dependencies + +runs: + using: 'composite' + steps: + - name: Restore dependencies + id: restore-modules + uses: actions/cache/restore@v4 + with: + path: "node_modules" + key: node_modules-${{ hashFiles('package-lock.json') }} + - name: Run npm ci + if: ${{ steps.restore-modules.outputs.cache-hit != 'true' }} + shell: bash + run: | + npm ci + - name: Cache dependencies + if: ${{ steps.restore-modules.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v4 + with: + path: "node_modules" + key: node_modules-${{ hashFiles('package-lock.json') }} diff --git a/.github/actions/save/action.yml b/.github/actions/save/action.yml new file mode 100644 index 00000000000..3efca584c7f --- /dev/null +++ b/.github/actions/save/action.yml @@ -0,0 +1,41 @@ +name: Save working directory +description: Save working directory, preserving permissions +inputs: + prefix: + description: Prefix to use for autogenerated names + required: false + name: + description: a name to reference with actions/load + required: false +outputs: + name: + description: a name to reference with actions/load + value: ${{ fromJSON(steps.platform.outputs.result).name }} + +runs: + using: 'composite' + steps: + - uses: actions/github-script@v8 + id: platform + with: + script: | + const os = require('os'); + const crypto = require("crypto"); + const id = crypto.randomBytes(16).toString("hex"); + return { + name: ${{ inputs.name && format('"{0}"', inputs.name) || format('"{0}" + id', inputs.prefix || '') }}, + platform: os.platform(), + } + - name: Tar working directory + shell: bash + run: | + wdir="$(pwd)" + parent="$(dirname "$wdir")" + target="$(basename "$wdir")" + tar ${{ fromJSON(steps.platform.outputs.result).platform == 'win32' && '--force-local' || '' }} -C "$parent" -cf "${{ runner.temp }}/${{ fromJSON(steps.platform.outputs.result).name }}.tar" "$target" + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + path: '${{ runner.temp }}/${{ fromJSON(steps.platform.outputs.result).name }}.tar' + name: ${{ fromJSON(steps.platform.outputs.result).name }} + overwrite: true diff --git a/.github/actions/wait-for-browserstack/action.yml b/.github/actions/wait-for-browserstack/action.yml index 63d24b87f88..3242e29fc50 100644 --- a/.github/actions/wait-for-browserstack/action.yml +++ b/.github/actions/wait-for-browserstack/action.yml @@ -1,18 +1,13 @@ name: Wait for browserstack sessions description: Wait until enough browserstack sessions have become available inputs: - BROWSERSTACK_USER_NAME: - description: "Browserstack user name" - BROWSERSTACK_ACCESS_KEY: - description: "Browserstack access key" - + sessions: + description: Number of sessions needed to continue + default: "6" runs: using: 'composite' steps: - - env: - BROWSERSTACK_USERNAME: ${{ inputs.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ inputs.BROWSERSTACK_ACCESS_KEY }} - shell: bash + - shell: bash run: | while status=$(curl -u "${BROWSERSTACK_USERNAME}:${BROWSERSTACK_ACCESS_KEY}" \ @@ -22,7 +17,7 @@ runs: queued=$(jq '.queued_sessions' <<< $status) max_queued=$(jq '.queued_sessions_max_allowed' <<< $status) spare=$(( ${max_running} + ${max_queued} - ${running} - ${queued} )) - required=6 + required=${{ inputs.sessions }} echo "Browserstack status: ${running} sessions running, ${queued} queued, ${spare} free" (( ${required} > ${spare} )) do diff --git a/.github/codeql/queries/autogen_fpDOMMethod.qll b/.github/codeql/queries/autogen_fpDOMMethod.qll index 388d7fa4fe8..15cce1bbe19 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 = "getChannelData" and weight = 827.19 and type = "AudioBuffer" ) + ( this = "toDataURL" and weight = 27.48 and type = "HTMLCanvasElement" ) or - ( this = "toDataURL" and weight = 27.15 and type = "HTMLCanvasElement" ) + ( this = "getChannelData" and weight = 849.03 and type = "AudioBuffer" ) } float getWeight() { diff --git a/.github/codeql/queries/autogen_fpEventProperty.qll b/.github/codeql/queries/autogen_fpEventProperty.qll index db529c7eae5..7d33f8a1d5f 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 = 149.23 and event = "devicemotion" ) + ( this = "candidate" and weight = 76.95 and event = "icecandidate" ) or - ( this = "beta" and weight = 1075.3 and event = "deviceorientation" ) + ( this = "accelerationIncludingGravity" and weight = 238.43 and event = "devicemotion" ) or - ( this = "gamma" and weight = 395.62 and event = "deviceorientation" ) + ( this = "beta" and weight = 736.03 and event = "deviceorientation" ) or - ( this = "alpha" and weight = 366.53 and event = "deviceorientation" ) + ( this = "gamma" and weight = 279.41 and event = "deviceorientation" ) or - ( this = "candidate" and weight = 69.63 and event = "icecandidate" ) + ( this = "alpha" and weight = 737.51 and event = "deviceorientation" ) or - ( this = "acceleration" and weight = 58.05 and event = "devicemotion" ) + ( this = "acceleration" and weight = 58.12 and event = "devicemotion" ) or - ( this = "rotationRate" and weight = 57.59 and event = "devicemotion" ) + ( this = "rotationRate" and weight = 57.64 and event = "devicemotion" ) or - ( this = "absolute" and weight = 387.12 and event = "deviceorientation" ) + ( this = "absolute" and weight = 344.13 and event = "deviceorientation" ) } float getWeight() { diff --git a/.github/codeql/queries/autogen_fpGlobalConstructor.qll b/.github/codeql/queries/autogen_fpGlobalConstructor.qll index 1c50c5822f0..e30fb7b2972 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 = "SharedWorker" and weight = 78.14 ) + ( this = "SharedWorker" and weight = 78.9 ) or - ( this = "OfflineAudioContext" and weight = 1135.77 ) + ( this = "OfflineAudioContext" and weight = 1110.27 ) or - ( this = "RTCPeerConnection" and weight = 49.44 ) + ( this = "RTCPeerConnection" and weight = 56.31 ) or - ( this = "Gyroscope" and weight = 142.79 ) + ( this = "Gyroscope" and weight = 109.74 ) or - ( this = "AudioWorkletNode" and weight = 17.63 ) + ( this = "AudioWorkletNode" and weight = 138.2 ) } float getWeight() { diff --git a/.github/codeql/queries/autogen_fpGlobalObjectProperty0.qll b/.github/codeql/queries/autogen_fpGlobalObjectProperty0.qll index de883c58c8f..89f92da1290 100644 --- a/.github/codeql/queries/autogen_fpGlobalObjectProperty0.qll +++ b/.github/codeql/queries/autogen_fpGlobalObjectProperty0.qll @@ -7,57 +7,57 @@ class GlobalObjectProperty0 extends string { GlobalObjectProperty0() { - ( this = "availHeight" and weight = 70.68 and global0 = "screen" ) + ( this = "availHeight" and weight = 68.69 and global0 = "screen" ) or - ( this = "availWidth" and weight = 65.56 and global0 = "screen" ) + ( this = "availWidth" and weight = 64.15 and global0 = "screen" ) or - ( this = "colorDepth" and weight = 34.27 and global0 = "screen" ) + ( this = "colorDepth" and weight = 35.15 and global0 = "screen" ) or - ( this = "deviceMemory" and weight = 75.06 and global0 = "navigator" ) + ( this = "availTop" and weight = 1340.55 and global0 = "screen" ) or - ( this = "availTop" and weight = 1240.09 and global0 = "screen" ) + ( this = "mimeTypes" and weight = 15.13 and global0 = "navigator" ) or - ( this = "cookieEnabled" and weight = 15.3 and global0 = "navigator" ) + ( this = "deviceMemory" and weight = 69.83 and global0 = "navigator" ) or - ( this = "pixelDepth" and weight = 37.72 and global0 = "screen" ) + ( this = "getBattery" and weight = 59.15 and global0 = "navigator" ) or - ( this = "availLeft" and weight = 547.54 and global0 = "screen" ) + ( this = "webdriver" and weight = 30.06 and global0 = "navigator" ) or - ( this = "orientation" and weight = 35.82 and global0 = "screen" ) + ( this = "permission" and weight = 26.25 and global0 = "Notification" ) or - ( this = "vendorSub" and weight = 1791.96 and global0 = "navigator" ) + ( this = "storage" and weight = 40.72 and global0 = "navigator" ) or - ( this = "productSub" and weight = 482.29 and global0 = "navigator" ) + ( this = "orientation" and weight = 34.85 and global0 = "screen" ) or - ( this = "webkitTemporaryStorage" and weight = 40.79 and global0 = "navigator" ) + ( this = "pixelDepth" and weight = 45.53 and global0 = "screen" ) or - ( this = "hardwareConcurrency" and weight = 67.85 and global0 = "navigator" ) + ( this = "availLeft" and weight = 574.21 and global0 = "screen" ) or - ( this = "appCodeName" and weight = 143.58 and global0 = "navigator" ) + ( this = "vendorSub" and weight = 1588.52 and global0 = "navigator" ) or - ( this = "onLine" and weight = 19.76 and global0 = "navigator" ) + ( this = "productSub" and weight = 557.44 and global0 = "navigator" ) or - ( this = "webdriver" and weight = 31.25 and global0 = "navigator" ) + ( this = "webkitTemporaryStorage" and weight = 32.71 and global0 = "navigator" ) or - ( this = "keyboard" and weight = 957.44 and global0 = "navigator" ) + ( this = "hardwareConcurrency" and weight = 61.57 and global0 = "navigator" ) or - ( this = "mediaDevices" and weight = 121.74 and global0 = "navigator" ) + ( this = "appCodeName" and weight = 170.17 and global0 = "navigator" ) or - ( this = "storage" and weight = 151.33 and global0 = "navigator" ) + ( this = "onLine" and weight = 19.42 and global0 = "navigator" ) or - ( this = "mediaCapabilities" and weight = 126.07 and global0 = "navigator" ) + ( this = "keyboard" and weight = 5667.18 and global0 = "navigator" ) or - ( this = "permissions" and weight = 66.75 and global0 = "navigator" ) + ( this = "mediaDevices" and weight = 129.67 and global0 = "navigator" ) or - ( this = "permission" and weight = 22.02 and global0 = "Notification" ) + ( this = "mediaCapabilities" and weight = 167.06 and global0 = "navigator" ) or - ( this = "getBattery" and weight = 114.16 and global0 = "navigator" ) + ( this = "permissions" and weight = 81.52 and global0 = "navigator" ) or - ( this = "webkitPersistentStorage" and weight = 150.79 and global0 = "navigator" ) + ( this = "webkitPersistentStorage" and weight = 132.63 and global0 = "navigator" ) or - ( this = "requestMediaKeySystemAccess" and weight = 17.34 and global0 = "navigator" ) + ( this = "requestMediaKeySystemAccess" and weight = 20.97 and global0 = "navigator" ) or - ( this = "getGamepads" and weight = 235.72 and global0 = "navigator" ) + ( this = "getGamepads" and weight = 441.8 and global0 = "navigator" ) } float getWeight() { diff --git a/.github/codeql/queries/autogen_fpGlobalObjectProperty1.qll b/.github/codeql/queries/autogen_fpGlobalObjectProperty1.qll index 4017e4e871d..e0d8248beb6 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 = 301.74 and global0 = "navigator" and global1 = "mediaDevices" ) + ( this = "enumerateDevices" and weight = 380.3 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 181c833165f..b49336e6468 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 = 5043.14 and global0 = "Gyroscope" ) + ( this = "x" and weight = 5667.18 and global0 = "Gyroscope" ) or - ( this = "y" and weight = 5043.14 and global0 = "Gyroscope" ) + ( this = "y" and weight = 5667.18 and global0 = "Gyroscope" ) or - ( this = "z" and weight = 5043.14 and global0 = "Gyroscope" ) + ( this = "z" and weight = 5667.18 and global0 = "Gyroscope" ) } float getWeight() { diff --git a/.github/codeql/queries/autogen_fpGlobalTypeProperty1.qll b/.github/codeql/queries/autogen_fpGlobalTypeProperty1.qll index 31d9d9808f7..a12f1fc92ad 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 = 17.99 and global0 = "Intl" and global1 = "DateTimeFormat" ) + ( this = "resolvedOptions" and weight = 19.12 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 1997bc1687f..27ad11c54c4 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 = 19.42 ) + ( this = "devicePixelRatio" and weight = 19.09 ) or - ( this = "screenX" and weight = 319.5 ) + ( this = "screenX" and weight = 401.78 ) or - ( this = "screenY" and weight = 303.5 ) + ( this = "screenY" and weight = 345.3 ) or - ( this = "outerWidth" and weight = 102.66 ) + ( this = "outerWidth" and weight = 107.67 ) or - ( this = "outerHeight" and weight = 183.94 ) + ( this = "outerHeight" and weight = 190.2 ) or - ( this = "screenLeft" and weight = 315.55 ) + ( this = "screenLeft" and weight = 372.82 ) or - ( this = "screenTop" and weight = 313.8 ) + ( this = "screenTop" and weight = 374.95 ) or - ( this = "indexedDB" and weight = 17.79 ) + ( this = "indexedDB" and weight = 18.61 ) or - ( this = "openDatabase" and weight = 143.97 ) + ( this = "openDatabase" and weight = 159.66 ) } float getWeight() { diff --git a/.github/codeql/queries/autogen_fpRenderingContextProperty.qll b/.github/codeql/queries/autogen_fpRenderingContextProperty.qll index 13574653e50..ddcb191a490 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 = 20.11 and contextType = "webgl" ) + ( this = "getExtension" and weight = 23.15 and contextType = "webgl" ) or - ( this = "getParameter" and weight = 22.92 and contextType = "webgl" ) + ( this = "getParameter" and weight = 27.52 and contextType = "webgl" ) or - ( this = "getImageData" and weight = 40.74 and contextType = "2d" ) + ( this = "getImageData" and weight = 48.33 and contextType = "2d" ) or - ( this = "getParameter" and weight = 41.44 and contextType = "webgl2" ) + ( this = "getParameter" and weight = 81.31 and contextType = "webgl2" ) or - ( this = "getShaderPrecisionFormat" and weight = 108.95 and contextType = "webgl2" ) + ( this = "getShaderPrecisionFormat" and weight = 144.52 and contextType = "webgl2" ) or - ( this = "getExtension" and weight = 44.59 and contextType = "webgl2" ) + ( this = "getExtension" and weight = 82.09 and contextType = "webgl2" ) or - ( this = "getContextAttributes" and weight = 187.09 and contextType = "webgl2" ) + ( this = "getContextAttributes" and weight = 228.41 and contextType = "webgl2" ) or - ( this = "getSupportedExtensions" and weight = 535.91 and contextType = "webgl2" ) + ( this = "getSupportedExtensions" and weight = 882.01 and contextType = "webgl2" ) or - ( this = "measureText" and weight = 45.5 and contextType = "2d" ) + ( this = "measureText" and weight = 47.58 and contextType = "2d" ) or - ( this = "getShaderPrecisionFormat" and weight = 632.69 and contextType = "webgl" ) + ( this = "getShaderPrecisionFormat" and weight = 664.14 and contextType = "webgl" ) or - ( this = "getContextAttributes" and weight = 1404.12 and contextType = "webgl" ) + ( this = "getContextAttributes" and weight = 1178.57 and contextType = "webgl" ) or - ( this = "getSupportedExtensions" and weight = 968.57 and contextType = "webgl" ) + ( this = "getSupportedExtensions" and weight = 1036.87 and contextType = "webgl" ) or - ( this = "readPixels" and weight = 21.25 and contextType = "webgl" ) + ( this = "readPixels" and weight = 25.3 and contextType = "webgl" ) or - ( this = "isPointInPath" and weight = 5043.14 and contextType = "2d" ) + ( this = "isPointInPath" and weight = 4284.36 and contextType = "2d" ) or - ( this = "readPixels" and weight = 68.7 and contextType = "webgl2" ) + ( this = "readPixels" and weight = 69.37 and contextType = "webgl2" ) } float getWeight() { diff --git a/.github/codeql/queries/autogen_fpSensorProperty.qll b/.github/codeql/queries/autogen_fpSensorProperty.qll index 2a50880a6e9..65d017e68bc 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 = 123.75 ) + ( this = "start" and weight = 97.55 ) } float getWeight() { diff --git a/.github/codeql/queries/jsonRequestContentType.ql b/.github/codeql/queries/jsonRequestContentType.ql index b0ec95850ff..dbb8586a60c 100644 --- a/.github/codeql/queries/jsonRequestContentType.ql +++ b/.github/codeql/queries/jsonRequestContentType.ql @@ -12,7 +12,8 @@ from Property prop where prop.getName() = "contentType" and prop.getInit() instanceof StringLiteral and - prop.getInit().(StringLiteral).getStringValue() = "application/json" + prop.getInit().(StringLiteral).getStringValue() = "application/json" and + prop.getFile().getBaseName().matches("%BidAdapter.%") select prop, "application/json request type triggers preflight requests and may increase bidder timeouts" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 80e3ea0be72..007ba6d26b4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -28,7 +28,6 @@ updates: target-branch: "master" schedule: interval: "daily" - security-updates-only: true open-pull-requests-limit: 0 groups: all-security: diff --git a/.github/workflows/PR-assignment.yml b/.github/workflows/PR-assignment.yml new file mode 100644 index 00000000000..a2b405396bb --- /dev/null +++ b/.github/workflows/PR-assignment.yml @@ -0,0 +1,60 @@ +name: Assign PR reviewers +on: + pull_request_target: + types: [opened, synchronize, reopened] + +jobs: + assign_reviewers: + name: Assign reviewers + runs-on: ubuntu-latest + + steps: + - name: Generate app token + id: token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.PR_BOT_ID }} + private-key: ${{ secrets.PR_BOT_PEM }} + + - name: Checkout + uses: actions/checkout@v6 + with: + ref: refs/pull/${{ github.event.pull_request.number }}/head + + - name: Install dependencies + uses: ./.github/actions/npm-ci + - name: Build + run: | + npx gulp build + - name: Install s3 client + run: | + npm install @aws-sdk/client-s3 + - name: Get PR properties + id: get-props + uses: actions/github-script@v8 + env: + AWS_ACCESS_KEY_ID: ${{ vars.PR_BOT_AWS_AK }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.PR_BOT_AWS_SAK }} + with: + github-token: ${{ steps.token.outputs.token }} + script: | + const getProps = require('./.github/workflows/scripts/getPRProperties.js') + const props = await getProps({ + github, + context, + prNo: ${{ github.event.pull_request.number }}, + reviewerTeam: '${{ vars.REVIEWER_TEAM }}', + engTeam: '${{ vars.ENG_TEAM }}', + authReviewTeam: '${{ vars.AUTH_REVIEWER_TEAM }}' + }); + console.log('PR properties:', JSON.stringify(props, null, 2)); + return props; + - name: Assign reviewers + if: ${{ !fromJSON(steps.get-props.outputs.result).review.ok }} + uses: actions/github-script@v8 + 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)); diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml new file mode 100644 index 00000000000..5e2b6e2541c --- /dev/null +++ b/.github/workflows/browser-tests.yml @@ -0,0 +1,133 @@ +name: Run unit tests on all browsers +on: + workflow_call: + outputs: + coverage: + description: Artifact name for coverage results + value: ${{ jobs.unit-tests.outputs.coverage }} + secrets: + BROWSERSTACK_USER_NAME: + description: "Browserstack user name" + BROWSERSTACK_ACCESS_KEY: + description: "Browserstack access key" +jobs: + build: + uses: ./.github/workflows/build.yml + with: + build-cmd: npx gulp build + + setup: + needs: build + name: "Define testing strategy" + runs-on: ubuntu-latest + outputs: + browsers: ${{ toJSON(fromJSON(steps.define.outputs.result).browsers) }} + latestBrowsers: ${{ toJSON(fromJSON(steps.define.outputs.result).latestBrowsers) }} + bstack-key: ${{ steps.bstack-save.outputs.name }} + bstack-sessions: ${{ fromJSON(steps.define.outputs.result).bsBrowsers }} + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Restore working directory + uses: ./.github/actions/load + with: + name: ${{ needs.build.outputs.built-key }} + - name: "Define testing strategy" + uses: actions/github-script@v8 + id: define + with: + script: | + const fs = require('node:fs/promises'); + const browsers = Object.entries( + require('./.github/workflows/browser_testing.json') + ).flatMap(([name, browser]) => { + browser = Object.assign({name, version: 'latest'}, browser); + const browsers = [browser]; + const versions = browser.versions; + if (versions) { + delete browser.versions; + browsers.push(...Object.entries(versions).map(([version, def]) => Object.assign({}, browser, {version, ...def}))) + } + return browsers; + }) + const bstackBrowsers = Object.fromEntries( + // exclude versions of browsers that we can test on GH actions + Object.entries(require('./browsers.json')) + .filter(([name, def]) => browsers.find(({bsName, version}) => bsName === def.browser && version === def.browser_version) == null) + ) + const updatedBrowsersJson = JSON.stringify(bstackBrowsers, null, 2); + console.log("Using browsers.json:", updatedBrowsersJson); + console.log("Browsers to be tested directly on runners:", JSON.stringify(browsers, null, 2)) + await fs.writeFile('./browsers.json', updatedBrowsersJson); + return { + bsBrowsers: Object.keys(bstackBrowsers).length, + browsers, + latestBrowsers: browsers.filter(browser => browser.version === 'latest') + } + - name: "Save working directory" + id: bstack-save + if: ${{ fromJSON(steps.define.outputs.result).bsBrowsers > 0 }} + uses: ./.github/actions/save + with: + prefix: browserstack- + + test-build-logic: + needs: build + name: "Test build logic" + uses: + ./.github/workflows/run-tests.yml + with: + built-key: ${{ needs.build.outputs.built-key }} + test-cmd: gulp test-build-logic + + e2e-tests: + needs: [setup, build] + name: "E2E (browser: ${{ matrix.browser.wdioName }})" + strategy: + fail-fast: false + matrix: + browser: ${{ fromJSON(needs.setup.outputs.browsers) }} + uses: + ./.github/workflows/run-tests.yml + with: + browser: ${{ matrix.browser.wdioName }} + built-key: ${{ needs.build.outputs.built-key }} + test-cmd: npx gulp e2e-test-nobuild --local + chunks: 1 + runs-on: ${{ matrix.browser.runsOn || 'ubuntu-latest' }} + install-safari: ${{ matrix.browser.runsOn == 'macos-latest' }} + run-npm-install: ${{ matrix.browser.runsOn == 'windows-latest' }} + browserstack: false + + unit-tests: + needs: [setup, build] + name: "Unit (browser: ${{ matrix.browser.name }} ${{ matrix.browser.version }})" + strategy: + fail-fast: false + matrix: + browser: ${{ fromJSON(needs.setup.outputs.browsers) }} + uses: + ./.github/workflows/run-tests.yml + with: + install-deb: ${{ matrix.browser.deb }} + install-chrome: ${{ matrix.browser.chrome }} + built-key: ${{ needs.build.outputs.built-key }} + test-cmd: npx gulp test-only-nobuild --browsers ${{ matrix.browser.name }} ${{ matrix.browser.coverage && '--coverage' || '--no-coverage' }} + chunks: 8 + runs-on: ${{ matrix.browser.runsOn || 'ubuntu-latest' }} + + browserstack-tests: + needs: setup + if: ${{ needs.setup.outputs.bstack-key }} + name: "Browserstack tests" + uses: + ./.github/workflows/run-tests.yml + with: + built-key: ${{ needs.setup.outputs.bstack-key }} + test-cmd: npx gulp test-only-nobuild --browserstack --no-coverage + chunks: 8 + browserstack: true + browserstack-sessions: ${{ fromJSON(needs.setup.outputs.bstack-sessions) }} + secrets: + BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} diff --git a/.github/workflows/browser_testing.json b/.github/workflows/browser_testing.json new file mode 100644 index 00000000000..46b0894e071 --- /dev/null +++ b/.github/workflows/browser_testing.json @@ -0,0 +1,28 @@ +{ + "ChromeHeadless": { + "bsName": "chrome", + "wdioName": "chrome", + "coverage": true, + "versions": { + "113.0": { + "coverage": false, + "chrome": "113.0.5672.0", + "name": "ChromeNoSandbox" + } + } + }, + "EdgeHeadless": { + "bsName": "edge", + "wdioName": "msedge", + "runsOn": "windows-latest" + }, + "SafariNative": { + "wdioName": "safari technology preview", + "bsName": "safari", + "runsOn": "macos-latest" + }, + "FirefoxHeadless": { + "wdioName": "firefox", + "bsName": "firefox" + } +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000000..64c3096274a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,43 @@ +name: Run unit tests +on: + workflow_call: + inputs: + source-key: + description: Artifact name for source directory + type: string + required: false + default: source + build-cmd: + description: Build command + required: false + type: string + outputs: + built-key: + description: Artifact name for built directory + value: ${{ jobs.build.outputs.built-key }} + +jobs: + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + built-key: ${{ inputs.build-cmd && steps.save.outputs.name || inputs.source-key }} + steps: + - name: Checkout + if: ${{ inputs.build-cmd }} + uses: actions/checkout@v6 + - name: Restore source + if: ${{ inputs.build-cmd }} + uses: ./.github/actions/load + with: + name: ${{ inputs.source-key }} + - name: Build + if: ${{ inputs.build-cmd }} + run: ${{ inputs.build-cmd }} + - name: 'Save working directory' + id: save + if: ${{ inputs.build-cmd }} + uses: ./.github/actions/save + with: + prefix: 'build-' diff --git a/.github/workflows/code-path-changes.yml b/.github/workflows/code-path-changes.yml index 8d327a7e2b1..f543394f479 100644 --- a/.github/workflows/code-path-changes.yml +++ b/.github/workflows/code-path-changes.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f86cd38a43c..691ca30b583 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/comment.yml b/.github/workflows/comment.yml new file mode 100644 index 00000000000..0795250d168 --- /dev/null +++ b/.github/workflows/comment.yml @@ -0,0 +1,69 @@ +name: Post a comment +on: + workflow_run: + workflows: + - Check for Duplicated Code + - Check for linter warnings / exceptions + types: + - completed + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + comment: + runs-on: ubuntu-latest + steps: + - name: 'Download artifact' + id: download + uses: actions/github-script@v8 + with: + result-encoding: string + script: | + let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "comment" + })[0]; + if (matchArtifact == null) { + return "false" + } + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + const fs = require('fs'); + const path = require('path'); + const temp = '${{ runner.temp }}/artifacts'; + if (!fs.existsSync(temp)){ + fs.mkdirSync(temp); + } + fs.writeFileSync(path.join(temp, 'comment.zip'), Buffer.from(download.data)); + return "true"; + + - name: 'Unzip artifact' + if: ${{ steps.download.outputs.result == 'true' }} + run: unzip "${{ runner.temp }}/artifacts/comment.zip" -d "${{ runner.temp }}/artifacts" + + - name: 'Comment on PR' + if: ${{ steps.download.outputs.result == 'true' }} + uses: actions/github-script@v8 + with: + script: | + const fs = require('fs'); + const path = require('path'); + const temp = '${{ runner.temp }}/artifacts'; + const {issue_number, body} = JSON.parse(fs.readFileSync(path.join(temp, 'comment.json'))); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number, + body + }); diff --git a/.github/workflows/jscpd.yml b/.github/workflows/jscpd.yml index 39e54bebcf0..c3021b2ced7 100644 --- a/.github/workflows/jscpd.yml +++ b/.github/workflows/jscpd.yml @@ -1,29 +1,30 @@ name: Check for Duplicated Code on: - pull_request_target: + pull_request: branches: - master +permissions: + contents: read + jobs: check-duplication: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: - fetch-depth: 0 # Fetch all history for all branches - ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: '20' - - name: Install dependencies - run: | - npm install -g jscpd diff-so-fancy + - name: Install jscpd + run: npm install -g jscpd - name: Create jscpd config file run: | @@ -35,26 +36,27 @@ jobs: ], "output": "./", "pattern": "**/*.js", - "ignore": "**/*spec.js" + "ignore": ["**/*spec.js"] }' > .jscpd.json - name: Run jscpd on entire codebase run: jscpd - - name: Fetch base and target branches + - name: Fetch base branch for comparison run: | - git fetch origin +refs/heads/${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }} - git fetch origin +refs/pull/${{ github.event.pull_request.number }}/merge:refs/remotes/pull/${{ github.event.pull_request.number }}/merge + git fetch origin refs/heads/${{ github.base_ref }} - - name: Get the diff - run: git diff --name-only origin/${{ github.event.pull_request.base.ref }}...refs/remotes/pull/${{ github.event.pull_request.number }}/merge > changed_files.txt + - name: Get changed files + run: | + git diff --name-only FETCH_HEAD...HEAD > changed_files.txt + cat changed_files.txt - name: List generated files (debug) run: ls -l - name: Upload unfiltered jscpd report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: unfiltered-jscpd-report path: ./jscpd-report.json @@ -87,12 +89,12 @@ jobs: - name: Upload filtered jscpd report if: env.filtered_report_exists == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: filtered-jscpd-report path: ./filtered-jscpd-report.json - - name: Post GitHub comment + - name: Generate PR comment if: env.filtered_report_exists == 'true' uses: actions/github-script@v8 with: @@ -101,7 +103,7 @@ jobs: const filteredReport = JSON.parse(fs.readFileSync('filtered-jscpd-report.json', 'utf8')); let comment = "Whoa there, partner! 🌵🤠 We wrangled some duplicated code in your PR:\n\n"; function link(dup) { - return `https://github.com/${{ github.event.repository.full_name }}/blob/${{ github.event.pull_request.head.sha }}/${dup.name}#L${dup.start + 1}-L${dup.end - 1}` + return `https://github.com/${{ github.repository }}/blob/${{ github.event.pull_request.head.sha }}/${dup.name}#L${dup.start + 1}-L${dup.end - 1}` } filteredReport.forEach(duplication => { const firstFile = duplication.firstFile; @@ -110,12 +112,17 @@ jobs: comment += `- [\`${firstFile.name}\`](${link(firstFile)}) has ${lines} duplicated lines with [\`${secondFile.name}\`](${link(secondFile)})\n`; }); comment += "\nReducing code duplication by importing common functions from a library not only makes our code cleaner but also easier to maintain. Please move the common code from both files into a library and import it in each. We hate that we have to mention this, however, commits designed to hide from this utility by renaming variables or reordering an object are poor conduct. We will not look upon them kindly! Keep up the great work! 🚀"; - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, + fs.writeFileSync('${{ runner.temp }}/comment.json', JSON.stringify({ issue_number: context.issue.number, body: comment - }); + })) + + - name: Upload comment data + if: env.filtered_report_exists == 'true' + uses: actions/upload-artifact@v4 + with: + name: comment + path: ${{ runner.temp }}/comment.json - name: Fail if duplications are found if: env.filtered_report_exists == 'true' diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 30d327d3495..39fdcc4067b 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,10 +1,13 @@ name: Check for linter warnings / exceptions on: - pull_request_target: + pull_request: branches: - master +permissions: + contents: read + jobs: check-linter: runs-on: ubuntu-latest @@ -16,7 +19,7 @@ jobs: node-version: '20' - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 ref: ${{ github.event.pull_request.base.sha }} @@ -46,7 +49,9 @@ jobs: - name: Compare them and post comment if necessary uses: actions/github-script@v8 + id: comment with: + result-encoding: string script: | const fs = require('fs'); const path = require('path'); @@ -101,10 +106,18 @@ jobs: const comment = mkComment(mkDiff(base, pr)); if (comment) { - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, + fs.writeFileSync("${{ runner.temp }}/comment.json", JSON.stringify({ issue_number: context.issue.number, body: comment - }); + })) + return "true"; + } else { + return "false"; } + + - name: Upload comment data + if: ${{ steps.comment.outputs.result == 'true' }} + uses: actions/upload-artifact@v4 + with: + name: comment + path: ${{ runner.temp }}/comment.json diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index a14e12664b6..53d458a3948 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -5,6 +5,7 @@ on: # branches to consider in the event; optional, defaults to all branches: - master + - '*.x-legacy' permissions: contents: read diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 00000000000..1cc6808f29f --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,237 @@ +name: Run unit tests +on: + workflow_call: + inputs: + browser: + description: values to set as the BROWSER env variable + required: false + type: string + chunks: + description: Number of chunks to split tests into + required: false + type: number + default: 1 + build-cmd: + description: Build command, run once + required: false + type: string + built-key: + description: Artifact name for built source + required: false + type: string + test-cmd: + description: Test command, run once per chunk + required: true + type: string + browserstack: + description: If true, set up browserstack environment and adjust concurrency + required: false + type: boolean + default: false + browserstack-sessions: + description: Number of browserstack sessions needed to run tests + required: false + type: number + default: 6 + timeout: + description: Timeout on test run + required: false + type: number + default: 10 + runs-on: + description: Runner image + required: false + default: ubuntu-latest + type: string + install-safari: + description: Install Safari + type: boolean + required: false + default: false + install-chrome: + description: Chrome version to install via @puppeteer/browsers + type: string + required: false + run-npm-install: + description: Run npm install before tests + type: boolean + required: false + default: false + install-deb: + description: URL to deb to install before tests + type: string + required: false + outputs: + coverage: + description: Artifact name for coverage results + value: ${{ jobs.collect-coverage.outputs.coverage }} + secrets: + BROWSERSTACK_USER_NAME: + description: "Browserstack user name" + BROWSERSTACK_ACCESS_KEY: + description: "Browserstack access key" + +jobs: + checkout: + name: "Define chunks" + runs-on: ubuntu-latest + outputs: + chunks: ${{ steps.chunks.outputs.chunks }} + id: ${{ steps.chunks.outputs.id }} + steps: + - name: Define chunks + id: chunks + run: | + echo 'chunks=[ '$(seq --separator=, 1 1 ${{ inputs.chunks }})' ]' >> out; + echo 'id='"$(uuidgen)" >> out; + cat out >> "$GITHUB_OUTPUT"; + + build: + uses: ./.github/workflows/build.yml + with: + build-cmd: ${{ !inputs.built-key && inputs.build-cmd || '' }} + source-key: ${{ inputs.built-key || 'source' }} + + run-tests: + needs: [checkout, build] + strategy: + fail-fast: false + max-parallel: ${{ inputs.browserstack && 1 || inputs.chunks }} + matrix: + chunk-no: ${{ fromJSON(needs.checkout.outputs.chunks) }} + + name: Test${{ inputs.chunks > 1 && format(' chunk {0}', matrix.chunk-no) || '' }} + env: + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USER_NAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + TEST_CHUNKS: ${{ inputs.chunks }} + TEST_CHUNK: ${{ matrix.chunk-no }} + BROWSER: ${{ inputs.browser }} + outputs: + coverage: ${{ steps.coverage.outputs.coverage }} + concurrency: + # The following generates 'browserstack-' when inputs.browserstack is true, and a hopefully unique ID otherwise + # Ideally we'd like to serialize browserstack access across all workflows, but github's max queue length is only 1 + # (cfr. https://github.com/orgs/community/discussions/12835) + # so we add the run_id to serialize only within one push / pull request (which has the effect of queueing e2e and unit tests) + group: ${{ inputs.browserstack && format('browserstack-{0}', github.run_id) || format('{0}-{1}', needs.checkout.outputs.id, matrix.chunk-no) }} + cancel-in-progress: false + + runs-on: ${{ inputs.runs-on }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Restore source + uses: ./.github/actions/load + with: + name: ${{ needs.build.outputs.built-key }} + + - name: Install safari + if: ${{ inputs.install-safari }} + run: | + brew install --cask safari-technology-preview + defaults write com.apple.Safari IncludeDevelopMenu YES + defaults write com.apple.Safari AllowRemoteAutomation 1 + sudo safaridriver --enable + sudo "/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver" --enable + + - name: Install Chrome + if: ${{ inputs.install-chrome }} + shell: bash + run: | + out=($(npx @puppeteer/browsers install chrome@${{ inputs.install-chrome }})) + echo 'CHROME_BIN='"${out[1]}" >> env; + cat env + cat env >> "$GITHUB_ENV" + + - name: Install deb + if: ${{ inputs.install-deb }} + uses: ./.github/actions/install-deb + with: + url: ${{ inputs.install-deb }} + + - name: Run npm install + if: ${{ inputs.run-npm-install }} + run: | + npm install + + - name: 'BrowserStack Env Setup' + if: ${{ inputs.browserstack }} + uses: 'browserstack/github-actions/setup-env@master' + with: + username: ${{ secrets.BROWSERSTACK_USER_NAME}} + access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + build-name: Run ${{github.run_id}}, attempt ${{ github.run_attempt }}, chunk ${{ matrix.chunk-no }}, ref ${{ github.event_name == 'pull_request_target' && format('PR {0}', github.event.pull_request.number) || github.ref }}, ${{ inputs.test-cmd }} + + - name: 'BrowserStackLocal Setup' + if: ${{ inputs.browserstack }} + uses: 'browserstack/github-actions/setup-local@master' + with: + local-testing: start + local-identifier: random + + - name: 'Wait for browserstack' + if: ${{ inputs.browserstack }} + uses: ./.github/actions/wait-for-browserstack + with: + sessions: ${{ inputs.browserstack-sessions }} + + - name: Run tests + uses: nick-fields/retry@v3 + with: + timeout_minutes: ${{ inputs.timeout }} + max_attempts: 3 + command: ${{ inputs.test-cmd }} + shell: bash + + - name: 'BrowserStackLocal Stop' + if: ${{ inputs.browserstack }} + uses: 'browserstack/github-actions/setup-local@master' + with: + local-testing: stop + + - name: 'Check for coverage' + id: 'coverage' + shell: bash + run: | + if [ -d "./build/coverage" ]; then + echo 'coverage=true' >> "$GITHUB_OUTPUT"; + fi + + - name: 'Save coverage result' + if: ${{ steps.coverage.outputs.coverage }} + uses: actions/upload-artifact@v6 + with: + name: coverage-partial-${{inputs.test-cmd}}-${{ matrix.chunk-no }} + path: ./build/coverage + overwrite: true + + collect-coverage: + if: ${{ needs.run-tests.outputs.coverage }} + needs: [build, run-tests] + name: 'Collect coverage results' + runs-on: ubuntu-latest + outputs: + coverage: coverage-complete-${{ inputs.test-cmd }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Restore source + uses: ./.github/actions/load + with: + name: ${{ needs.build.outputs.built-key }} + + - name: Download coverage results + uses: actions/download-artifact@v7 + with: + path: ./build/coverage + pattern: coverage-partial-${{ inputs.test-cmd }}-* + merge-multiple: true + + - name: 'Save working directory' + uses: ./.github/actions/save + with: + name: coverage-complete-${{ inputs.test-cmd }} + diff --git a/.github/workflows/scripts/assignReviewers.js b/.github/workflows/scripts/assignReviewers.js new file mode 100644 index 00000000000..8bbe50f6104 --- /dev/null +++ b/.github/workflows/scripts/assignReviewers.js @@ -0,0 +1,39 @@ +const ghRequester = require('./ghRequest.js'); + +function pickFrom(candidates, exclude, no) { + exclude = exclude.slice(); + const winners = []; + while (winners.length < no) { + const candidate = candidates[Math.floor(Math.random() * candidates.length)]; + if (!exclude.includes(candidate)) { + winners.push(candidate); + exclude.push(candidate); + } + } + return winners; +} + +async function assignReviewers({github, context, prData}) { + const allReviewers = prData.review.reviewers.map(rv => rv.login); + const requestedReviewers = prData.review.requestedReviewers; + const missingPrebidEng = prData.review.requires.prebidEngineers - prData.review.prebidEngineers; + const missingPrebidReviewers = prData.review.requires.prebidReviewers - prData.review.prebidReviewers - (missingPrebidEng > 0 ? missingPrebidEng : 0); + + if (missingPrebidEng > 0) { + requestedReviewers.push(...pickFrom(prData.prebidEngineers, [...allReviewers, prData.author.login], missingPrebidEng)) + } + if (missingPrebidReviewers > 0) { + requestedReviewers.push(...pickFrom(prData.prebidReviewers, [...allReviewers, prData.author.login], missingPrebidReviewers)) + } + + const request = ghRequester(github); + await request('POST /repos/{owner}/{repo}/pulls/{prNo}/requested_reviewers', { + owner: context.repo.owner, + repo: context.repo.repo, + prNo: prData.pr, + reviewers: requestedReviewers + }) + return requestedReviewers; +} + +module.exports = assignReviewers; diff --git a/.github/workflows/scripts/getPRProperties.js b/.github/workflows/scripts/getPRProperties.js new file mode 100644 index 00000000000..f663e976ce5 --- /dev/null +++ b/.github/workflows/scripts/getPRProperties.js @@ -0,0 +1,163 @@ +const ghRequester = require('./ghRequest.js'); +const AWS = require("@aws-sdk/client-s3"); + +const MODULE_PATTERNS = [ + /^modules\/([^\/]+)BidAdapter(\.(\w+)|\/)/, + /^modules\/([^\/]+)AnalyticsAdapter(\.(\w+)|\/)/, + /^modules\/([^\/]+)RtdProvider(\.(\w+)|\/)/, + /^modules\/([^\/]+)IdSystem(\.(\w+)|\/)/ +] + +const EXCLUDE_PATTERNS = [ + /^test\//, + /^integrationExamples\// +] +const LIBRARY_PATTERN = /^libraries\/([^\/]+)\//; + +function extractVendor(chunkName) { + for (const pat of MODULE_PATTERNS) { + const match = pat.exec(`modules/${chunkName}`); + if (match != null) { + return match[1]; + } + } + return chunkName; +} + +const getLibraryRefs = (() => { + const deps = require('../../../build/dist/dependencies.json'); + const refs = {}; + return function (libraryName) { + if (!refs.hasOwnProperty(libraryName)) { + refs[libraryName] = new Set(); + Object.entries(deps) + .filter(([name, deps]) => deps.includes(`${libraryName}.js`)) + .forEach(([name]) => refs[libraryName].add(extractVendor(name))) + } + return refs[libraryName]; + } +})(); + +function isCoreFile(path) { + if (EXCLUDE_PATTERNS.find(pat => pat.test(path))) { + return false; + } + if (MODULE_PATTERNS.find(pat => pat.test(path)) ) { + return false; + } + const lib = LIBRARY_PATTERN.exec(path); + if (lib != null) { + // a library is "core" if it's used by more than one vendor + return getLibraryRefs(lib[1]).size > 1; + } + return true; +} + +async function isPrebidMember(ghHandle) { + const client = new AWS.S3({region: 'us-east-2'}); + const res = await client.getObject({ + Bucket: 'repo-dashboard-files-891377123989', + Key: 'memberMapping.json' + }); + const members = JSON.parse(await res.Body.transformToString()); + return members.includes(ghHandle); +} + + +async function getPRProperties({github, context, prNo, reviewerTeam, engTeam, authReviewTeam}) { + const request = ghRequester(github); + let [files, pr, prReviews, prebidReviewers, prebidEngineers, authorizedReviewers] = await Promise.all([ + request('GET /repos/{owner}/{repo}/pulls/{prNo}/files', { + owner: context.repo.owner, + repo: context.repo.repo, + prNo, + }), + request('GET /repos/{owner}/{repo}/pulls/{prNo}', { + owner: context.repo.owner, + repo: context.repo.repo, + prNo, + }), + request('GET /repos/{owner}/{repo}/pulls/{prNo}/reviews', { + owner: context.repo.owner, + repo: context.repo.repo, + prNo, + }), + ...[reviewerTeam, engTeam, authReviewTeam].map(team => request('GET /orgs/{org}/teams/{team}/members', { + org: context.repo.owner, + team, + })) + ]); + prebidReviewers = prebidReviewers.data.map(datum => datum.login); + prebidEngineers = prebidEngineers.data.map(datum=> datum.login); + authorizedReviewers = authorizedReviewers.data.map(datum=> datum.login); + let isCoreChange = false; + files = files.data.map(datum => datum.filename).map(file => { + const core = isCoreFile(file); + if (core) isCoreChange = true; + return { + file, + core + } + }); + const review = { + prebidEngineers: 0, + prebidReviewers: 0, + reviewers: [], + requestedReviewers: [] + }; + const author = pr.data.user.login; + const allReviewers = new Set(); + pr.data.requested_reviewers + .forEach(rv => { + allReviewers.add(rv.login); + review.requestedReviewers.push(rv.login); + }); + prReviews.data.forEach(datum => allReviewers.add(datum.user.login)); + + allReviewers + .forEach(reviewer => { + if (reviewer === author) return; + const isPrebidEngineer = prebidEngineers.includes(reviewer); + const isPrebidReviewer = isPrebidEngineer || prebidReviewers.includes(reviewer) || authorizedReviewers.includes(reviewer); + if (isPrebidEngineer) { + review.prebidEngineers += 1; + } + if (isPrebidReviewer) { + review.prebidReviewers += 1 + } + review.reviewers.push({ + login: reviewer, + isPrebidEngineer, + isPrebidReviewer, + }) + }); + const data = { + pr: prNo, + author: { + login: author, + isPrebidMember: await isPrebidMember(author) + }, + isCoreChange, + files, + prebidReviewers, + prebidEngineers, + review, + } + data.review.requires = reviewRequirements(data); + data.review.ok = satisfiesReviewRequirements(data.review); + return data; +} + +function reviewRequirements(prData) { + return { + prebidEngineers: prData.author.isPrebidMember ? 1 : 0, + prebidReviewers: prData.isCoreChange ? 2 : 1 + } +} + +function satisfiesReviewRequirements({requires, prebidEngineers, prebidReviewers}) { + return prebidEngineers >= requires.prebidEngineers && prebidReviewers >= requires.prebidReviewers +} + + +module.exports = getPRProperties; diff --git a/.github/workflows/scripts/ghRequest.js b/.github/workflows/scripts/ghRequest.js new file mode 100644 index 00000000000..cc09edaf390 --- /dev/null +++ b/.github/workflows/scripts/ghRequest.js @@ -0,0 +1,9 @@ +module.exports = function githubRequester(github) { + return function (verb, params) { + return github.request(verb, Object.assign({ + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }, params)) + } +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d243a67ca5d..54a41e25946 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,14 +33,14 @@ jobs: - name: Checkout code (PR) id: checkout-pr if: ${{ github.event_name == 'pull_request_target' }} - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: refs/pull/${{ github.event.pull_request.number }}/head - name: Checkout code (push) id: checkout-push if: ${{ github.event_name == 'push' }} - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Commit info id: info @@ -53,37 +53,24 @@ jobs: echo base-commit="${{ github.event.pull_request.base.sha || github.event.before }}" >> $GITHUB_OUTPUT - name: Install dependencies - run: npm ci + uses: ./.github/actions/npm-ci - - name: Cache source - uses: actions/cache/save@v4 + - name: 'Save working directory' + uses: ./.github/actions/save with: - path: . - key: source-${{ github.run_id }} - - - name: Verify cache - uses: actions/cache/restore@v4 - with: - path: . - key: source-${{ github.run_id }} - lookup-only: true - fail-on-cache-miss: true + name: source lint: name: "Run linter" needs: checkout runs-on: ubuntu-latest steps: - - name: Set up Node.js - uses: actions/setup-node@v6 - with: - node-version: '20' + - name: Checkout + uses: actions/checkout@v6 - name: Restore source - uses: actions/cache/restore@v4 + uses: ./.github/actions/load with: - path: . - key: source-${{ github.run_id }} - fail-on-cache-miss: true + name: source - name: lint run: | npx eslint @@ -91,90 +78,36 @@ jobs: test-no-features: name: "Unit tests (all features disabled)" needs: checkout - uses: ./.github/workflows/run-unit-tests.yml + uses: ./.github/workflows/run-tests.yml with: + chunks: 8 build-cmd: npx gulp precompile-all-features-disabled test-cmd: npx gulp test-all-features-disabled-nobuild - serialize: false + browserstack: false secrets: BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} test: - name: "Unit tests (all features enabled + coverage)" + name: "Browser tests" needs: checkout - uses: ./.github/workflows/run-unit-tests.yml - with: - build-cmd: npx gulp precompile - test-cmd: npx gulp test-only-nobuild --browserstack - serialize: true + uses: ./.github/workflows/browser-tests.yml secrets: BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - test-e2e: - name: "End-to-end tests" - needs: checkout - runs-on: ubuntu-latest - concurrency: - # see test-chunk.yml for notes on concurrency groups - group: browserstack-${{ github.run_id }} - cancel-in-progress: false - env: - BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - steps: - - name: Set up Node.js - uses: actions/setup-node@v6 - with: - node-version: '20' - - name: Restore source - uses: actions/cache/restore@v4 - with: - path: . - key: source-${{ github.run_id }} - fail-on-cache-miss: true - - - name: 'BrowserStack Env Setup' - uses: 'browserstack/github-actions/setup-env@master' - with: - username: ${{ secrets.BROWSERSTACK_USER_NAME}} - access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - - - name: 'BrowserStackLocal Setup' - uses: 'browserstack/github-actions/setup-local@master' - with: - local-testing: start - local-identifier: random - - - name: 'Wait for browserstack' - uses: ./.github/actions/wait-for-browserstack - with: - BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - - - name: Run tests - uses: nick-fields/retry@v3 - with: - timeout_minutes: 20 - max_attempts: 3 - command: npx gulp e2e-test - - - name: 'BrowserStackLocal Stop' - uses: 'browserstack/github-actions/setup-local@master' - with: - local-testing: stop - coveralls: name: Update coveralls needs: [checkout, test] runs-on: ubuntu-latest steps: - - name: Restore working directory - uses: actions/cache/restore@v4 + - name: Checkout + uses: actions/checkout@v6 + + - name: Restore source + uses: ./.github/actions/load with: - path: . - key: ${{ needs.test.outputs.wdir }} - fail-on-cache-miss: true + name: ${{ needs.test.outputs.coverage }} + - name: Coveralls uses: coverallsapp/github-action@v2 with: diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000000..4812751a94a --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +lockfile-version=2 diff --git a/AGENTS.md b/AGENTS.md index ec1601c61f4..ce4e1353a9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,7 @@ This file contains instructions for the Codex agent and its friends when working - Always include the string 'codex' or 'agent' in any branch you create. If you instructed to not do that, always include the string 'perbid'. - Do not submit pr's with changes to creative.html or creative.js - Read CONTRIBUTING.md and PR_REVIEW.md for additional context +- Use the guidelines at PR_REVIEW.md when doing PR reviews. Make all your comments and code suggestions on the PR itself instead of in linked tasks when commenting in a PR review. ## Testing - When you modify or add source or test files, run only the affected unit tests. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 606d26cd25a..b7a797beeb4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ Prebid uses [Mocha](http://mochajs.org/) and [Chai](http://chaijs.com/) for unit provides mocks, stubs, and spies. [Karma](https://karma-runner.github.io/1.0/index.html) runs the tests and generates code coverage reports at `build/coverage/lcov/lcov-report/index.html`. -Tests are stored in the [test/spec](test/spec) directory. Tests for Adapters are located in [test/spec/adapters](test/spec/adapters). +Tests are stored in the [test/spec](test/spec) directory. Tests for Adapters are located in [test/spec/modules](test/spec/modules). They can be run with the following commands: - `gulp test` - run the test suite once (`npm test` is aliased to call `gulp test`) diff --git a/browsers.json b/browsers.json index 974df030ee7..f37d708221e 100644 --- a/browsers.json +++ b/browsers.json @@ -15,11 +15,11 @@ "device": null, "os": "Windows" }, - "bs_chrome_109_windows_10": { + "bs_chrome_113_windows_10": { "base": "BrowserStack", "os_version": "10", "browser": "chrome", - "browser_version": "109.0", + "browser_version": "113.0", "device": null, "os": "Windows" }, @@ -47,5 +47,4 @@ "device": null, "os": "OS X" } - } diff --git a/customize/buildOptions.mjs b/customize/buildOptions.mjs index 7b8013ab269..3341f35b0e0 100644 --- a/customize/buildOptions.mjs +++ b/customize/buildOptions.mjs @@ -1,8 +1,12 @@ import path from 'path' -import validate from 'schema-utils' +import { validate } from 'schema-utils' const boModule = path.resolve(import.meta.dirname, '../dist/src/buildOptions.mjs') +/** + * Resolve the absolute path of the default build options module. + * @returns {string} Absolute path to the generated build options module. + */ export function getBuildOptionsModule () { return boModule } @@ -25,6 +29,11 @@ const schema = { } } +/** + * Validate and load build options overrides. + * @param {object} [options] user supplied overrides + * @returns {Promise} Promise resolving to merged build options. + */ export function getBuildOptions (options = {}) { validate(schema, options, { name: 'Prebid build options', diff --git a/gulpfile.js b/gulpfile.js index dc1cc51f62e..e5a4db41884 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -39,7 +39,7 @@ const TerserPlugin = require('terser-webpack-plugin'); const {precompile, babelPrecomp} = require('./gulp.precompilation.js'); -const TEST_CHUNKS = 4; +const TEST_CHUNKS = 8; // these modules must be explicitly listed in --modules to be included in the build, won't be part of "all" modules var explicitModules = [ @@ -521,7 +521,7 @@ gulp.task('build-bundle-verbose', gulp.series(precompile(), makeWebpackPkg(makeV // public tasks (dependencies are needed for each task since they can be ran on their own) gulp.task('update-browserslist', execaTask('npx update-browserslist-db@latest')); gulp.task('test-build-logic', execaTask('npx mocha ./test/build-logic')) -gulp.task('test-only-nobuild', gulp.series('test-build-logic', testTaskMaker({coverage: true}))) +gulp.task('test-only-nobuild', gulp.series(testTaskMaker({coverage: argv.coverage ?? true}))) gulp.task('test-only', gulp.series('test-build-logic', 'precompile', test)); gulp.task('test-all-features-disabled-nobuild', testTaskMaker({disableFeatures: helpers.getTestDisableFeatures(), oneBrowser: 'chrome', watch: false})); @@ -554,6 +554,7 @@ gulp.task('default', gulp.series('build')); gulp.task('e2e-test-only', gulp.series(requireNodeVersion(16), () => runWebdriver({file: argv.file}))); gulp.task('e2e-test', gulp.series(requireNodeVersion(16), clean, 'build-bundle-prod', e2eTestTaskMaker())); +gulp.task('e2e-test-nobuild', gulp.series(requireNodeVersion(16), e2eTestTaskMaker())) // other tasks gulp.task(bundleToStdout); diff --git a/integrationExamples/chromeai/japanese.html b/integrationExamples/chromeai/japanese.html index fd212b03550..3d7c5954090 100644 --- a/integrationExamples/chromeai/japanese.html +++ b/integrationExamples/chromeai/japanese.html @@ -135,8 +135,8 @@ enabled: true }, summarizer:{ - enabled: false, - length: 190 + enabled: true, + length: "medium" } }, }, diff --git a/integrationExamples/gpt/mediago_test.html b/integrationExamples/gpt/mediago_test.html new file mode 100644 index 00000000000..d09e814eda4 --- /dev/null +++ b/integrationExamples/gpt/mediago_test.html @@ -0,0 +1,442 @@ + + + + + + Mediago Bid Adapter Test + + + + + + +

Mediago Bid Adapter Test Page

+

This page is used to verify that the ID uniqueness issue has been resolved when there are multiple ad units

+ +
+ + +
+ +
+

Waiting for request...

+

Click the "Run Auction" button to start testing

+
+ +
+

Waiting for response...

+
+ +
+

Ad Unit 1 (300x250) - mpu_left

+
+

Ad Unit 1 - mpu_left

+
+
+ +
+

Ad Unit 2 (300x250)

+
+

Ad Unit 2

+
+
+ +
+

Ad Unit 3 (728x90)

+
+

Ad Unit 3

+
+
+ + + + + + diff --git a/integrationExamples/gpt/neuwoRtdProvider_example.html b/integrationExamples/gpt/neuwoRtdProvider_example.html index 33cdccd0e69..3d6fef98995 100644 --- a/integrationExamples/gpt/neuwoRtdProvider_example.html +++ b/integrationExamples/gpt/neuwoRtdProvider_example.html @@ -121,10 +121,30 @@ const inputIabContentTaxonomyVersion = document.getElementById('iab-content-taxonomy-version'); const iabContentTaxonomyVersion = inputIabContentTaxonomyVersion ? inputIabContentTaxonomyVersion.value : undefined; + // Cache Option + const inputEnableCache = document.getElementById('enable-cache'); + const enableCache = inputEnableCache ? inputEnableCache.checked : undefined; + + // URL Stripping Options + const inputStripAllQueryParams = document.getElementById('strip-all-query-params'); + const stripAllQueryParams = inputStripAllQueryParams ? inputStripAllQueryParams.checked : undefined; + + const inputStripQueryParamsForDomains = document.getElementById('strip-query-params-for-domains'); + const stripQueryParamsForDomainsValue = inputStripQueryParamsForDomains ? inputStripQueryParamsForDomains.value.trim() : ''; + const stripQueryParamsForDomains = stripQueryParamsForDomainsValue ? stripQueryParamsForDomainsValue.split(',').map(d => d.trim()).filter(d => d) : undefined; + + const inputStripQueryParams = document.getElementById('strip-query-params'); + const stripQueryParamsValue = inputStripQueryParams ? inputStripQueryParams.value.trim() : ''; + const stripQueryParams = stripQueryParamsValue ? stripQueryParamsValue.split(',').map(p => p.trim()).filter(p => p) : undefined; + + const inputStripFragments = document.getElementById('strip-fragments'); + const stripFragments = inputStripFragments ? inputStripFragments.checked : undefined; + pbjs.que.push(function () { pbjs.setConfig({ debug: true, realTimeData: { + auctionDelay: 500, dataProviders: [ { name: "NeuwoRTDModule", @@ -133,7 +153,12 @@ neuwoApiUrl, neuwoApiToken, websiteToAnalyseUrl, - iabContentTaxonomyVersion + iabContentTaxonomyVersion, + enableCache, + stripAllQueryParams, + stripQueryParamsForDomains, + stripQueryParams, + stripFragments } } ] @@ -166,11 +191,14 @@

Basic Prebid.js Example using Neuwo Rtd Provider

after running commands in the prebid.js source folder that includes libraries/modules/neuwoRtdProvider.js + // Install dependencies npm ci + + // Run a local development server npx gulp serve --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter // No tests - npx gulp serve-fast --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter --notests + npx gulp serve-fast --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter // Only tests npx gulp test-only --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter --file=test/spec/modules/neuwoRtdProvider_spec.js @@ -180,10 +208,49 @@

Basic Prebid.js Example using Neuwo Rtd Provider

Neuwo Rtd Provider Configuration

Add token and url to use for Neuwo extension configuration

- - - - +
+ +
+
+ +
+
+ +
+ +

IAB Content Taxonomy Options

+
+ +
+ +

Cache Options

+
+ +
+ +

URL Cleaning Options

+
+ +
+
+ +
+
+ +
+
+ +
+
@@ -232,4 +299,86 @@

Neuwo Data in Bid Request

if (helper) helper.style.display = location.href !== 'http://localhost:9999/integrationExamples/gpt/neuwoRtdProvider_example.html' ? 'block' : 'none'; + + \ No newline at end of file diff --git a/integrationExamples/gpt/prebidServer_example.html b/integrationExamples/gpt/prebidServer_example.html index f247dd6d565..ff37eed3ead 100644 --- a/integrationExamples/gpt/prebidServer_example.html +++ b/integrationExamples/gpt/prebidServer_example.html @@ -83,9 +83,10 @@ + + + + + + + + + + + + +

Prebid.js Test

+
Div-1111
+
+ +
+
+ + +
Div 2
+
+ +
+ + diff --git a/integrationExamples/gpt/x-domain/creative.html b/integrationExamples/gpt/x-domain/creative.html index ae8456c19e0..14deeb4f559 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' - +

Prebid Test Bidder Example

+

+

Banner ad
- \ No newline at end of file + diff --git a/karma.conf.maker.js b/karma.conf.maker.js index 1068e9828d8..f6f9d903d58 100644 --- a/karma.conf.maker.js +++ b/karma.conf.maker.js @@ -40,6 +40,7 @@ function newWebpackConfig(codeCoverage, disableFeatures) { function newPluginsArray(browserstack) { var plugins = [ 'karma-chrome-launcher', + 'karma-safarinative-launcher', 'karma-coverage', 'karma-mocha', 'karma-chai', @@ -47,14 +48,14 @@ function newPluginsArray(browserstack) { 'karma-sourcemap-loader', 'karma-spec-reporter', 'karma-webpack', - 'karma-mocha-reporter' + 'karma-mocha-reporter', + '@chiragrupani/karma-chromium-edge-launcher', ]; if (browserstack) { plugins.push('karma-browserstack-launcher'); } plugins.push('karma-firefox-launcher'); plugins.push('karma-opera-launcher'); - plugins.push('karma-safari-launcher'); plugins.push('karma-script-launcher'); return plugins; } @@ -84,13 +85,19 @@ function setReporters(karmaConf, codeCoverage, browserstack, chunkNo) { } function setBrowsers(karmaConf, browserstack) { + karmaConf.customLaunchers = karmaConf.customLaunchers || {}; + karmaConf.customLaunchers.ChromeNoSandbox = { + base: 'ChromeHeadless', + // disable sandbox - necessary within Docker and when using versions installed through @puppeteer/browsers + flags: ['--no-sandbox'] + } if (browserstack) { karmaConf.browserStack = { username: process.env.BROWSERSTACK_USERNAME, accessKey: process.env.BROWSERSTACK_ACCESS_KEY, - build: 'Prebidjs Unit Tests ' + new Date().toLocaleString() + build: process.env.BROWSERSTACK_BUILD_NAME } - if (process.env.TRAVIS) { + if (process.env.BROWSERSTACK_LOCAL_IDENTIFIER) { karmaConf.browserStack.startTunnel = false; karmaConf.browserStack.tunnelIdentifier = process.env.BROWSERSTACK_LOCAL_IDENTIFIER; } @@ -99,14 +106,7 @@ function setBrowsers(karmaConf, browserstack) { } else { var isDocker = require('is-docker')(); if (isDocker) { - karmaConf.customLaunchers = karmaConf.customLaunchers || {}; - karmaConf.customLaunchers.ChromeCustom = { - base: 'ChromeHeadless', - // We must disable the Chrome sandbox when running Chrome inside Docker (Chrome's sandbox needs - // more permissions than Docker allows by default) - flags: ['--no-sandbox'] - } - karmaConf.browsers = ['ChromeCustom']; + karmaConf.browsers = ['ChromeNoSandbox']; } else { karmaConf.browsers = ['ChromeHeadless']; } @@ -174,10 +174,10 @@ module.exports = function(codeCoverage, browserstack, watchMode, file, disableFe // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits singleRun: !watchMode, - browserDisconnectTimeout: 1e5, // default 2000 - browserNoActivityTimeout: 1e5, // default 10000 - captureTimeout: 3e5, // default 60000, - browserDisconnectTolerance: 3, + browserDisconnectTimeout: 1e4, + browserNoActivityTimeout: 3e4, + captureTimeout: 2e4, + browserDisconnectTolerance: 5, concurrency: 5, // browserstack allows us 5 concurrent sessions plugins: plugins diff --git a/libraries/adagioUtils/adagioUtils.js b/libraries/adagioUtils/adagioUtils.js index c2614c45d0c..265a442017a 100644 --- a/libraries/adagioUtils/adagioUtils.js +++ b/libraries/adagioUtils/adagioUtils.js @@ -22,6 +22,7 @@ export const _ADAGIO = (function() { const w = getBestWindowForAdagio(); w.ADAGIO = w.ADAGIO || {}; + // TODO: consider using the Prebid-generated page view ID instead of generating a custom one w.ADAGIO.pageviewId = w.ADAGIO.pageviewId || generateUUID(); w.ADAGIO.adUnits = w.ADAGIO.adUnits || {}; w.ADAGIO.pbjsAdUnits = w.ADAGIO.pbjsAdUnits || []; diff --git a/libraries/bidderTimeoutUtils/bidderTimeoutUtils.js b/libraries/bidderTimeoutUtils/bidderTimeoutUtils.js new file mode 100644 index 00000000000..721df815159 --- /dev/null +++ b/libraries/bidderTimeoutUtils/bidderTimeoutUtils.js @@ -0,0 +1,119 @@ +import { logInfo } from '../../src/utils.js'; + +// this allows the stubbing of functions during testing +export const bidderTimeoutFunctions = { + getDeviceType, + checkVideo, + getConnectionSpeed, + calculateTimeoutModifier +}; + +/** + * Returns an array of a given object's own enumerable string-keyed property [key, value] pairs. + * @param {Object} obj + * @return {Array} + */ +const entries = Object.entries || function (obj) { + const ownProps = Object.keys(obj); + let i = ownProps.length; + let resArray = new Array(i); + while (i--) { resArray[i] = [ownProps[i], obj[ownProps[i]]]; } + return resArray; +}; + +function getDeviceType() { + const userAgent = window.navigator.userAgent.toLowerCase(); + if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(userAgent))) { + return 5; // tablet + } + if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(userAgent))) { + return 4; // mobile + } + return 2; // personal computer +} + +function checkVideo(adUnits) { + return adUnits.some((adUnit) => { + return adUnit.mediaTypes && adUnit.mediaTypes.video; + }); +} + +function getConnectionSpeed() { + const connection = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection || {} + const connectionType = connection.type || connection.effectiveType; + + switch (connectionType) { + case 'slow-2g': + case '2g': + return 'slow'; + + case '3g': + return 'medium'; + + case 'bluetooth': + case 'cellular': + case 'ethernet': + case 'wifi': + case 'wimax': + case '4g': + return 'fast'; + } + + return 'unknown'; +} + +/** + * Calculate the time to be added to the timeout + * @param {Array} adUnits + * @param {Object} rules + * @return {number} + */ +function calculateTimeoutModifier(adUnits, rules) { + if (!rules) { + return 0; + } + + logInfo('Timeout rules', rules); + let timeoutModifier = 0; + let toAdd = 0; + + if (rules.includesVideo) { + const hasVideo = bidderTimeoutFunctions.checkVideo(adUnits); + toAdd = rules.includesVideo[hasVideo] || 0; + logInfo(`Adding ${toAdd} to timeout for includesVideo ${hasVideo}`) + timeoutModifier += toAdd; + } + + if (rules.numAdUnits) { + const numAdUnits = adUnits.length; + if (rules.numAdUnits[numAdUnits]) { + timeoutModifier += rules.numAdUnits[numAdUnits]; + } else { + for (const [rangeStr, timeoutVal] of entries(rules.numAdUnits)) { + const [lowerBound, upperBound] = rangeStr.split('-'); + if (parseInt(lowerBound) <= numAdUnits && numAdUnits <= parseInt(upperBound)) { + logInfo(`Adding ${timeoutVal} to timeout for numAdUnits ${numAdUnits}`) + timeoutModifier += timeoutVal; + break; + } + } + } + } + + if (rules.deviceType) { + const deviceType = bidderTimeoutFunctions.getDeviceType(); + toAdd = rules.deviceType[deviceType] || 0; + logInfo(`Adding ${toAdd} to timeout for deviceType ${deviceType}`) + timeoutModifier += toAdd; + } + + if (rules.connectionSpeed) { + const connectionSpeed = bidderTimeoutFunctions.getConnectionSpeed(); + toAdd = rules.connectionSpeed[connectionSpeed] || 0; + logInfo(`Adding ${toAdd} to timeout for connectionSpeed ${connectionSpeed}`) + timeoutModifier += toAdd; + } + + logInfo('timeout Modifier calculated', timeoutModifier); + return timeoutModifier; +} diff --git a/libraries/connectionInfo/connectionUtils.js b/libraries/connectionInfo/connectionUtils.js index 29d1e310986..562872d75ba 100644 --- a/libraries/connectionInfo/connectionUtils.js +++ b/libraries/connectionInfo/connectionUtils.js @@ -3,11 +3,51 @@ * * @returns {number} - Type of connection. */ +function resolveNavigator() { + if (typeof window !== 'undefined' && window.navigator) { + return window.navigator; + } + + if (typeof navigator !== 'undefined') { + return navigator; + } + + return null; +} + +function resolveNetworkInformation() { + const nav = resolveNavigator(); + if (!nav) { + return null; + } + + return nav.connection || nav.mozConnection || nav.webkitConnection || null; +} + +export function getConnectionInfo() { + const connection = resolveNetworkInformation(); + + if (!connection) { + return null; + } + + return { + type: connection.type ?? null, + effectiveType: connection.effectiveType ?? null, + downlink: typeof connection.downlink === 'number' ? connection.downlink : null, + downlinkMax: typeof connection.downlinkMax === 'number' ? connection.downlinkMax : null, + rtt: typeof connection.rtt === 'number' ? connection.rtt : null, + saveData: typeof connection.saveData === 'boolean' ? connection.saveData : null, + bandwidth: typeof connection.bandwidth === 'number' ? connection.bandwidth : null + }; +} + export function getConnectionType() { - const connection = navigator.connection || navigator.webkitConnection; + const connection = getConnectionInfo(); if (!connection) { return 0; } + switch (connection.type) { case 'ethernet': return 1; diff --git a/libraries/devicePixelRatio/devicePixelRatio.js b/libraries/devicePixelRatio/devicePixelRatio.js new file mode 100644 index 00000000000..8bfe23bcb3d --- /dev/null +++ b/libraries/devicePixelRatio/devicePixelRatio.js @@ -0,0 +1,17 @@ +import {canAccessWindowTop, internal as utilsInternals} from '../../src/utils.js'; + +function getFallbackWindow(win) { + if (win) { + return win; + } + + return canAccessWindowTop() ? utilsInternals.getWindowTop() : utilsInternals.getWindowSelf(); +} + +export function getDevicePixelRatio(win) { + try { + return getFallbackWindow(win).devicePixelRatio; + } catch (e) { + } + return 1; +} diff --git a/libraries/fpdUtils/pageInfo.js b/libraries/fpdUtils/pageInfo.js index 8e02134e070..3d23f317085 100644 --- a/libraries/fpdUtils/pageInfo.js +++ b/libraries/fpdUtils/pageInfo.js @@ -69,3 +69,12 @@ export function getReferrer(bidRequest = {}, bidderRequest = {}) { } return pageUrl; } + +/** + * get the document complexity + * @param document + * @returns {*|number} + */ +export function getDomComplexity(document) { + return document?.querySelectorAll('*')?.length ?? -1; +} diff --git a/libraries/intentIqConstants/intentIqConstants.js b/libraries/intentIqConstants/intentIqConstants.js index 0992f4c26b3..0fa479a19ad 100644 --- a/libraries/intentIqConstants/intentIqConstants.js +++ b/libraries/intentIqConstants/intentIqConstants.js @@ -1,19 +1,19 @@ -export const FIRST_PARTY_KEY = '_iiq_fdata'; +export const FIRST_PARTY_KEY = "_iiq_fdata"; -export const SUPPORTED_TYPES = ['html5', 'cookie'] +export const SUPPORTED_TYPES = ["html5", "cookie"]; -export const WITH_IIQ = 'A'; -export const WITHOUT_IIQ = 'B'; -export const NOT_YET_DEFINED = 'U'; -export const BLACK_LIST = 'L'; -export const CLIENT_HINTS_KEY = '_iiq_ch'; -export const EMPTY = 'EMPTY'; -export const GVLID = '1323'; -export const VERSION = 0.32; -export const PREBID = 'pbjs'; +export const WITH_IIQ = "A"; +export const WITHOUT_IIQ = "B"; +export const DEFAULT_PERCENTAGE = 95; +export const BLACK_LIST = "L"; +export const CLIENT_HINTS_KEY = "_iiq_ch"; +export const EMPTY = "EMPTY"; +export const GVLID = "1323"; +export const VERSION = 0.33; +export const PREBID = "pbjs"; export const HOURS_24 = 86400000; -export const INVALID_ID = 'INVALID_ID'; +export const INVALID_ID = "INVALID_ID"; export const SYNC_REFRESH_MILL = 3600000; export const META_DATA_CONSTANT = 256; @@ -25,10 +25,24 @@ export const MAX_REQUEST_LENGTH = { opera: 2097152, edge: 2048, firefox: 65536, - ie: 2048 + ie: 2048, }; export const CH_KEYS = [ - 'brands', 'mobile', 'platform', 'bitness', 'wow64', 'architecture', - 'model', 'platformVersion', 'fullVersionList' + "brands", + "mobile", + "platform", + "bitness", + "wow64", + "architecture", + "model", + "platformVersion", + "fullVersionList", ]; + +export const AB_CONFIG_SOURCE = { + PERCENTAGE: "percentage", + GROUP: "group", + IIQ_SERVER: "IIQServer", + DISABLED: "disabled", +}; diff --git a/libraries/intentIqUtils/defineABTestingGroupUtils.js b/libraries/intentIqUtils/defineABTestingGroupUtils.js new file mode 100644 index 00000000000..af810ac278c --- /dev/null +++ b/libraries/intentIqUtils/defineABTestingGroupUtils.js @@ -0,0 +1,74 @@ +import { + WITH_IIQ, + WITHOUT_IIQ, + DEFAULT_PERCENTAGE, + AB_CONFIG_SOURCE, +} from "../intentIqConstants/intentIqConstants.js"; + +/** + * Fix percentage if provided some incorrect data + * clampPct(150) => 100 + * clampPct(-5) => 0 + * clampPct('abc') => DEFAULT_PERCENTAGE + */ +function clampPct(val) { + const n = Number(val); + if (!Number.isFinite(n)) return DEFAULT_PERCENTAGE; // fallback = 95 + return Math.max(0, Math.min(100, n)); +} + +/** + * Randomly assigns a user to group A or B based on the given percentage. + * Generates a random number (1–100) and compares it with the percentage. + * + * @param {number} pct The percentage threshold (0–100). + * @returns {string} Returns WITH_IIQ for Group A or WITHOUT_IIQ for Group B. + */ +function pickABByPercentage(pct) { + const percentageToUse = + typeof pct === "number" ? pct : DEFAULT_PERCENTAGE; + const percentage = clampPct(percentageToUse); + const roll = Math.floor(Math.random() * 100) + 1; + return roll <= percentage ? WITH_IIQ : WITHOUT_IIQ; // A : B +} + +function configurationSourceGroupInitialization(group) { + return typeof group === 'string' && group.toUpperCase() === WITHOUT_IIQ ? WITHOUT_IIQ : WITH_IIQ; +} + +/** + * Determines the runtime A/B testing group without saving it to Local Storage. + * 1. If terminationCause (tc) exists: + * - tc = 41 → Group B (WITHOUT_IIQ) + * - any other value → Group A (WITH_IIQ) + * 2. Otherwise, assigns the group randomly based on DEFAULT_PERCENTAGE (default 95% for A, 5% for B). + * + * @param {number} [tc] The termination cause value returned by the server. + * @param {number} [abPercentage] A/B percentage provided by partner. + * @returns {string} The determined group: WITH_IIQ (A) or WITHOUT_IIQ (B). + */ + +function IIQServerConfigurationSource(tc, abPercentage) { + if (typeof tc === "number" && Number.isFinite(tc)) { + return tc === 41 ? WITHOUT_IIQ : WITH_IIQ; + } + + return pickABByPercentage(abPercentage); +} + +export function defineABTestingGroup(configObject, tc) { + switch (configObject.ABTestingConfigurationSource) { + case AB_CONFIG_SOURCE.GROUP: + return configurationSourceGroupInitialization( + configObject.group + ); + case AB_CONFIG_SOURCE.PERCENTAGE: + return pickABByPercentage(configObject.abPercentage); + default: { + if (!configObject.ABTestingConfigurationSource) { + configObject.ABTestingConfigurationSource = AB_CONFIG_SOURCE.IIQ_SERVER; + } + return IIQServerConfigurationSource(tc, configObject.abPercentage); + } + } +} diff --git a/libraries/intentIqUtils/gamPredictionReport.js b/libraries/intentIqUtils/gamPredictionReport.js index 09db065f494..2191ade6d35 100644 --- a/libraries/intentIqUtils/gamPredictionReport.js +++ b/libraries/intentIqUtils/gamPredictionReport.js @@ -55,6 +55,7 @@ export function gamPredictionReport (gamObjectReference, sendData) { dataToSend.originalCurrency = bid.originalCurrency; dataToSend.status = bid.status; dataToSend.prebidAuctionId = element.args?.auctionId; + if (!dataToSend.bidderCode) dataToSend.bidderCode = 'GAM'; }; if (dataToSend.bidderCode) { const relevantBid = element.args?.bidsReceived.find( @@ -86,12 +87,12 @@ export function gamPredictionReport (gamObjectReference, sendData) { gamObjectReference.pubads().addEventListener('slotRenderEnded', (event) => { if (event.isEmpty) return; const data = extractWinData(event); - if (data?.cpm) { + if (data) { sendData(data); } }); }); } catch (error) { - this.logger.error('Failed to subscribe to GAM: ' + error); + logError('Failed to subscribe to GAM: ' + error); } }; diff --git a/libraries/nativeAssetsUtils.js b/libraries/nativeAssetsUtils.js new file mode 100644 index 00000000000..9a59716cc68 --- /dev/null +++ b/libraries/nativeAssetsUtils.js @@ -0,0 +1,153 @@ +import { isEmpty } from '../src/utils.js'; + +export const NATIVE_PARAMS = { + title: { + id: 1, + name: 'title' + }, + icon: { + id: 2, + type: 1, + name: 'img' + }, + image: { + id: 3, + type: 3, + name: 'img' + }, + body: { + id: 4, + name: 'data', + type: 2 + }, + sponsoredBy: { + id: 5, + name: 'data', + type: 1 + }, + cta: { + id: 6, + type: 12, + name: 'data' + }, + body2: { + id: 7, + name: 'data', + type: 10 + }, + rating: { + id: 8, + name: 'data', + type: 3 + }, + likes: { + id: 9, + name: 'data', + type: 4 + }, + downloads: { + id: 10, + name: 'data', + type: 5 + }, + displayUrl: { + id: 11, + name: 'data', + type: 11 + }, + price: { + id: 12, + name: 'data', + type: 6 + }, + salePrice: { + id: 13, + name: 'data', + type: 7 + }, + address: { + id: 14, + name: 'data', + type: 9 + }, + phone: { + id: 15, + name: 'data', + type: 8 + } +}; + +const NATIVE_ID_MAP = Object.entries(NATIVE_PARAMS).reduce((result, [key, asset]) => { + result[asset.id] = key; + return result; +}, {}); + +export function buildNativeRequest(nativeParams) { + const assets = []; + if (nativeParams) { + Object.keys(nativeParams).forEach((key) => { + if (NATIVE_PARAMS[key]) { + const {name, type, id} = NATIVE_PARAMS[key]; + const assetObj = type ? {type} : {}; + let {len, sizes, required, aspect_ratios: aRatios} = nativeParams[key]; + if (len) { + assetObj.len = len; + } + if (aRatios && aRatios[0]) { + aRatios = aRatios[0]; + const wmin = aRatios.min_width || 0; + const hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; + assetObj.wmin = wmin; + assetObj.hmin = hmin; + } + if (sizes && sizes.length) { + sizes = [].concat(...sizes); + assetObj.w = sizes[0]; + assetObj.h = sizes[1]; + } + const asset = {required: required ? 1 : 0, id}; + asset[name] = assetObj; + assets.push(asset); + } + }); + } + return { + ver: '1.2', + request: { + assets: assets, + context: 1, + plcmttype: 1, + ver: '1.2' + } + }; +} + +export function parseNativeResponse(native) { + const {assets, link, imptrackers, jstracker} = native; + const result = { + clickUrl: link.url, + clickTrackers: link.clicktrackers || [], + impressionTrackers: imptrackers || [], + javascriptTrackers: jstracker ? [jstracker] : [] + }; + + (assets || []).forEach((asset) => { + const {id, img, data, title} = asset; + const key = NATIVE_ID_MAP[id]; + if (key) { + if (!isEmpty(title)) { + result.title = title.text; + } else if (!isEmpty(img)) { + result[key] = { + url: img.url, + height: img.h, + width: img.w + }; + } else if (!isEmpty(data)) { + result[key] = data.value; + } + } + }); + + return result; +} diff --git a/libraries/navigatorData/navigatorData.js b/libraries/navigatorData/navigatorData.js index f1a34fc51eb..e91a39493bf 100644 --- a/libraries/navigatorData/navigatorData.js +++ b/libraries/navigatorData/navigatorData.js @@ -7,23 +7,3 @@ export function getHLen(win = window) { } return hLen; } - -export function getHC(win = window) { - let hc; - try { - hc = win.top.navigator.hardwareConcurrency; - } catch (error) { - hc = undefined; - } - return hc; -} - -export function getDM(win = window) { - let dm; - try { - dm = win.top.navigator.deviceMemory; - } catch (error) { - dm = undefined; - } - return dm; -} diff --git a/libraries/nexx360Utils/index.ts b/libraries/nexx360Utils/index.ts new file mode 100644 index 00000000000..0b8ed3fd719 --- /dev/null +++ b/libraries/nexx360Utils/index.ts @@ -0,0 +1,246 @@ +import { deepAccess, deepSetValue, generateUUID, logInfo } from '../../src/utils.js'; +import {Renderer} from '../../src/Renderer.js'; +import { getCurrencyFromBidderRequest } from '../ortb2Utils/currency.js'; +import { INSTREAM, OUTSTREAM } from '../../src/video.js'; +import { BANNER, MediaType, NATIVE, VIDEO } from '../../src/mediaTypes.js'; +import { BidResponse, VideoBidResponse } from '../../src/bidfactory.js'; +import { StorageManager } from '../../src/storageManager.js'; +import { BidRequest, ORTBImp, ORTBRequest, ORTBResponse } from '../../src/prebid.public.js'; +import { AdapterResponse, ServerResponse } from '../../src/adapters/bidderFactory.js'; + +const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; + +let sessionId:string | null = null; + +const getSessionId = ():string => { + if (!sessionId) { + sessionId = generateUUID(); + } + return sessionId; +} + +let lastPageUrl:string = ''; +let requestCounter:number = 0; + +const getRequestCount = ():number => { + if (lastPageUrl === window.location.pathname) { + return ++requestCounter; + } + lastPageUrl = window.location.pathname; + return 0; +} + +export const getLocalStorageFunctionGenerator = < + T extends Record +>( + storage: StorageManager, + bidderCode: string, + storageKey: string, + jsonKey: keyof T + ): (() => T | null) => { + return () => { + if (!storage.localStorageIsEnabled()) { + logInfo(`localstorage not enabled for ${bidderCode}`); + return null; + } + + const output = storage.getDataFromLocalStorage(storageKey); + if (output === null) { + const storageElement: T = { [jsonKey]: generateUUID() } as T; + storage.setDataInLocalStorage(storageKey, JSON.stringify(storageElement)); + return storageElement; + } + try { + return JSON.parse(output) as T; + } catch (e) { + logInfo(`failed to parse localstorage for ${bidderCode}:`, e); + return null; + } + }; +}; + +export function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + if (typeof serverResponses === 'object' && + serverResponses != null && + serverResponses.length > 0 && + serverResponses[0].hasOwnProperty('body') && + serverResponses[0].body.hasOwnProperty('ext') && + serverResponses[0].body.ext.hasOwnProperty('cookies') && + typeof serverResponses[0].body.ext.cookies === 'object') { + return serverResponses[0].body.ext.cookies.slice(0, 5); + } else { + return []; + } +}; + +const createOustreamRendererFunction = ( + divId: string, + width: number, + height: number +) => (bidResponse: VideoBidResponse) => { + bidResponse.renderer.push(() => { + (window as any).ANOutstreamVideo.renderAd({ + sizes: [width, height], + targetId: divId, + adResponse: bidResponse.vastXml, + rendererOptions: { + showBigPlayButton: false, + showProgressBar: 'bar', + showVolume: false, + allowFullscreen: true, + skippable: false, + content: bidResponse.vastXml + } + }); + }); +}; + +export type CreateRenderPayload = { + requestId: string, + vastXml: string, + divId: string, + width: number, + height: number +} + +export const createRenderer = ( + { requestId, vastXml, divId, width, height }: CreateRenderPayload +): Renderer | undefined => { + if (!vastXml) { + logInfo('No VAST in bidResponse'); + return; + } + const installPayload = { + id: requestId, + url: OUTSTREAM_RENDERER_URL, + loaded: false, + adUnitCode: divId, + targetId: divId, + }; + const renderer = Renderer.install(installPayload); + renderer.setRender(createOustreamRendererFunction(divId, width, height)); + return renderer; +}; + +export const enrichImp = (imp:ORTBImp, bidRequest:BidRequest): ORTBImp => { + deepSetValue(imp, 'tagid', bidRequest.adUnitCode); + deepSetValue(imp, 'ext.adUnitCode', bidRequest.adUnitCode); + const divId = bidRequest.params.divId || bidRequest.adUnitCode; + deepSetValue(imp, 'ext.divId', divId); + if (imp.video) { + const playerSize = deepAccess(bidRequest, 'mediaTypes.video.playerSize'); + const videoContext = deepAccess(bidRequest, 'mediaTypes.video.context'); + deepSetValue(imp, 'video.ext.playerSize', playerSize); + deepSetValue(imp, 'video.ext.context', videoContext); + } + return imp; +} + +export const enrichRequest = ( + request: ORTBRequest, + amxId: string | null, + pageViewId: string, + bidderVersion: string):ORTBRequest => { + if (amxId) { + deepSetValue(request, 'ext.localStorage.amxId', amxId); + if (!request.user) request.user = {}; + if (!request.user.ext) request.user.ext = {}; + if (!request.user.ext.eids) request.user.ext.eids = []; + (request.user.ext.eids as any).push({ + source: 'amxdt.net', + uids: [{ + id: `${amxId}`, + atype: 1 + }] + }); + } + deepSetValue(request, 'ext.version', '$prebid.version$'); + deepSetValue(request, 'ext.source', 'prebid.js'); + deepSetValue(request, 'ext.pageViewId', pageViewId); + deepSetValue(request, 'ext.bidderVersion', bidderVersion); + deepSetValue(request, 'ext.sessionId', getSessionId()); + deepSetValue(request, 'ext.requestCounter', getRequestCount()); + deepSetValue(request, 'cur', [getCurrencyFromBidderRequest(request) || 'USD']); + if (!request.user) request.user = {}; + return request; +}; + +export function createResponse(bid:any, ortbResponse:any): BidResponse { + let mediaType: MediaType = BANNER; + if ([INSTREAM, OUTSTREAM].includes(bid.ext.mediaType as string)) mediaType = VIDEO; + if (bid.ext.mediaType === NATIVE) mediaType = NATIVE; + const response:any = { + requestId: bid.impid, + cpm: bid.price, + width: bid.w, + height: bid.h, + creativeId: bid.crid, + currency: ortbResponse.cur, + netRevenue: true, + ttl: 120, + mediaType, + meta: { + advertiserDomains: bid.adomain, + demandSource: bid.ext.ssp, + }, + }; + if (bid.dealid) response.dealid = bid.dealid; + + if (bid.ext.mediaType === BANNER) response.ad = bid.adm; + if ([INSTREAM, OUTSTREAM].includes(bid.ext.mediaType as string)) response.vastXml = bid.adm; + if (bid.ext.mediaType === OUTSTREAM && (bid.ext.divId || bid.ext.adUnitCode)) { + const renderer = createRenderer({ + requestId: response.requestId, + vastXml: response.vastXml, + divId: bid.ext.divId || bid.ext.adUnitCode, + width: response.width, + height: response.height + }); + if (renderer) { + response.renderer = renderer; + response.divId = bid.ext.divId; + } else { + logInfo('Could not create renderer for outstream bid'); + } + }; + + if (bid.ext.mediaType === NATIVE) { + try { + response.native = { ortb: JSON.parse(bid.adm) } + } catch (e) {} + } + return response as BidResponse; +} + +export const interpretResponse = (serverResponse: ServerResponse): AdapterResponse => { + if (!serverResponse.body) return []; + const respBody = serverResponse.body as ORTBResponse; + if (!respBody.seatbid || respBody.seatbid.length === 0) return []; + + const responses: BidResponse[] = []; + for (let i = 0; i < respBody.seatbid.length; i++) { + const seatbid = respBody.seatbid[i]; + for (let j = 0; j < seatbid.bid.length; j++) { + const bid = seatbid.bid[j]; + const response:BidResponse = createResponse(bid, respBody); + responses.push(response); + } + } + return responses; +} + +/** + * Get the AMX ID + * @return { string | false } false if localstorageNotEnabled + */ +export const getAmxId = ( + storage: StorageManager, + bidderCode: string +): string | null => { + if (!storage.localStorageIsEnabled()) { + logInfo(`localstorage not enabled for ${bidderCode}`); + return null; + } + const amxId = storage.getDataFromLocalStorage('__amuidpb'); + return amxId || null; +} diff --git a/libraries/objectGuard/objectGuard.js b/libraries/objectGuard/objectGuard.js index 78ba0910bb9..973e56aad5f 100644 --- a/libraries/objectGuard/objectGuard.js +++ b/libraries/objectGuard/objectGuard.js @@ -26,6 +26,10 @@ export function objectGuard(rules) { // build a tree representation of them, where the root is the object itself, // and each node's children are properties of the corresponding (nested) object. + function invalid() { + return new Error('incompatible redaction rules'); + } + rules.forEach(rule => { rule.paths.forEach(path => { let node = root; @@ -36,15 +40,22 @@ export function objectGuard(rules) { node.wpRules = node.wpRules ?? []; node.redactRules = node.redactRules ?? []; }); - (rule.wp ? node.wpRules : node.redactRules).push(rule); - if (rule.wp) { - // mark the whole path as write protected, so that write operations - // on parents do not need to walk down the tree - let parent = node; - while (parent && !parent.hasWP) { - parent.hasWP = true; - parent = parent.parent; + const tag = rule.wp ? 'hasWP' : 'hasRedact'; + const ruleset = rule.wp ? 'wpRules' : 'redactRules'; + // sanity check: do not allow rules of the same type on related paths, + // e.g. redact both 'user' and 'user.eids'; we don't need and this logic + // does not handle it + if (node[tag] && !node[ruleset]?.length) { + throw invalid(); + } + node[ruleset].push(rule); + let parent = node; + while (parent) { + parent[tag] = true; + if (parent !== node && parent[ruleset]?.length) { + throw invalid(); } + parent = parent.parent; } }); }); @@ -128,25 +139,31 @@ export function objectGuard(rules) { return true; } - function mkGuard(obj, tree, final, applies) { - return new Proxy(obj, { + function mkGuard(obj, tree, final, applies, cache = new WeakMap()) { + // If this object is already proxied, return the cached proxy + if (cache.has(obj)) { + return cache.get(obj); + } + + const proxy = new Proxy(obj, { get(target, prop, receiver) { const val = Reflect.get(target, prop, receiver); if (final && val != null && typeof val === 'object') { // a parent property has write protect rules, keep guarding - return mkGuard(val, tree, final, applies) + return mkGuard(val, tree, final, applies, cache) } else if (tree.children?.hasOwnProperty(prop)) { const {children, hasWP} = tree.children[prop]; - if ((children || hasWP) && val != null && typeof val === 'object') { - // some nested properties have rules, return a guard for the branch - return mkGuard(val, tree.children?.[prop] || tree, final || children == null, applies); - } else if (isData(val)) { + if (isData(val)) { // if this property has redact rules, apply them const rule = getRedactRule(tree.children[prop]); if (rule && rule.check(applies)) { return rule.get(val); } } + if ((children || hasWP) && val != null && typeof val === 'object') { + // some nested properties have rules, return a guard for the branch + return mkGuard(val, tree.children?.[prop] || tree, final || children == null, applies, cache); + } } return val; }, @@ -162,7 +179,7 @@ export function objectGuard(rules) { // apply all (possibly nested) write protect rules const curValue = Reflect.get(target, prop, receiver); newValue = cleanup(tree.children[prop], curValue, newValue, applies); - if (!isData(newValue) && !target.hasOwnProperty(prop)) { + if (typeof newValue === 'undefined' && !target.hasOwnProperty(prop)) { return true; } } @@ -183,6 +200,10 @@ export function objectGuard(rules) { return Reflect.deleteProperty(target, prop); } }); + + // Cache the proxy before returning + cache.set(obj, proxy); + return proxy; } return function guard(obj, ...args) { diff --git a/libraries/pbsExtensions/processors/pageViewIds.js b/libraries/pbsExtensions/processors/pageViewIds.js new file mode 100644 index 00000000000..c71d32b7735 --- /dev/null +++ b/libraries/pbsExtensions/processors/pageViewIds.js @@ -0,0 +1,9 @@ +import {deepSetValue} from '../../../src/utils.js'; + +export function setRequestExtPrebidPageViewIds(ortbRequest, bidderRequest) { + deepSetValue( + ortbRequest, + `ext.prebid.page_view_ids.${bidderRequest.bidderCode}`, + bidderRequest.pageViewId + ); +} diff --git a/libraries/pbsExtensions/processors/pbs.js b/libraries/pbsExtensions/processors/pbs.js index 9346334abdb..3fa97ae674b 100644 --- a/libraries/pbsExtensions/processors/pbs.js +++ b/libraries/pbsExtensions/processors/pbs.js @@ -7,6 +7,7 @@ import {setImpAdUnitCode} from './adUnitCode.js'; import {setRequestExtPrebid, setRequestExtPrebidChannel} from './requestExtPrebid.js'; import {setBidResponseVideoCache} from './video.js'; import {addEventTrackers} from './eventTrackers.js'; +import {setRequestExtPrebidPageViewIds} from './pageViewIds.js'; export const PBS_PROCESSORS = { [REQUEST]: { @@ -21,7 +22,11 @@ export const PBS_PROCESSORS = { extPrebidAliases: { // sets ext.prebid.aliases fn: setRequestExtPrebidAliases - } + }, + extPrebidPageViewIds: { + // sets ext.prebid.page_view_ids + fn: setRequestExtPrebidPageViewIds + }, }, [IMP]: { params: { diff --git a/libraries/permutiveUtils/index.js b/libraries/permutiveUtils/index.js new file mode 100644 index 00000000000..ac410d6d662 --- /dev/null +++ b/libraries/permutiveUtils/index.js @@ -0,0 +1,34 @@ +import { deepAccess } from '../../src/utils.js' + +export const PERMUTIVE_VENDOR_ID = 361 + +/** + * Determine if required GDPR purposes are allowed, optionally requiring vendor consent. + * @param {Object} userConsent + * @param {number[]} requiredPurposes + * @param {boolean} enforceVendorConsent + * @returns {boolean} + */ +export function hasPurposeConsent(userConsent, requiredPurposes, enforceVendorConsent) { + const gdprApplies = deepAccess(userConsent, 'gdpr.gdprApplies') + if (!gdprApplies) return true + + if (enforceVendorConsent) { + const vendorConsents = deepAccess(userConsent, 'gdpr.vendorData.vendor.consents') || {} + const vendorLegitimateInterests = deepAccess(userConsent, 'gdpr.vendorData.vendor.legitimateInterests') || {} + const purposeConsents = deepAccess(userConsent, 'gdpr.vendorData.purpose.consents') || {} + const purposeLegitimateInterests = deepAccess(userConsent, 'gdpr.vendorData.purpose.legitimateInterests') || {} + const hasVendorConsent = vendorConsents[PERMUTIVE_VENDOR_ID] === true || vendorLegitimateInterests[PERMUTIVE_VENDOR_ID] === true + + return hasVendorConsent && requiredPurposes.every((purposeId) => + purposeConsents[purposeId] === true || purposeLegitimateInterests[purposeId] === true + ) + } + + const purposeConsents = deepAccess(userConsent, 'gdpr.vendorData.publisher.consents') || {} + const purposeLegitimateInterests = deepAccess(userConsent, 'gdpr.vendorData.publisher.legitimateInterests') || {} + + return requiredPurposes.every((purposeId) => + purposeConsents[purposeId] === true || purposeLegitimateInterests[purposeId] === true + ) +} diff --git a/libraries/precisoUtils/bidUtils.js b/libraries/precisoUtils/bidUtils.js index 98ac87ad193..050f758601c 100644 --- a/libraries/precisoUtils/bidUtils.js +++ b/libraries/precisoUtils/bidUtils.js @@ -4,11 +4,12 @@ import { ajax } from '../../src/ajax.js'; // import { NATIVE } from '../../src/mediaTypes.js'; import { consentCheck, getBidFloor } from './bidUtilsCommon.js'; import { interpretNativeBid } from './bidNativeUtils.js'; +import { getTimeZone } from '../timezone/timezone.js'; export const buildRequests = (endpoint) => (validBidRequests = [], bidderRequest) => { validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); logInfo('validBidRequests1 ::' + JSON.stringify(validBidRequests)); - var city = Intl.DateTimeFormat().resolvedOptions().timeZone; + const city = getTimeZone(); let req = { id: validBidRequests[0].auctionId, imp: validBidRequests.map(slot => mapImpression(slot, bidderRequest)), diff --git a/libraries/pubmaticUtils/plugins/dynamicTimeout.js b/libraries/pubmaticUtils/plugins/dynamicTimeout.js new file mode 100644 index 00000000000..ed8cdd52e9f --- /dev/null +++ b/libraries/pubmaticUtils/plugins/dynamicTimeout.js @@ -0,0 +1,209 @@ +import { logInfo } from '../../../src/utils.js'; +import { getGlobal } from '../../../src/prebidGlobal.js'; +import { bidderTimeoutFunctions } from '../../bidderTimeoutUtils/bidderTimeoutUtils.js'; +import { shouldThrottle } from '../pubmaticUtils.js'; + +let _dynamicTimeoutConfig = null; +export const getDynamicTimeoutConfig = () => _dynamicTimeoutConfig; +export const setDynamicTimeoutConfig = (config) => { _dynamicTimeoutConfig = config; } + +export const CONSTANTS = Object.freeze({ + LOG_PRE_FIX: 'PubMatic-Dynamic-Timeout: ', + INCLUDES_VIDEOS: 'includesVideo', + NUM_AD_UNITS: 'numAdUnits', + DEVICE_TYPE: 'deviceType', + CONNECTION_SPEED: 'connectionSpeed', + DEFAULT_SKIP_RATE: 50, + DEFAULT_THRESHOLD_TIMEOUT: 500 +}); + +export const RULES_PERCENTAGE = { + [CONSTANTS.INCLUDES_VIDEOS]: { + "true": 20, // 20% of bidderTimeout + "false": 5 // 5% of bidderTimeout + }, + [CONSTANTS.NUM_AD_UNITS]: { + "1-5": 10, // 10% of bidderTimeout + "6-10": 20, // 20% of bidderTimeout + "11-15": 30 // 30% of bidderTimeout + }, + [CONSTANTS.DEVICE_TYPE]: { + "2": 5, // 5% of bidderTimeout + "4": 10, // 10% of bidderTimeout + "5": 20 // 20% of bidderTimeout + }, + [CONSTANTS.CONNECTION_SPEED]: { + "slow": 20, // 20% of bidderTimeout + "medium": 10, // 10% of bidderTimeout + "fast": 5, // 5% of bidderTimeout + "unknown": 1 // 1% of bidderTimeout + } +}; + +/** + * Initialize the dynamic timeout plugin + * @param {Object} pluginName - Plugin name + * @param {Object} configJsonManager - Configuration JSON manager object + * @returns {Promise} - Promise resolving to initialization status + */ +export async function init(pluginName, configJsonManager) { + const config = configJsonManager.getConfigByName(pluginName); + if (!config) { + logInfo(`${CONSTANTS.LOG_PRE_FIX} Dynamic Timeout configuration not found`); + return false; + } + // Set the Dynamic Timeout config + setDynamicTimeoutConfig(config); + + if (!getDynamicTimeoutConfig()?.enabled) { + logInfo(`${CONSTANTS.LOG_PRE_FIX} Dynamic Timeout configuration is disabled`); + return false; + } + return true; +} + +/** + * Process bid request by applying dynamic timeout adjustments + * @param {Object} reqBidsConfigObj - Bid request config object + * @returns {Object} - Updated bid request config object with adjusted timeout + */ +export function processBidRequest(reqBidsConfigObj) { + // Cache config to avoid multiple calls + const timeoutConfig = getDynamicTimeoutConfig(); + + // Check if request should be throttled based on skipRate + const skipRate = (timeoutConfig?.config?.skipRate !== undefined && timeoutConfig?.config?.skipRate !== null) ? timeoutConfig?.config?.skipRate : CONSTANTS.DEFAULT_SKIP_RATE; + if (shouldThrottle(skipRate)) { + logInfo(`${CONSTANTS.LOG_PRE_FIX} Dynamic timeout is skipped (skipRate: ${skipRate}%)`); + return reqBidsConfigObj; + } + + logInfo(`${CONSTANTS.LOG_PRE_FIX} Dynamic timeout is applying...`); + + // Get ad units and bidder timeout + const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; + const bidderTimeout = getBidderTimeout(reqBidsConfigObj); + + // Calculate and apply additional timeout + const rules = getRules(bidderTimeout); + const additionalTimeout = bidderTimeoutFunctions.calculateTimeoutModifier(adUnits, rules); + + reqBidsConfigObj.timeout = getFinalTimeout(bidderTimeout, additionalTimeout); + + logInfo(`${CONSTANTS.LOG_PRE_FIX} Timeout adjusted from ${bidderTimeout}ms to ${reqBidsConfigObj.timeout}ms (added ${additionalTimeout}ms)`); + return reqBidsConfigObj; +} + +/** + * Get targeting data + * @param {Array} adUnitCodes - Ad unit codes + * @param {Object} config - Module configuration + * @param {Object} userConsent - User consent data + * @param {Object} auction - Auction object + * @returns {Object} - Targeting data + */ +export function getTargeting(adUnitCodes, config, userConsent, auction) { + // Implementation for targeting data, if not applied then do nothing +} + +// Export the dynamic timeout functions +export const DynamicTimeout = { + init, + processBidRequest, + getTargeting +}; + +// Helper Functions + +export const getFinalTimeout = (bidderTimeout, additionalTimeout) => { + // Calculate the final timeout by adding bidder timeout and additional timeout + const calculatedTimeout = parseInt(bidderTimeout) + parseInt(additionalTimeout); + const thresholdTimeout = getDynamicTimeoutConfig()?.config?.thresholdTimeout || CONSTANTS.DEFAULT_THRESHOLD_TIMEOUT; + + // Handle cases where the calculated timeout might be negative or below threshold + if (calculatedTimeout < thresholdTimeout) { + // Log warning for negative or very low timeouts + if (calculatedTimeout < 0) { + logInfo(`${CONSTANTS.LOG_PRE_FIX} Warning: Negative timeout calculated (${calculatedTimeout}ms), using threshold (${thresholdTimeout}ms)`); + } else if (calculatedTimeout < thresholdTimeout) { + logInfo(`${CONSTANTS.LOG_PRE_FIX} Calculated timeout (${calculatedTimeout}ms) below threshold, using threshold (${thresholdTimeout}ms)`); + } + return thresholdTimeout; + } + + return calculatedTimeout; +} + +export const getBidderTimeout = (reqBidsConfigObj) => { + return getDynamicTimeoutConfig()?.config?.bidderTimeout + ? getDynamicTimeoutConfig()?.config?.bidderTimeout + : reqBidsConfigObj?.timeout || getGlobal()?.getConfig('bidderTimeout'); +} + +/** + * Get rules based on percentage values and bidderTimeout + * @param {number} bidderTimeout - Bidder timeout in milliseconds + * @returns {Object} - Rules with calculated millisecond values + */ +export const getRules = (bidderTimeout) => { + const timeoutConfig = getDynamicTimeoutConfig(); + + // In milliseconds - If timeout rules provided by publishers are available then return it + if (timeoutConfig?.config?.timeoutRules && Object.keys(timeoutConfig.config.timeoutRules).length > 0) { + return timeoutConfig.config.timeoutRules; + } + // In milliseconds - Check for rules in priority order, If ML model rules are available then return it + if (timeoutConfig?.data && Object.keys(timeoutConfig.data).length > 0) { + return timeoutConfig.data; + } + // In Percentage - If no rules are available then create rules from the default defined - values are in percentages + return createDynamicRules(RULES_PERCENTAGE, bidderTimeout); +} + +/** + * Creates dynamic rules based on percentage values and bidder timeout + * @param {Object} percentageRules - Rules with percentage values + * @param {number} bidderTimeout - Bidder timeout in milliseconds + * @return {Object} - Rules with calculated millisecond values + */ +export const createDynamicRules = (percentageRules, bidderTimeout) => { + // Return empty object if required parameters are missing or invalid + if (!percentageRules || typeof percentageRules !== 'object') { + logInfo(`${CONSTANTS.LOG_PRE_FIX} Invalid percentage rules provided to createDynamicRules`); + return {}; + } + + // Handle negative or zero bidderTimeout gracefully + if (!bidderTimeout || typeof bidderTimeout !== 'number' || bidderTimeout <= 0) { + logInfo(`${CONSTANTS.LOG_PRE_FIX} Invalid bidderTimeout (${bidderTimeout}ms) provided to createDynamicRules`); + return {}; + } + + // Create a new rules object with millisecond values + return Object.entries(percentageRules).reduce((dynamicRules, [category, rules]) => { + // Skip if rules is not an object + if (!rules || typeof rules !== 'object') { + logInfo(`${CONSTANTS.LOG_PRE_FIX} Skipping invalid rule category: ${category}`); + return dynamicRules; + } + + // Initialize category in the dynamic rules + dynamicRules[category] = {}; + + // Convert each percentage value to milliseconds + Object.entries(rules).forEach(([key, percentValue]) => { + // Ensure percentage value is a number and not zero + if (typeof percentValue === 'number' && percentValue !== 0) { + const calculatedTimeout = Math.floor(bidderTimeout * (percentValue / 100)); + dynamicRules[category][key] = calculatedTimeout; + + // Log warning for negative calculated timeouts + if (calculatedTimeout < 0) { + logInfo(`${CONSTANTS.LOG_PRE_FIX} Warning: Negative timeout calculated for ${category}.${key}: ${calculatedTimeout}ms`); + } + } + }); + + return dynamicRules; + }, {}); +}; diff --git a/libraries/pubmaticUtils/plugins/floorProvider.js b/libraries/pubmaticUtils/plugins/floorProvider.js new file mode 100644 index 00000000000..a6112634353 --- /dev/null +++ b/libraries/pubmaticUtils/plugins/floorProvider.js @@ -0,0 +1,167 @@ +// plugins/floorProvider.js +import { logInfo, logError, logMessage, isEmpty } from '../../../src/utils.js'; +import { getDeviceType as fetchDeviceType, getOS } from '../../userAgentUtils/index.js'; +import { getBrowserType, getCurrentTimeOfDay, getUtmValue, getDayOfWeek, getHourOfDay } from '../pubmaticUtils.js'; +import { config as conf } from '../../../src/config.js'; + +/** + * This RTD module has a dependency on the priceFloors module. + * We utilize the continueAuction function from the priceFloors module to incorporate price floors data into the current auction. + */ +import { continueAuction } from '../../../modules/priceFloors.js'; // eslint-disable-line prebid/validate-imports + +let _floorConfig = null; +export const getFloorConfig = () => _floorConfig; +export const setFloorsConfig = (config) => { _floorConfig = config; } + +let _configJsonManager = null; +export const getConfigJsonManager = () => _configJsonManager; +export const setConfigJsonManager = (configJsonManager) => { _configJsonManager = configJsonManager; } + +export const CONSTANTS = Object.freeze({ + LOG_PRE_FIX: 'PubMatic-Floor-Provider: ' +}); + +/** + * Initialize the floor provider + * @param {Object} pluginName - Plugin name + * @param {Object} configJsonManager - Configuration JSON manager object + * @returns {Promise} - Promise resolving to initialization status + */ +export async function init(pluginName, configJsonManager) { + // Process floor-specific configuration + const config = configJsonManager.getConfigByName(pluginName); + if (!config) { + logInfo(`${CONSTANTS.LOG_PRE_FIX} Floor configuration not found`); + return false; + } + setFloorsConfig(config); + + if (!getFloorConfig()?.enabled) { + logInfo(`${CONSTANTS.LOG_PRE_FIX} Floor configuration is disabled`); + return false; + } + + setConfigJsonManager(configJsonManager); + try { + conf.setConfig(prepareFloorsConfig()); + logMessage(`${CONSTANTS.LOG_PRE_FIX} dynamicFloors config set successfully`); + } catch (error) { + logError(`${CONSTANTS.LOG_PRE_FIX} Error setting dynamicFloors config: ${error}`); + } + + logInfo(`${CONSTANTS.LOG_PRE_FIX} Floor configuration loaded`); + + return true; +} + +/** + * Process bid request + * @param {Object} reqBidsConfigObj - Bid request config object + * @returns {Object} - Updated bid request config object + */ +export function processBidRequest(reqBidsConfigObj) { + try { + const hookConfig = { + reqBidsConfigObj, + context: null, // Removed 'this' as it's not applicable in function-based implementation + nextFn: () => true, + haveExited: false, + timer: null + }; + + // Apply floor configuration + continueAuction(hookConfig); + logInfo(`${CONSTANTS.LOG_PRE_FIX} Applied floor configuration to auction`); + + return reqBidsConfigObj; + } catch (error) { + logError(`${CONSTANTS.LOG_PRE_FIX} Error applying floor configuration: ${error}`); + return reqBidsConfigObj; + } +} + +/** + * Get targeting data + * @param {Array} adUnitCodes - Ad unit codes + * @param {Object} config - Module configuration + * @param {Object} userConsent - User consent data + * @param {Object} auction - Auction object + * @returns {Object} - Targeting data + */ +export function getTargeting(adUnitCodes, config, userConsent, auction) { + // Implementation for targeting data, if not applied then do nothing +} + +// Export the floor provider functions +export const FloorProvider = { + init, + processBidRequest, + getTargeting +}; + +// Helper Functions + +export const defaultValueTemplate = { + currency: 'USD', + skipRate: 0, + schema: { + fields: ['mediaType', 'size'] + } +}; + +// Getter Functions +export const getTimeOfDay = () => getCurrentTimeOfDay(); +export const getBrowser = () => getBrowserType(); +export const getOs = () => getOS().toString(); +export const getDeviceType = () => fetchDeviceType().toString(); +export const getCountry = () => getConfigJsonManager().country; +export const getBidder = (request) => request?.bidder; +export const getUtm = () => getUtmValue(); +export const getDOW = () => getDayOfWeek(); +export const getHOD = () => getHourOfDay(); + +export const prepareFloorsConfig = () => { + if (!getFloorConfig()?.enabled || !getFloorConfig()?.config) { + return undefined; + } + + // Floor configs from adunit / setconfig + const defaultFloorConfig = conf.getConfig('floors') ?? {}; + if (defaultFloorConfig?.endpoint) { + delete defaultFloorConfig.endpoint; + } + + let ymUiConfig = { ...getFloorConfig().config }; + + // default values provided by publisher on YM UI + const defaultValues = ymUiConfig.defaultValues ?? {}; + // If floorsData is not present or is an empty object, use default values + const ymFloorsData = isEmpty(getFloorConfig().data) + ? { ...defaultValueTemplate, values: { ...defaultValues } } + : getFloorConfig().data; + + delete ymUiConfig.defaultValues; + // If skiprate is provided in configs, overwrite the value in ymFloorsData + (ymUiConfig.skipRate !== undefined) && (ymFloorsData.skipRate = ymUiConfig.skipRate); + + // merge default configs from page, configs + return { + floors: { + ...defaultFloorConfig, + ...ymUiConfig, + data: ymFloorsData, + additionalSchemaFields: { + deviceType: getDeviceType, + timeOfDay: getTimeOfDay, + browser: getBrowser, + os: getOs, + utm: getUtm, + country: getCountry, + bidder: getBidder, + dayOfWeek: getDOW, + hourOfDay: getHOD + }, + }, + }; +}; diff --git a/libraries/pubmaticUtils/plugins/pluginManager.js b/libraries/pubmaticUtils/plugins/pluginManager.js new file mode 100644 index 00000000000..d9f955a9fe2 --- /dev/null +++ b/libraries/pubmaticUtils/plugins/pluginManager.js @@ -0,0 +1,106 @@ +import { logInfo, logWarn, logError } from "../../../src/utils.js"; + +// pluginManager.js +export const plugins = new Map(); +export const CONSTANTS = Object.freeze({ + LOG_PRE_FIX: 'PubMatic-Plugin-Manager: ' +}); + +/** + * Initialize the plugin manager with constants + * @returns {Object} - Plugin manager functions + */ +export const PluginManager = () => ({ + register, + initialize, + executeHook +}); + +/** + * Register a plugin with the plugin manager + * @param {string} name - Plugin name + * @param {Object} plugin - Plugin object + * @returns {Object} - Plugin manager functions + */ +const register = (name, plugin) => { + if (plugins.has(name)) { + logWarn(`${CONSTANTS.LOG_PRE_FIX} Plugin ${name} already registered`); + return; + } + plugins.set(name, plugin); +}; + +/** + * Unregister a plugin from the plugin manager + * @param {string} name - Plugin name + * @returns {Object} - Plugin manager functions + */ +const unregister = (name) => { + if (plugins.has(name)) { + logInfo(`${CONSTANTS.LOG_PRE_FIX} Unregistering plugin ${name}`); + plugins.delete(name); + } +}; + +/** + * Initialize all registered plugins with their specific config + * @param {Object} configJsonManager - Configuration JSON manager object + * @returns {Promise} - Promise resolving when all plugins are initialized + */ +const initialize = async (configJsonManager) => { + const initPromises = []; + + // Initialize each plugin with its specific config + for (const [name, plugin] of plugins.entries()) { + if (plugin.init) { + const initialized = await plugin.init(name, configJsonManager); + if (!initialized) { + unregister(name); + } + initPromises.push(initialized); + } + } + + return Promise.all(initPromises); +}; + +/** + * Execute a hook on all registered plugins synchronously + * @param {string} hookName - Name of the hook to execute + * @param {...any} args - Arguments to pass to the hook + * @returns {Object} - Object containing merged results from all plugins + */ +const executeHook = (hookName, ...args) => { + // Cache results to avoid repeated processing + const results = {}; + + try { + // Get all plugins that have the specified hook method + const pluginsWithHook = Array.from(plugins.entries()) + .filter(([_, plugin]) => typeof plugin[hookName] === 'function'); + + // Process each plugin synchronously + for (const [name, plugin] of pluginsWithHook) { + try { + // Call the plugin's hook method synchronously + const result = plugin[hookName](...args); + + // Skip null/undefined results + if (result === null || result === undefined) { + continue; + } + + // If result is an object, merge it + if (typeof result === 'object') { + Object.assign(results, result); + } + } catch (error) { + logError(`${CONSTANTS.LOG_PRE_FIX} Error executing hook ${hookName} in plugin ${name}: ${error.message}`); + } + } + } catch (error) { + logError(`${CONSTANTS.LOG_PRE_FIX} Error in executeHookSync: ${error.message}`); + } + + return results; +}; diff --git a/libraries/pubmaticUtils/plugins/unifiedPricingRule.js b/libraries/pubmaticUtils/plugins/unifiedPricingRule.js new file mode 100644 index 00000000000..4060af17044 --- /dev/null +++ b/libraries/pubmaticUtils/plugins/unifiedPricingRule.js @@ -0,0 +1,375 @@ +// plugins/unifiedPricingRule.js +import { logError, logInfo } from '../../../src/utils.js'; +import { getGlobal } from '../../../src/prebidGlobal.js'; +import { REJECTION_REASON } from '../../../src/constants.js'; + +const CONSTANTS = Object.freeze({ + LOG_PRE_FIX: 'PubMatic-Unified-Pricing-Rule: ', + BID_STATUS: { + NOBID: 0, + WON: 1, + FLOORED: 2 + }, + MULTIPLIERS: { + WIN: 1.0, + FLOORED: 1.0, + NOBID: 1.0 + }, + TARGETING_KEYS: { + PM_YM_FLRS: 'pm_ym_flrs', // Whether RTD floor was applied + PM_YM_FLRV: 'pm_ym_flrv', // Final floor value (after applying multiplier) + PM_YM_BID_S: 'pm_ym_bid_s' // Bid status (0: No bid, 1: Won, 2: Floored) + } +}); +export const getProfileConfigs = () => getConfigJsonManager()?.getYMConfig(); + +let _configJsonManager = null; +export const getConfigJsonManager = () => _configJsonManager; +export const setConfigJsonManager = (configJsonManager) => { _configJsonManager = configJsonManager; } + +/** + * Initialize the floor provider + * @param {Object} pluginName - Plugin name + * @param {Object} configJsonManager - Configuration JSON manager object + * @returns {Promise} - Promise resolving to initialization status + */ +export async function init(pluginName, configJsonManager) { + setConfigJsonManager(configJsonManager); + return true; +} + +/** + * Process bid request + * @param {Object} reqBidsConfigObj - Bid request config object + * @returns {Object} - Updated bid request config object + */ +export function processBidRequest(reqBidsConfigObj) { + return reqBidsConfigObj; +} + +/** + * Get targeting data + * @param {Array} adUnitCodes - Ad unit codes + * @param {Object} config - Module configuration + * @param {Object} userConsent - User consent data + * @param {Object} auction - Auction object + * @returns {Object} - Targeting data + */ +export function getTargeting(adUnitCodes, config, userConsent, auction) { + // Access the profile configs stored globally + const profileConfigs = getProfileConfigs(); + + // Return empty object if profileConfigs is undefined or pmTargetingKeys.enabled is explicitly set to false + if (!profileConfigs || profileConfigs?.plugins?.dynamicFloors?.pmTargetingKeys?.enabled === false) { + logInfo(`${CONSTANTS.LOG_PRE_FIX} pmTargetingKeys is disabled or profileConfigs is undefined`); + return {}; + } + + // Helper to check if RTD floor is applied to a bid + const isRtdFloorApplied = bid => bid.floorData?.floorProvider === "PM" && !bid.floorData.skipped; + + // Check if any bid has RTD floor applied + const hasRtdFloorAppliedBid = + auction?.adUnits?.some(adUnit => adUnit.bids?.some(isRtdFloorApplied)) || + auction?.bidsReceived?.some(isRtdFloorApplied); + + // Only log when RTD floor is applied + if (hasRtdFloorAppliedBid) { + logInfo(CONSTANTS.LOG_PRE_FIX, 'Setting targeting via getTargetingData:'); + } + + // Process each ad unit code + const targeting = {}; + + adUnitCodes.forEach(code => { + targeting[code] = {}; + + // For non-RTD floor applied cases, only set pm_ym_flrs to 0 + if (!hasRtdFloorAppliedBid) { + targeting[code][CONSTANTS.TARGETING_KEYS.PM_YM_FLRS] = 0; + return; + } + + // Find bids and determine status for RTD floor applied cases + const bidsForAdUnit = findBidsForAdUnit(auction, code); + const rejectedBidsForAdUnit = findRejectedBidsForAdUnit(auction, code); + const rejectedFloorBid = findRejectedFloorBid(rejectedBidsForAdUnit); + const winningBid = findWinningBid(code); + + // Determine bid status and values + const { bidStatus, baseValue, multiplier } = determineBidStatusAndValues( + winningBid, + rejectedFloorBid, + bidsForAdUnit, + auction, + code + ); + + // Set all targeting keys + targeting[code][CONSTANTS.TARGETING_KEYS.PM_YM_FLRS] = 1; + targeting[code][CONSTANTS.TARGETING_KEYS.PM_YM_FLRV] = (baseValue * multiplier).toFixed(2); + targeting[code][CONSTANTS.TARGETING_KEYS.PM_YM_BID_S] = bidStatus; + }); + + return targeting; +} + +// Export the floor provider functions +export const UnifiedPricingRule = { + init, + processBidRequest, + getTargeting +}; + +// Find all bids for a specific ad unit +function findBidsForAdUnit(auction, code) { + return auction?.bidsReceived?.filter(bid => bid.adUnitCode === code) || []; +} + +// Find rejected bids for a specific ad unit +function findRejectedBidsForAdUnit(auction, code) { + if (!auction?.bidsRejected) return []; + + // If bidsRejected is an array + if (Array.isArray(auction.bidsRejected)) { + return auction.bidsRejected.filter(bid => bid.adUnitCode === code); + } + + // If bidsRejected is an object mapping bidders to their rejected bids + if (typeof auction.bidsRejected === 'object') { + return Object.values(auction.bidsRejected) + .filter(Array.isArray) + .flatMap(bidderBids => bidderBids.filter(bid => bid.adUnitCode === code)); + } + + return []; +} + +// Find a rejected bid due to price floor +function findRejectedFloorBid(rejectedBids) { + return rejectedBids.find(bid => { + return bid.rejectionReason === REJECTION_REASON.FLOOR_NOT_MET && + (bid.floorData?.floorValue && bid.cpm < bid.floorData.floorValue); + }); +} + +// Find the winning or highest bid for an ad unit +function findWinningBid(adUnitCode) { + try { + const pbjs = getGlobal(); + if (!pbjs?.getHighestCpmBids) return null; + + const highestCpmBids = pbjs.getHighestCpmBids(adUnitCode); + if (!highestCpmBids?.length) { + logInfo(CONSTANTS.LOG_PRE_FIX, `No highest CPM bids found for ad unit: ${adUnitCode}`); + return null; + } + + const highestCpmBid = highestCpmBids[0]; + logInfo(CONSTANTS.LOG_PRE_FIX, `Found highest CPM bid using pbjs.getHighestCpmBids() for ad unit: ${adUnitCode}, CPM: ${highestCpmBid.cpm}`); + return highestCpmBid; + } catch (error) { + logError(CONSTANTS.LOG_PRE_FIX, `Error finding highest CPM bid: ${error}`); + return null; + } +} + +// Find floor value from bidder requests +function findFloorValueFromBidderRequests(auction, code) { + if (!auction?.bidderRequests?.length) return 0; + + // Find all bids in bidder requests for this ad unit + const bidsFromRequests = auction.bidderRequests + .flatMap(request => request.bids || []) + .filter(bid => bid.adUnitCode === code); + + if (!bidsFromRequests.length) { + logInfo(CONSTANTS.LOG_PRE_FIX, `No bids found for ad unit: ${code}`); + return 0; + } + + const bidWithGetFloor = bidsFromRequests.find(bid => bid.getFloor); + if (!bidWithGetFloor) { + logInfo(CONSTANTS.LOG_PRE_FIX, `No bid with getFloor method found for ad unit: ${code}`); + return 0; + } + + // Helper function to extract sizes with their media types from a source object + const extractSizes = (source) => { + if (!source) return null; + + const result = []; + + // Extract banner sizes + if (source.mediaTypes?.banner?.sizes) { + source.mediaTypes.banner.sizes.forEach(size => { + result.push({ + size, + mediaType: 'banner' + }); + }); + } + + // Extract video sizes + if (source.mediaTypes?.video?.playerSize) { + const playerSize = source.mediaTypes.video.playerSize; + // Handle both formats: [[w, h]] and [w, h] + const videoSizes = Array.isArray(playerSize[0]) ? playerSize : [playerSize]; + + videoSizes.forEach(size => { + result.push({ + size, + mediaType: 'video' + }); + }); + } + + // Use general sizes as fallback if no specific media types found + if (result.length === 0 && source.sizes) { + source.sizes.forEach(size => { + result.push({ + size, + mediaType: 'banner' // Default to banner for general sizes + }); + }); + } + + return result.length > 0 ? result : null; + }; + + // Try to get sizes from different sources in order of preference + const adUnit = auction.adUnits?.find(unit => unit.code === code); + let sizes = extractSizes(adUnit) || extractSizes(bidWithGetFloor); + + // Handle fallback to wildcard size if no sizes found + if (!sizes) { + sizes = [{ size: ['*', '*'], mediaType: 'banner' }]; + logInfo(CONSTANTS.LOG_PRE_FIX, `No sizes found, using wildcard size for ad unit: ${code}`); + } + + // Try to get floor values for each size + let minFloor = -1; + + for (const sizeObj of sizes) { + // Extract size and mediaType from the object + const { size, mediaType } = sizeObj; + + // Call getFloor with the appropriate media type + const floorInfo = bidWithGetFloor.getFloor({ + currency: 'USD', // Default currency + mediaType: mediaType, // Use the media type we extracted + size: size + }); + + if (floorInfo?.floor && !isNaN(parseFloat(floorInfo.floor))) { + const floorValue = parseFloat(floorInfo.floor); + logInfo(CONSTANTS.LOG_PRE_FIX, `Floor value for ${mediaType} size ${size}: ${floorValue}`); + + // Update minimum floor value + minFloor = minFloor === -1 ? floorValue : Math.min(minFloor, floorValue); + } + } + + if (minFloor !== -1) { + logInfo(CONSTANTS.LOG_PRE_FIX, `Calculated minimum floor value ${minFloor} for ad unit: ${code}`); + return minFloor; + } + + logInfo(CONSTANTS.LOG_PRE_FIX, `No floor data found for ad unit: ${code}`); + return 0; +} + +// Select multiplier based on priority order: floors.json → config.json → default +function selectMultiplier(multiplierKey, profileConfigs) { + // Define sources in priority order + const multiplierSources = [ + { + name: 'config.json', + getValue: () => { + const configPath = profileConfigs?.plugins?.dynamicFloors?.pmTargetingKeys?.multiplier; + const lowerKey = multiplierKey.toLowerCase(); + return configPath && lowerKey in configPath ? configPath[lowerKey] : null; + } + }, + { + name: 'floor.json', + getValue: () => { + const configPath = profileConfigs?.plugins?.dynamicFloors?.data?.multiplier; + const lowerKey = multiplierKey.toLowerCase(); + return configPath && lowerKey in configPath ? configPath[lowerKey] : null; + } + }, + { + name: 'default', + getValue: () => CONSTANTS.MULTIPLIERS[multiplierKey] + } + ]; + + // Find the first source with a non-null value + for (const source of multiplierSources) { + const value = source.getValue(); + if (value != null) { + return { value, source: source.name }; + } + } + + // Fallback (shouldn't happen due to default source) + return { value: CONSTANTS.MULTIPLIERS[multiplierKey], source: 'default' }; +} + +// Identify winning bid scenario and return scenario data +function handleWinningBidScenario(winningBid, code) { + return { + scenario: 'winning', + bidStatus: CONSTANTS.BID_STATUS.WON, + baseValue: winningBid.cpm, + multiplierKey: 'WIN', + logMessage: `Bid won for ad unit: ${code}, CPM: ${winningBid.cpm}` + }; +} + +// Identify rejected floor bid scenario and return scenario data +function handleRejectedFloorBidScenario(rejectedFloorBid, code) { + const baseValue = rejectedFloorBid.floorData?.floorValue || 0; + return { + scenario: 'rejected', + bidStatus: CONSTANTS.BID_STATUS.FLOORED, + baseValue, + multiplierKey: 'FLOORED', + logMessage: `Bid rejected due to price floor for ad unit: ${code}, Floor value: ${baseValue}, Bid CPM: ${rejectedFloorBid.cpm}` + }; +} + +// Identify no bid scenario and return scenario data +function handleNoBidScenario(auction, code) { + const baseValue = findFloorValueFromBidderRequests(auction, code); + return { + scenario: 'nobid', + bidStatus: CONSTANTS.BID_STATUS.NOBID, + baseValue, + multiplierKey: 'NOBID', + logMessage: `No bids for ad unit: ${code}, Floor value: ${baseValue}` + }; +} + +// Determine which scenario applies based on bid conditions +function determineScenario(winningBid, rejectedFloorBid, bidsForAdUnit, auction, code) { + return winningBid ? handleWinningBidScenario(winningBid, code) + : rejectedFloorBid ? handleRejectedFloorBidScenario(rejectedFloorBid, code) + : handleNoBidScenario(auction, code); +} + +// Main function that determines bid status and calculates values +function determineBidStatusAndValues(winningBid, rejectedFloorBid, bidsForAdUnit, auction, code) { + const profileConfigs = getProfileConfigs(); + + // Determine the scenario based on bid conditions + const { bidStatus, baseValue, multiplierKey, logMessage } = + determineScenario(winningBid, rejectedFloorBid, bidsForAdUnit, auction, code); + + // Select the appropriate multiplier + const { value: multiplier, source } = selectMultiplier(multiplierKey, profileConfigs); + logInfo(CONSTANTS.LOG_PRE_FIX, logMessage + ` (Using ${source} multiplier: ${multiplier})`); + + return { bidStatus, baseValue, multiplier }; +} diff --git a/libraries/pubmaticUtils/pubmaticUtils.js b/libraries/pubmaticUtils/pubmaticUtils.js new file mode 100644 index 00000000000..a7e23f1ee76 --- /dev/null +++ b/libraries/pubmaticUtils/pubmaticUtils.js @@ -0,0 +1,86 @@ +import { getLowEntropySUA } from '../../src/fpd/sua.js'; + +const CONSTANTS = Object.freeze({ + TIME_OF_DAY_VALUES: { + MORNING: 'morning', + AFTERNOON: 'afternoon', + EVENING: 'evening', + NIGHT: 'night' + }, + UTM: 'utm_', + UTM_VALUES: { + TRUE: '1', + FALSE: '0' + }, +}); + +const BROWSER_REGEX_MAP = [ + { regex: /\b(?:crios)\/([\w.]+)/i, id: 1 }, // Chrome for iOS + { regex: /(edg|edge)(?:e|ios|a)?(?:\/([\w.]+))?/i, id: 2 }, // Edge + { regex: /(opera|opr)(?:.+version\/|\/|\s+)([\w.]+)/i, id: 3 }, // Opera + { regex: /(?:ms|\()(ie) ([\w.]+)|(?:trident\/[\w.]+)/i, id: 4 }, // Internet Explorer + { regex: /fxios\/([-\w.]+)/i, id: 5 }, // Firefox for iOS + { regex: /((?:fban\/fbios|fb_iab\/fb4a)(?!.+fbav)|;fbav\/([\w.]+);)/i, id: 6 }, // Facebook In-App Browser + { regex: / wv\).+(chrome)\/([\w.]+)/i, id: 7 }, // Chrome WebView + { regex: /droid.+ version\/([\w.]+)\b.+(?:mobile safari|safari)/i, id: 8 }, // Android Browser + { regex: /(chrome|crios)(?:\/v?([\w.]+))?\b/i, id: 9 }, // Chrome + { regex: /version\/([\w.,]+) .*mobile\/\w+ (safari)/i, id: 10 }, // Safari Mobile + { regex: /version\/([\w.,]+) .*(mobile ?safari|safari)/i, id: 11 }, // Safari + { regex: /(firefox)\/([\w.]+)/i, id: 12 } // Firefox +]; + +export const getBrowserType = () => { + const brandName = getLowEntropySUA()?.browsers + ?.map(b => b.brand.toLowerCase()) + .join(' ') || ''; + const browserMatch = brandName ? BROWSER_REGEX_MAP.find(({ regex }) => regex.test(brandName)) : -1; + + if (browserMatch?.id) return browserMatch.id.toString(); + + const userAgent = navigator?.userAgent; + let browserIndex = userAgent == null ? -1 : 0; + + if (userAgent) { + browserIndex = BROWSER_REGEX_MAP.find(({ regex }) => regex.test(userAgent))?.id || 0; + } + return browserIndex.toString(); +} + +export const getCurrentTimeOfDay = () => { + const currentHour = new Date().getHours(); + + return currentHour < 5 ? CONSTANTS.TIME_OF_DAY_VALUES.NIGHT + : currentHour < 12 ? CONSTANTS.TIME_OF_DAY_VALUES.MORNING + : currentHour < 17 ? CONSTANTS.TIME_OF_DAY_VALUES.AFTERNOON + : currentHour < 19 ? CONSTANTS.TIME_OF_DAY_VALUES.EVENING + : CONSTANTS.TIME_OF_DAY_VALUES.NIGHT; +} + +export const getUtmValue = () => { + const url = new URL(window.location?.href); + const urlParams = new URLSearchParams(url?.search); + return urlParams && urlParams.toString().includes(CONSTANTS.UTM) ? CONSTANTS.UTM_VALUES.TRUE : CONSTANTS.UTM_VALUES.FALSE; +} + +export const getDayOfWeek = () => { + const dayOfWeek = new Date().getDay(); + return dayOfWeek.toString(); +} + +export const getHourOfDay = () => { + const hourOfDay = new Date().getHours(); + return hourOfDay.toString(); +} + +/** + * Determines whether an action should be throttled based on a given percentage. + * + * @param {number} skipRate - The percentage rate at which throttling will be applied (0-100). + * @param {number} maxRandomValue - The upper bound for generating a random number (default is 100). + * @returns {boolean} - Returns true if the action should be throttled, false otherwise. + */ +export const shouldThrottle = (skipRate, maxRandomValue = 100) => { + // Determine throttling based on the throttle rate and a random value + const rate = skipRate ?? maxRandomValue; + return Math.floor(Math.random() * maxRandomValue) < rate; +}; diff --git a/libraries/smartyadsUtils/getAdUrlByRegion.js b/libraries/smartyadsUtils/getAdUrlByRegion.js index cad9055f671..8465d3e1584 100644 --- a/libraries/smartyadsUtils/getAdUrlByRegion.js +++ b/libraries/smartyadsUtils/getAdUrlByRegion.js @@ -1,3 +1,5 @@ +import { getTimeZone } from '../timezone/timezone.js'; + const adUrls = { US_EAST: 'https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', EU: 'https://n2.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', @@ -10,21 +12,16 @@ export function getAdUrlByRegion(bid) { if (bid.params.region && adUrls[bid.params.region]) { adUrl = adUrls[bid.params.region]; } else { - try { - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const region = timezone.split('/')[0]; + const region = getTimeZone().split('/')[0]; - switch (region) { - case 'Europe': - adUrl = adUrls['EU']; - break; - case 'Asia': - adUrl = adUrls['SGP']; - break; - default: adUrl = adUrls['US_EAST']; - } - } catch (err) { - adUrl = adUrls['US_EAST']; + switch (region) { + case 'Europe': + adUrl = adUrls['EU']; + break; + case 'Asia': + adUrl = adUrls['SGP']; + break; + default: adUrl = adUrls['US_EAST']; } } diff --git a/libraries/timezone/timezone.js b/libraries/timezone/timezone.js new file mode 100644 index 00000000000..e4ef39f28ef --- /dev/null +++ b/libraries/timezone/timezone.js @@ -0,0 +1,3 @@ +export function getTimeZone() { + return Intl.DateTimeFormat().resolvedOptions().timeZone; +} diff --git a/libraries/uid2IdSystemShared/uid2IdSystem_shared.js b/libraries/uid2IdSystemShared/uid2IdSystem_shared.js index 2230fb4efb0..70a607a0f8f 100644 --- a/libraries/uid2IdSystemShared/uid2IdSystem_shared.js +++ b/libraries/uid2IdSystemShared/uid2IdSystem_shared.js @@ -771,12 +771,6 @@ export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { }).catch((e) => { logError('error refreshing token: ', e); }); } }; } - // If should refresh (but don't need to), refresh in the background. - if (Date.now() > newestAvailableToken.refresh_from) { - logInfo(`Refreshing token in background with low priority.`); - refreshTokenAndStore(config.apiBaseUrl, newestAvailableToken, config.clientId, storageManager, logInfo, _logWarn) - .catch((e) => { logError('error refreshing token in background: ', e); }); - } const tokens = { originalToken: suppliedToken ?? storedTokens?.originalToken, latestToken: newestAvailableToken, @@ -785,6 +779,23 @@ export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { tokens.originalIdentity = storedTokens?.originalIdentity; } storageManager.storeValue(tokens); + + // If should refresh (but don't need to), refresh in the background. + // Return both immediate id and callback so idObj gets updated when refresh completes. + if (Date.now() > newestAvailableToken.refresh_from) { + logInfo(`Refreshing token in background with low priority.`); + const refreshPromise = refreshTokenAndStore(config.apiBaseUrl, newestAvailableToken, config.clientId, storageManager, logInfo, _logWarn); + return { + id: tokens, + callback: (cb) => { + refreshPromise.then((refreshedTokens) => { + logInfo('Background token refresh completed, updating ID.', refreshedTokens); + cb(refreshedTokens); + }).catch((e) => { logError('error refreshing token in background: ', e); }); + } + }; + } + return { id: tokens }; } diff --git a/libraries/vidazooUtils/bidderUtils.js b/libraries/vidazooUtils/bidderUtils.js index 08432936858..fa2cea1b6fa 100644 --- a/libraries/vidazooUtils/bidderUtils.js +++ b/libraries/vidazooUtils/bidderUtils.js @@ -6,7 +6,8 @@ import { parseSizesInput, parseUrl, triggerPixel, - uniques + uniques, + getWinDimensions } from '../../src/utils.js'; import {chunk} from '../chunk/chunk.js'; import {CURRENCY, DEAL_ID_EXPIRY, SESSION_ID_KEY, TTL_SECONDS, UNIQUE_DEAL_ID_EXPIRY} from './constants.js'; @@ -280,7 +281,7 @@ export function buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidder uniqueDealId: uniqueDealId, bidderVersion: bidderVersion, prebidVersion: '$prebid.version$', - res: `${screen.width}x${screen.height}`, + res: getScreenResolution(), schain: schain, mediaTypes: mediaTypes, isStorageAllowed: isStorageAllowed, @@ -374,6 +375,15 @@ export function buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidder return data; } +function getScreenResolution() { + const dimensions = getWinDimensions(); + const width = dimensions?.screen?.width; + const height = dimensions?.screen?.height; + if (width != null && height != null) { + return `${width}x${height}` + } +} + export function createInterpretResponseFn(bidderCode, allowSingleRequest) { return function interpretResponse(serverResponse, request) { if (!serverResponse || !serverResponse.body) { diff --git a/metadata/modules.json b/metadata/modules.json index bbca2363d99..1ff79e4c63b 100644 --- a/metadata/modules.json +++ b/metadata/modules.json @@ -512,6 +512,13 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "appmonsta", + "aliasOf": "adkernel", + "gvlid": 1283, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "admaru", @@ -1016,6 +1023,13 @@ "gvlid": 1169, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "allegro", + "aliasOf": null, + "gvlid": 1493, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "alvads", @@ -1709,6 +1723,20 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "clickio", + "aliasOf": null, + "gvlid": 1500, + "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "clydo", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "codefuel", @@ -1912,6 +1940,20 @@ "gvlid": 573, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "das", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "ringieraxelspringer", + "aliasOf": "das", + "gvlid": null, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "datablocks", @@ -2388,13 +2430,6 @@ "gvlid": null, "disclosureURL": null }, - { - "componentType": "bidder", - "componentName": "trustx", - "aliasOf": "grid", - "gvlid": null, - "disclosureURL": null - }, { "componentType": "bidder", "componentName": "growads", @@ -2836,6 +2871,20 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "performist", + "aliasOf": "limelightDigital", + "gvlid": null, + "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "oveeo", + "aliasOf": "limelightDigital", + "gvlid": null, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "livewrapped", @@ -3158,6 +3207,13 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "mycodemedia", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "mytarget", @@ -3328,16 +3384,23 @@ }, { "componentType": "bidder", - "componentName": "revnew", + "componentName": "pubxai", "aliasOf": "nexx360", - "gvlid": 1468, + "gvlid": 1485, "disclosureURL": null }, { "componentType": "bidder", - "componentName": "pubxai", + "componentName": "ybidder", "aliasOf": "nexx360", - "gvlid": 1485, + "gvlid": 1253, + "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "netads", + "aliasOf": "nexx360", + "gvlid": 965, "disclosureURL": null }, { @@ -3690,6 +3753,13 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "publicgood", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "publir", @@ -3893,6 +3963,13 @@ "gvlid": 203, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "revnew", + "aliasOf": null, + "gvlid": 1468, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "rhythmone", @@ -3914,13 +3991,6 @@ "gvlid": 108, "disclosureURL": null }, - { - "componentType": "bidder", - "componentName": "ringieraxelspringer", - "aliasOf": null, - "gvlid": null, - "disclosureURL": null - }, { "componentType": "bidder", "componentName": "rise", @@ -4236,6 +4306,27 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "amcom", + "aliasOf": "smarthub", + "gvlid": null, + "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "adastra", + "aliasOf": "smarthub", + "gvlid": null, + "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "radiantfusion", + "aliasOf": "smarthub", + "gvlid": null, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "smartico", @@ -4495,6 +4586,13 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "topon", + "aliasOf": null, + "gvlid": 1305, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "tpmn", @@ -4530,6 +4628,13 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "trustx", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "ttd", @@ -5249,7 +5354,7 @@ "componentType": "rtd", "componentName": "permutive", "gvlid": null, - "disclosureURL": null + "disclosureURL": "https://assets.permutive.app/tcf/tcf.json" }, { "componentType": "rtd", @@ -5654,7 +5759,7 @@ "componentType": "userId", "componentName": "permutiveIdentityManagerId", "gvlid": null, - "disclosureURL": null, + "disclosureURL": "https://assets.permutive.app/tcf/tcf.json", "aliasOf": null }, { diff --git a/metadata/modules/33acrossBidAdapter.json b/metadata/modules/33acrossBidAdapter.json index 05ea618fdee..b508d492689 100644 --- a/metadata/modules/33acrossBidAdapter.json +++ b/metadata/modules/33acrossBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://platform.33across.com/disclosures.json": { - "timestamp": "2025-11-10T11:41:10.873Z", + "timestamp": "2026-01-15T16:36:37.468Z", "disclosures": [] } }, diff --git a/metadata/modules/33acrossIdSystem.json b/metadata/modules/33acrossIdSystem.json index 8d13cbe209f..c00e1e452a1 100644 --- a/metadata/modules/33acrossIdSystem.json +++ b/metadata/modules/33acrossIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://platform.33across.com/disclosures.json": { - "timestamp": "2025-11-10T11:41:10.975Z", + "timestamp": "2026-01-15T16:36:37.575Z", "disclosures": [] } }, diff --git a/metadata/modules/acuityadsBidAdapter.json b/metadata/modules/acuityadsBidAdapter.json index c071fc720e2..4d6a5fda3e8 100644 --- a/metadata/modules/acuityadsBidAdapter.json +++ b/metadata/modules/acuityadsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://privacy.acuityads.com/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:10.977Z", + "timestamp": "2026-01-15T16:36:37.577Z", "disclosures": [] } }, diff --git a/metadata/modules/adagioBidAdapter.json b/metadata/modules/adagioBidAdapter.json index 906df47afad..de31aded310 100644 --- a/metadata/modules/adagioBidAdapter.json +++ b/metadata/modules/adagioBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adagio.io/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:11.014Z", + "timestamp": "2026-01-15T16:36:37.610Z", "disclosures": [] } }, diff --git a/metadata/modules/adagioRtdProvider.json b/metadata/modules/adagioRtdProvider.json index e3c9b439e92..aceec16684b 100644 --- a/metadata/modules/adagioRtdProvider.json +++ b/metadata/modules/adagioRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adagio.io/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:11.073Z", + "timestamp": "2026-01-15T16:36:37.832Z", "disclosures": [] } }, diff --git a/metadata/modules/adbroBidAdapter.json b/metadata/modules/adbroBidAdapter.json index 27ebdb1565f..601d4d75eb0 100644 --- a/metadata/modules/adbroBidAdapter.json +++ b/metadata/modules/adbroBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tag.adbro.me/privacy/devicestorage.json": { - "timestamp": "2025-11-10T11:41:11.073Z", + "timestamp": "2026-01-15T16:36:37.832Z", "disclosures": [] } }, diff --git a/metadata/modules/addefendBidAdapter.json b/metadata/modules/addefendBidAdapter.json index 2a2de2b086c..156b0a7b850 100644 --- a/metadata/modules/addefendBidAdapter.json +++ b/metadata/modules/addefendBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.addefend.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:11.231Z", + "timestamp": "2026-01-15T16:36:38.129Z", "disclosures": [] } }, diff --git a/metadata/modules/adfBidAdapter.json b/metadata/modules/adfBidAdapter.json index 4d5867af9d2..430e0ee2005 100644 --- a/metadata/modules/adfBidAdapter.json +++ b/metadata/modules/adfBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://site.adform.com/assets/devicestorage.json": { - "timestamp": "2025-11-10T11:41:11.878Z", + "timestamp": "2026-01-15T16:36:38.754Z", "disclosures": [] } }, diff --git a/metadata/modules/adfusionBidAdapter.json b/metadata/modules/adfusionBidAdapter.json index 5ab5cdbffd2..7f8ad865d23 100644 --- a/metadata/modules/adfusionBidAdapter.json +++ b/metadata/modules/adfusionBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://spicyrtb.com/static/iab-disclosure.json": { - "timestamp": "2025-11-10T11:41:11.908Z", + "timestamp": "2026-01-15T16:36:38.755Z", "disclosures": [] } }, diff --git a/metadata/modules/adheseBidAdapter.json b/metadata/modules/adheseBidAdapter.json index a6ab9d0f94b..6a1bc353718 100644 --- a/metadata/modules/adheseBidAdapter.json +++ b/metadata/modules/adheseBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adhese.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:12.255Z", + "timestamp": "2026-01-15T16:36:39.105Z", "disclosures": [] } }, diff --git a/metadata/modules/adipoloBidAdapter.json b/metadata/modules/adipoloBidAdapter.json index 4412e00c463..c6186699e97 100644 --- a/metadata/modules/adipoloBidAdapter.json +++ b/metadata/modules/adipoloBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adipolo.com/device_storage_disclosure.json": { - "timestamp": "2025-11-10T11:41:12.517Z", + "timestamp": "2026-01-15T16:36:39.373Z", "disclosures": [] } }, diff --git a/metadata/modules/adkernelAdnBidAdapter.json b/metadata/modules/adkernelAdnBidAdapter.json index f377f494e85..f05baa21a71 100644 --- a/metadata/modules/adkernelAdnBidAdapter.json +++ b/metadata/modules/adkernelAdnBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://static.adkernel.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:12.640Z", + "timestamp": "2026-01-15T16:36:39.507Z", "disclosures": [ { "identifier": "adk_rtb_conv_id", diff --git a/metadata/modules/adkernelBidAdapter.json b/metadata/modules/adkernelBidAdapter.json index e6f5a7c877b..a0063f22aa1 100644 --- a/metadata/modules/adkernelBidAdapter.json +++ b/metadata/modules/adkernelBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://static.adkernel.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:12.669Z", + "timestamp": "2026-01-15T16:36:39.547Z", "disclosures": [ { "identifier": "adk_rtb_conv_id", @@ -17,15 +17,19 @@ ] }, "https://data.converge-digital.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:12.669Z", + "timestamp": "2026-01-15T16:36:39.547Z", "disclosures": [] }, "https://spinx.biz/tcf-spinx.json": { - "timestamp": "2025-11-10T11:41:12.711Z", + "timestamp": "2026-01-15T16:36:39.586Z", "disclosures": [] }, "https://gdpr.memob.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:13.448Z", + "timestamp": "2026-01-15T16:36:40.358Z", + "disclosures": [] + }, + "https://appmonsta.ai/DeviceStorageDisclosure.json": { + "timestamp": "2026-01-15T16:36:40.374Z", "disclosures": [] } }, @@ -337,6 +341,13 @@ "aliasOf": "adkernel", "gvlid": null, "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "appmonsta", + "aliasOf": "adkernel", + "gvlid": 1283, + "disclosureURL": "https://appmonsta.ai/DeviceStorageDisclosure.json" } ] } \ No newline at end of file diff --git a/metadata/modules/admaticBidAdapter.json b/metadata/modules/admaticBidAdapter.json index a6c3063ecba..28aa7f6f8fe 100644 --- a/metadata/modules/admaticBidAdapter.json +++ b/metadata/modules/admaticBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://static.admatic.de/iab-europe/tcfv2/disclosure.json": { - "timestamp": "2025-11-10T11:41:13.921Z", + "timestamp": "2026-01-15T16:36:50.419Z", "disclosures": [ { "identifier": "px_pbjs", @@ -12,7 +12,7 @@ ] }, "https://adtarget.com.tr/.well-known/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:13.576Z", + "timestamp": "2026-01-15T16:36:41.675Z", "disclosures": [ { "identifier": "adt_pbjs", diff --git a/metadata/modules/admixerBidAdapter.json b/metadata/modules/admixerBidAdapter.json index 3c693dfc5a6..47cf50a9bd3 100644 --- a/metadata/modules/admixerBidAdapter.json +++ b/metadata/modules/admixerBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://admixer.com/tcf.json": { - "timestamp": "2025-11-10T11:41:13.922Z", + "timestamp": "2026-01-15T16:36:50.420Z", "disclosures": [] } }, diff --git a/metadata/modules/admixerIdSystem.json b/metadata/modules/admixerIdSystem.json index 402a63412bf..a8393b4f630 100644 --- a/metadata/modules/admixerIdSystem.json +++ b/metadata/modules/admixerIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://admixer.com/tcf.json": { - "timestamp": "2025-11-10T11:41:14.298Z", + "timestamp": "2026-01-15T16:36:50.913Z", "disclosures": [] } }, diff --git a/metadata/modules/adnowBidAdapter.json b/metadata/modules/adnowBidAdapter.json index 0e1390ad14f..14189d1c55f 100644 --- a/metadata/modules/adnowBidAdapter.json +++ b/metadata/modules/adnowBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adnow.com/vdsod.json": { - "timestamp": "2025-11-10T11:41:14.298Z", + "timestamp": "2026-01-15T16:36:50.913Z", "disclosures": [ { "identifier": "SC_unique_*", diff --git a/metadata/modules/adnuntiusBidAdapter.json b/metadata/modules/adnuntiusBidAdapter.json index 7de3b1ae7d7..b48ccbded64 100644 --- a/metadata/modules/adnuntiusBidAdapter.json +++ b/metadata/modules/adnuntiusBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://delivery.adnuntius.com/.well-known/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:14.530Z", + "timestamp": "2026-01-15T16:37:14.929Z", "disclosures": [ { "identifier": "adn.metaData", diff --git a/metadata/modules/adnuntiusRtdProvider.json b/metadata/modules/adnuntiusRtdProvider.json index e8a18066eaf..c2e5c1cd7a2 100644 --- a/metadata/modules/adnuntiusRtdProvider.json +++ b/metadata/modules/adnuntiusRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://delivery.adnuntius.com/.well-known/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:14.862Z", + "timestamp": "2026-01-15T16:37:15.246Z", "disclosures": [ { "identifier": "adn.metaData", diff --git a/metadata/modules/adotBidAdapter.json b/metadata/modules/adotBidAdapter.json index 264db2c36fb..18c8489f9c7 100644 --- a/metadata/modules/adotBidAdapter.json +++ b/metadata/modules/adotBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://assets.adotmob.com/tcf/tcf.json": { - "timestamp": "2025-11-10T11:41:14.863Z", + "timestamp": "2026-01-15T16:37:15.247Z", "disclosures": [] } }, diff --git a/metadata/modules/adponeBidAdapter.json b/metadata/modules/adponeBidAdapter.json index 6e9378657da..e5b816c7d49 100644 --- a/metadata/modules/adponeBidAdapter.json +++ b/metadata/modules/adponeBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adserver.adpone.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:14.891Z", + "timestamp": "2026-01-15T16:37:15.386Z", "disclosures": [] } }, diff --git a/metadata/modules/adqueryBidAdapter.json b/metadata/modules/adqueryBidAdapter.json index c8467b15b94..a47536be831 100644 --- a/metadata/modules/adqueryBidAdapter.json +++ b/metadata/modules/adqueryBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://api.adquery.io/tcf/adQuery.json": { - "timestamp": "2025-11-10T11:41:14.919Z", + "timestamp": "2026-01-15T16:37:15.412Z", "disclosures": [] } }, diff --git a/metadata/modules/adqueryIdSystem.json b/metadata/modules/adqueryIdSystem.json index c78547386a8..c09bd93d13e 100644 --- a/metadata/modules/adqueryIdSystem.json +++ b/metadata/modules/adqueryIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://api.adquery.io/tcf/adQuery.json": { - "timestamp": "2025-11-10T11:41:15.250Z", + "timestamp": "2026-01-15T16:37:15.776Z", "disclosures": [] } }, diff --git a/metadata/modules/adrinoBidAdapter.json b/metadata/modules/adrinoBidAdapter.json index ad1179ceb20..a3c444e2024 100644 --- a/metadata/modules/adrinoBidAdapter.json +++ b/metadata/modules/adrinoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.adrino.cloud/iab/device-storage.json": { - "timestamp": "2025-11-10T11:41:15.250Z", + "timestamp": "2026-01-15T16:37:15.777Z", "disclosures": [] } }, diff --git a/metadata/modules/ads_interactiveBidAdapter.json b/metadata/modules/ads_interactiveBidAdapter.json index f9646e522b1..61dd58a2056 100644 --- a/metadata/modules/ads_interactiveBidAdapter.json +++ b/metadata/modules/ads_interactiveBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adsinteractive.com/vendor.json": { - "timestamp": "2025-11-10T11:41:15.279Z", + "timestamp": "2026-01-15T16:37:15.878Z", "disclosures": [] } }, diff --git a/metadata/modules/adtargetBidAdapter.json b/metadata/modules/adtargetBidAdapter.json index f063da51be4..61b5353fddb 100644 --- a/metadata/modules/adtargetBidAdapter.json +++ b/metadata/modules/adtargetBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adtarget.com.tr/.well-known/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:15.577Z", + "timestamp": "2026-01-15T16:37:16.186Z", "disclosures": [ { "identifier": "adt_pbjs", diff --git a/metadata/modules/adtelligentBidAdapter.json b/metadata/modules/adtelligentBidAdapter.json index 40c95ac8807..0fc34ae0a08 100644 --- a/metadata/modules/adtelligentBidAdapter.json +++ b/metadata/modules/adtelligentBidAdapter.json @@ -2,11 +2,11 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adtelligent.com/.well-known/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:15.578Z", + "timestamp": "2026-01-15T16:37:16.186Z", "disclosures": [] }, "https://www.selectmedia.asia/gdpr/devicestorage.json": { - "timestamp": "2025-11-10T11:41:15.595Z", + "timestamp": "2026-01-15T16:37:16.204Z", "disclosures": [ { "identifier": "waterFallCacheAnsKey_*", @@ -81,7 +81,7 @@ ] }, "https://orangeclickmedia.com/device_storage_disclosure.json": { - "timestamp": "2025-11-10T11:41:15.730Z", + "timestamp": "2026-01-15T16:37:16.366Z", "disclosures": [] } }, diff --git a/metadata/modules/adtelligentIdSystem.json b/metadata/modules/adtelligentIdSystem.json index 8f7a2aec1cd..74b3d26bf3e 100644 --- a/metadata/modules/adtelligentIdSystem.json +++ b/metadata/modules/adtelligentIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adtelligent.com/.well-known/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:15.779Z", + "timestamp": "2026-01-15T16:37:16.444Z", "disclosures": [] } }, diff --git a/metadata/modules/aduptechBidAdapter.json b/metadata/modules/aduptechBidAdapter.json index 3ec7a4151ca..4fdbda58179 100644 --- a/metadata/modules/aduptechBidAdapter.json +++ b/metadata/modules/aduptechBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://s.d.adup-tech.com/gdpr/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:15.780Z", + "timestamp": "2026-01-15T16:37:16.445Z", "disclosures": [] } }, diff --git a/metadata/modules/adyoulikeBidAdapter.json b/metadata/modules/adyoulikeBidAdapter.json index 0b028e27ade..0f6da67e6c2 100644 --- a/metadata/modules/adyoulikeBidAdapter.json +++ b/metadata/modules/adyoulikeBidAdapter.json @@ -2,8 +2,8 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adyoulike.com/deviceStorageDisclosureURL.json": { - "timestamp": "2025-11-10T11:41:15.797Z", - "disclosures": [] + "timestamp": "2026-01-15T16:37:16.471Z", + "disclosures": null } }, "components": [ diff --git a/metadata/modules/airgridRtdProvider.json b/metadata/modules/airgridRtdProvider.json index f4cdee236ff..7b14bbae2ac 100644 --- a/metadata/modules/airgridRtdProvider.json +++ b/metadata/modules/airgridRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.wearemiq.com/privacy-and-compliance/devicestoragedisclosures.json": { - "timestamp": "2025-11-10T11:41:16.258Z", + "timestamp": "2026-01-15T16:37:20.002Z", "disclosures": [] } }, diff --git a/metadata/modules/alkimiBidAdapter.json b/metadata/modules/alkimiBidAdapter.json index b0fa3c1bbf7..dab27980871 100644 --- a/metadata/modules/alkimiBidAdapter.json +++ b/metadata/modules/alkimiBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://d1xjh92lb8fey3.cloudfront.net/tcf/alkimi_exchange_tcf.json": { - "timestamp": "2025-11-10T11:41:16.285Z", + "timestamp": "2026-01-15T16:37:20.031Z", "disclosures": [] } }, diff --git a/metadata/modules/allegroBidAdapter.json b/metadata/modules/allegroBidAdapter.json new file mode 100644 index 00000000000..e7788f68978 --- /dev/null +++ b/metadata/modules/allegroBidAdapter.json @@ -0,0 +1,18 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": { + "https://assets.allegrostatic.com/dsp-tcf-external/device-storage.json": { + "timestamp": "2026-01-15T16:37:20.319Z", + "disclosures": [] + } + }, + "components": [ + { + "componentType": "bidder", + "componentName": "allegro", + "aliasOf": null, + "gvlid": 1493, + "disclosureURL": "https://assets.allegrostatic.com/dsp-tcf-external/device-storage.json" + } + ] +} \ No newline at end of file diff --git a/metadata/modules/amxBidAdapter.json b/metadata/modules/amxBidAdapter.json index 4273603a956..595cc4e40ff 100644 --- a/metadata/modules/amxBidAdapter.json +++ b/metadata/modules/amxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://assets.a-mo.net/tcf/device-storage.json": { - "timestamp": "2025-11-10T11:41:16.574Z", + "timestamp": "2026-01-15T16:37:20.750Z", "disclosures": [] } }, diff --git a/metadata/modules/amxIdSystem.json b/metadata/modules/amxIdSystem.json index f8c2ed5427f..d350419f8bf 100644 --- a/metadata/modules/amxIdSystem.json +++ b/metadata/modules/amxIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://assets.a-mo.net/tcf/device-storage.json": { - "timestamp": "2025-11-10T11:41:16.692Z", + "timestamp": "2026-01-15T16:37:20.794Z", "disclosures": [] } }, diff --git a/metadata/modules/aniviewBidAdapter.json b/metadata/modules/aniviewBidAdapter.json index 5d854db440b..c932ad48b30 100644 --- a/metadata/modules/aniviewBidAdapter.json +++ b/metadata/modules/aniviewBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://player.aniview.com/gdpr/gdpr.json": { - "timestamp": "2025-11-10T11:41:16.693Z", + "timestamp": "2026-01-15T16:37:20.794Z", "disclosures": [ { "identifier": "av_*", diff --git a/metadata/modules/anonymisedRtdProvider.json b/metadata/modules/anonymisedRtdProvider.json index 3943f391686..b17d38e89f2 100644 --- a/metadata/modules/anonymisedRtdProvider.json +++ b/metadata/modules/anonymisedRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://static.anonymised.io/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:17.033Z", + "timestamp": "2026-01-15T16:37:21.051Z", "disclosures": [ { "identifier": "oidc.user*", diff --git a/metadata/modules/appStockSSPBidAdapter.json b/metadata/modules/appStockSSPBidAdapter.json index d251299601b..4418fdb219e 100644 --- a/metadata/modules/appStockSSPBidAdapter.json +++ b/metadata/modules/appStockSSPBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://app-stock.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:17.051Z", + "timestamp": "2026-01-15T16:37:21.070Z", "disclosures": [] } }, diff --git a/metadata/modules/appierBidAdapter.json b/metadata/modules/appierBidAdapter.json index de538718dc9..0e6d091d779 100644 --- a/metadata/modules/appierBidAdapter.json +++ b/metadata/modules/appierBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tcf.appier.com/deviceStorage2025.json": { - "timestamp": "2025-11-10T11:41:17.074Z", + "timestamp": "2026-01-15T16:37:21.109Z", "disclosures": [ { "identifier": "_atrk_ssid", diff --git a/metadata/modules/appnexusBidAdapter.json b/metadata/modules/appnexusBidAdapter.json index 5c5adf4e0d0..4daa7849c38 100644 --- a/metadata/modules/appnexusBidAdapter.json +++ b/metadata/modules/appnexusBidAdapter.json @@ -2,23 +2,23 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://acdn.adnxs.com/gvl/1d/xandrdevicestoragedisclosures.json": { - "timestamp": "2025-11-10T11:41:17.879Z", + "timestamp": "2026-01-15T16:37:21.729Z", "disclosures": [] }, "https://tcf.emetriq.de/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:17.182Z", + "timestamp": "2026-01-15T16:37:21.220Z", "disclosures": [] }, "https://beintoo-support.b-cdn.net/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:17.204Z", + "timestamp": "2026-01-15T16:37:21.242Z", "disclosures": [] }, "https://projectagora.net/1032_deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:17.429Z", + "timestamp": "2026-01-15T16:37:21.262Z", "disclosures": [] }, "https://adzymic.com/tcf.json": { - "timestamp": "2025-11-10T11:41:17.879Z", + "timestamp": "2026-01-15T16:37:21.729Z", "disclosures": [] } }, diff --git a/metadata/modules/appushBidAdapter.json b/metadata/modules/appushBidAdapter.json index 4ea33ee1005..1f2a1001edc 100644 --- a/metadata/modules/appushBidAdapter.json +++ b/metadata/modules/appushBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.thebiding.com/disclosures.json": { - "timestamp": "2025-11-10T11:41:17.906Z", + "timestamp": "2026-01-15T16:37:21.750Z", "disclosures": [] } }, diff --git a/metadata/modules/apstreamBidAdapter.json b/metadata/modules/apstreamBidAdapter.json index 3958ccce654..6b4ccb793f1 100644 --- a/metadata/modules/apstreamBidAdapter.json +++ b/metadata/modules/apstreamBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://sak.userreport.com/tcf.json": { - "timestamp": "2025-11-10T11:41:17.970Z", + "timestamp": "2026-01-15T16:37:21.827Z", "disclosures": [ { "identifier": "apr_dsu", diff --git a/metadata/modules/audiencerunBidAdapter.json b/metadata/modules/audiencerunBidAdapter.json index 4996580fa8d..f45b96d4045 100644 --- a/metadata/modules/audiencerunBidAdapter.json +++ b/metadata/modules/audiencerunBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.audiencerun.com/tcf.json": { - "timestamp": "2025-11-10T11:41:17.990Z", + "timestamp": "2026-01-15T16:37:21.848Z", "disclosures": [] } }, diff --git a/metadata/modules/axisBidAdapter.json b/metadata/modules/axisBidAdapter.json index f22cefed7c6..d6e67748fc6 100644 --- a/metadata/modules/axisBidAdapter.json +++ b/metadata/modules/axisBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://axis-marketplace.com/tcf.json": { - "timestamp": "2025-11-10T11:41:18.038Z", + "timestamp": "2026-01-15T16:37:21.895Z", "disclosures": [] } }, diff --git a/metadata/modules/azerionedgeRtdProvider.json b/metadata/modules/azerionedgeRtdProvider.json index bfd34532f14..7d1760d0cdf 100644 --- a/metadata/modules/azerionedgeRtdProvider.json +++ b/metadata/modules/azerionedgeRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://sellers.improvedigital.com/tcf-cookies.json": { - "timestamp": "2025-11-10T11:41:18.076Z", + "timestamp": "2026-01-15T16:37:21.937Z", "disclosures": [ { "identifier": "tuuid", diff --git a/metadata/modules/beachfrontBidAdapter.json b/metadata/modules/beachfrontBidAdapter.json index f7d9015cc0a..10084ba55b4 100644 --- a/metadata/modules/beachfrontBidAdapter.json +++ b/metadata/modules/beachfrontBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tcf.seedtag.com/vendor.json": { - "timestamp": "2025-11-10T11:41:18.097Z", + "timestamp": "2026-01-15T16:37:21.980Z", "disclosures": [] } }, diff --git a/metadata/modules/beopBidAdapter.json b/metadata/modules/beopBidAdapter.json index df982ea319c..ea81c16179b 100644 --- a/metadata/modules/beopBidAdapter.json +++ b/metadata/modules/beopBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://beop.io/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:18.465Z", + "timestamp": "2026-01-15T16:37:22.002Z", "disclosures": [] } }, diff --git a/metadata/modules/betweenBidAdapter.json b/metadata/modules/betweenBidAdapter.json index 5e19632907b..dd5d5f5c0eb 100644 --- a/metadata/modules/betweenBidAdapter.json +++ b/metadata/modules/betweenBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://en.betweenx.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:18.566Z", + "timestamp": "2026-01-15T16:37:22.125Z", "disclosures": [] } }, diff --git a/metadata/modules/bidfuseBidAdapter.json b/metadata/modules/bidfuseBidAdapter.json index 50707a0167f..fe1587ae778 100644 --- a/metadata/modules/bidfuseBidAdapter.json +++ b/metadata/modules/bidfuseBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bidfuse.com/disclosure.json": { - "timestamp": "2025-11-10T11:41:18.621Z", + "timestamp": "2026-01-15T16:37:22.186Z", "disclosures": [] } }, diff --git a/metadata/modules/bidmaticBidAdapter.json b/metadata/modules/bidmaticBidAdapter.json index b27b3e07e76..9e9067213e2 100644 --- a/metadata/modules/bidmaticBidAdapter.json +++ b/metadata/modules/bidmaticBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bidmatic.io/.well-known/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:18.804Z", + "timestamp": "2026-01-15T16:37:22.372Z", "disclosures": [] } }, diff --git a/metadata/modules/bidtheatreBidAdapter.json b/metadata/modules/bidtheatreBidAdapter.json index 63983a136e9..f9f2fabc74c 100644 --- a/metadata/modules/bidtheatreBidAdapter.json +++ b/metadata/modules/bidtheatreBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://privacy.bidtheatre.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:18.841Z", + "timestamp": "2026-01-15T16:37:22.410Z", "disclosures": [] } }, diff --git a/metadata/modules/bliinkBidAdapter.json b/metadata/modules/bliinkBidAdapter.json index ae80661cb96..f5bb94bcc55 100644 --- a/metadata/modules/bliinkBidAdapter.json +++ b/metadata/modules/bliinkBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bliink.io/disclosures.json": { - "timestamp": "2025-11-10T11:41:19.153Z", + "timestamp": "2026-01-15T16:37:22.672Z", "disclosures": [] } }, diff --git a/metadata/modules/blockthroughBidAdapter.json b/metadata/modules/blockthroughBidAdapter.json index 54fd5fb8aaf..83615319f90 100644 --- a/metadata/modules/blockthroughBidAdapter.json +++ b/metadata/modules/blockthroughBidAdapter.json @@ -2,324 +2,8 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://blockthrough.com/tcf_disclosures.json": { - "timestamp": "2025-11-10T11:41:19.544Z", - "disclosures": [ - { - "identifier": "BT_AA_DETECTION", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "btUserCountry", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "btUserCountryExpiry", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "btUserIsFromRestrictedCountry", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "BT_BUNDLE_VERSION", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "BT_DIGEST_VERSION", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "BT_sid", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "BT_traceID", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "uids", - "type": "cookie", - "maxAgeSeconds": 7776000, - "cookieRefresh": true, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "BT_pvSent", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "BT_WHITELISTING_IFRAME_ACCESS", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "BT_BLOCKLISTED_CREATIVES", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "BT_AM_SOFTWALL_RENDERED", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "BT_AM_SOFTWALL_DISMISSED", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "BT_AM_SOFTWALL_RECOVERED", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "BT_AM_SOFTWALL_RENDER_COUNT", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "BT_AM_SOFTWALL_ABTEST", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "BT_AM_ATTRIBUTION_EXPIRY", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "BT_AM_PREMIUM_ADBLOCK_USER_DETECTED", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "BT_AM_PREMIUM_ADBLOCK_USER_DETECTION_DATE", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - }, - { - "identifier": "BT_AM_SCA_SUCCEED", - "type": "web", - "maxAgeSeconds": null, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 3, - 4, - 7, - 9, - 10 - ] - } - ] + "timestamp": "2026-01-15T16:37:23.054Z", + "disclosures": null } }, "components": [ diff --git a/metadata/modules/blueBidAdapter.json b/metadata/modules/blueBidAdapter.json index 0e5ca4a4b88..518be157904 100644 --- a/metadata/modules/blueBidAdapter.json +++ b/metadata/modules/blueBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://getblue.io/iab/iab.json": { - "timestamp": "2025-11-10T11:41:19.703Z", + "timestamp": "2026-01-15T16:37:23.144Z", "disclosures": [] } }, diff --git a/metadata/modules/bmsBidAdapter.json b/metadata/modules/bmsBidAdapter.json index 220d3ded386..303021966b3 100644 --- a/metadata/modules/bmsBidAdapter.json +++ b/metadata/modules/bmsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bluems.com/iab.json": { - "timestamp": "2025-11-10T11:41:20.048Z", + "timestamp": "2026-01-15T16:37:23.500Z", "disclosures": [] } }, diff --git a/metadata/modules/boldwinBidAdapter.json b/metadata/modules/boldwinBidAdapter.json index c9c9f02f877..70d07826aa6 100644 --- a/metadata/modules/boldwinBidAdapter.json +++ b/metadata/modules/boldwinBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://magav.videowalldirect.com/iab/videowalldirectiab.json": { - "timestamp": "2025-11-10T11:41:20.069Z", + "timestamp": "2026-01-15T16:37:23.535Z", "disclosures": [] } }, diff --git a/metadata/modules/bridBidAdapter.json b/metadata/modules/bridBidAdapter.json index 7ab0f21eb7b..8aac69ef3cd 100644 --- a/metadata/modules/bridBidAdapter.json +++ b/metadata/modules/bridBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://target-video.com/vendors-device-storage-and-operational-disclosures.json": { - "timestamp": "2025-11-10T11:41:20.090Z", + "timestamp": "2026-01-15T16:37:23.568Z", "disclosures": [ { "identifier": "brid_location", diff --git a/metadata/modules/browsiBidAdapter.json b/metadata/modules/browsiBidAdapter.json index 104ecacb74c..0c262185ff5 100644 --- a/metadata/modules/browsiBidAdapter.json +++ b/metadata/modules/browsiBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.browsiprod.com/ads/tcf.json": { - "timestamp": "2025-11-10T11:41:20.230Z", + "timestamp": "2026-01-15T16:37:23.706Z", "disclosures": [] } }, diff --git a/metadata/modules/bucksenseBidAdapter.json b/metadata/modules/bucksenseBidAdapter.json index 08f016dbbe0..47eca0c0a76 100644 --- a/metadata/modules/bucksenseBidAdapter.json +++ b/metadata/modules/bucksenseBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://j.bksnimages.com/iab/devsto02.json": { - "timestamp": "2025-11-10T11:41:20.278Z", + "timestamp": "2026-01-15T16:37:23.743Z", "disclosures": [] } }, diff --git a/metadata/modules/carodaBidAdapter.json b/metadata/modules/carodaBidAdapter.json index fbb539fea09..7e0d20f7c1f 100644 --- a/metadata/modules/carodaBidAdapter.json +++ b/metadata/modules/carodaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn2.caroda.io/tcfvds/2022-05-17/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:20.331Z", + "timestamp": "2026-01-15T16:37:23.785Z", "disclosures": [] } }, diff --git a/metadata/modules/categoryTranslation.json b/metadata/modules/categoryTranslation.json index 1121827cd76..e92582fc32f 100644 --- a/metadata/modules/categoryTranslation.json +++ b/metadata/modules/categoryTranslation.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/prebid/categoryTranslation.json": { - "timestamp": "2025-11-10T11:41:10.870Z", + "timestamp": "2026-01-15T16:36:37.466Z", "disclosures": [ { "identifier": "iabToFwMappingkey", diff --git a/metadata/modules/ceeIdSystem.json b/metadata/modules/ceeIdSystem.json index 101afaccdb7..cf988945eed 100644 --- a/metadata/modules/ceeIdSystem.json +++ b/metadata/modules/ceeIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ssp.wp.pl/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:20.510Z", + "timestamp": "2026-01-15T16:37:24.082Z", "disclosures": null } }, diff --git a/metadata/modules/chromeAiRtdProvider.json b/metadata/modules/chromeAiRtdProvider.json index c37211d4647..c15ff7b9baa 100644 --- a/metadata/modules/chromeAiRtdProvider.json +++ b/metadata/modules/chromeAiRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/modules/chromeAiRtdProvider.json": { - "timestamp": "2025-11-10T11:41:20.842Z", + "timestamp": "2026-01-15T16:37:24.403Z", "disclosures": [ { "identifier": "chromeAi_detected_data", diff --git a/metadata/modules/clickioBidAdapter.json b/metadata/modules/clickioBidAdapter.json new file mode 100644 index 00000000000..e4b74ffa33f --- /dev/null +++ b/metadata/modules/clickioBidAdapter.json @@ -0,0 +1,18 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": { + "https://o.clickiocdn.com/tcf_storage_info.json": { + "timestamp": "2026-01-15T16:37:24.404Z", + "disclosures": [] + } + }, + "components": [ + { + "componentType": "bidder", + "componentName": "clickio", + "aliasOf": null, + "gvlid": 1500, + "disclosureURL": "https://o.clickiocdn.com/tcf_storage_info.json" + } + ] +} \ No newline at end of file diff --git a/metadata/modules/clydoBidAdapter.json b/metadata/modules/clydoBidAdapter.json new file mode 100644 index 00000000000..9d4cca50254 --- /dev/null +++ b/metadata/modules/clydoBidAdapter.json @@ -0,0 +1,13 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": {}, + "components": [ + { + "componentType": "bidder", + "componentName": "clydo", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + } + ] +} \ No newline at end of file diff --git a/metadata/modules/compassBidAdapter.json b/metadata/modules/compassBidAdapter.json index 818aeccec74..9d09d5cfd34 100644 --- a/metadata/modules/compassBidAdapter.json +++ b/metadata/modules/compassBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.marphezis.com/tcf-vendor-disclosures.json": { - "timestamp": "2025-11-10T11:41:20.844Z", + "timestamp": "2026-01-15T16:37:24.820Z", "disclosures": [] } }, diff --git a/metadata/modules/conceptxBidAdapter.json b/metadata/modules/conceptxBidAdapter.json index 82a4165c439..a866c1f014e 100644 --- a/metadata/modules/conceptxBidAdapter.json +++ b/metadata/modules/conceptxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cncptx.com/device_storage_disclosure.json": { - "timestamp": "2025-11-10T11:41:20.859Z", + "timestamp": "2026-01-15T16:37:24.836Z", "disclosures": [] } }, diff --git a/metadata/modules/connatixBidAdapter.json b/metadata/modules/connatixBidAdapter.json index a98da503920..b8bc893e2af 100644 --- a/metadata/modules/connatixBidAdapter.json +++ b/metadata/modules/connatixBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://connatix.com/iab-tcf-disclosure.json": { - "timestamp": "2025-11-10T11:41:20.878Z", + "timestamp": "2026-01-15T16:37:24.861Z", "disclosures": [ { "identifier": "cnx_userId", @@ -12,22 +12,10 @@ "purposes": [ 1, 2, + 3, 4, 7, - 8 - ] - }, - { - "identifier": "cnx_player_reload", - "type": "cookie", - "maxAgeSeconds": 60, - "cookieRefresh": false, - "purposes": [ - 1, - 2, - 4, - 7, - 8 + 10 ] } ] diff --git a/metadata/modules/connectIdSystem.json b/metadata/modules/connectIdSystem.json index 71a575ea516..f1942c65604 100644 --- a/metadata/modules/connectIdSystem.json +++ b/metadata/modules/connectIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://meta.legal.yahoo.com/iab-tcf/v2/device-storage-disclosure.json": { - "timestamp": "2025-11-10T11:41:20.946Z", + "timestamp": "2026-01-15T16:37:24.938Z", "disclosures": [ { "identifier": "vmcid", diff --git a/metadata/modules/connectadBidAdapter.json b/metadata/modules/connectadBidAdapter.json index 5d4a28aa89f..699183a7953 100644 --- a/metadata/modules/connectadBidAdapter.json +++ b/metadata/modules/connectadBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.connectad.io/tcf_storage_info.json": { - "timestamp": "2025-11-10T11:41:20.966Z", + "timestamp": "2026-01-15T16:37:24.959Z", "disclosures": [] } }, diff --git a/metadata/modules/contentexchangeBidAdapter.json b/metadata/modules/contentexchangeBidAdapter.json index d53b22757e2..03ffa8a4331 100644 --- a/metadata/modules/contentexchangeBidAdapter.json +++ b/metadata/modules/contentexchangeBidAdapter.json @@ -2,8 +2,8 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://hb.contentexchange.me/template/device_storage.json": { - "timestamp": "2025-11-10T11:41:21.385Z", - "disclosures": [] + "timestamp": "2026-01-15T16:37:24.995Z", + "disclosures": null } }, "components": [ diff --git a/metadata/modules/conversantBidAdapter.json b/metadata/modules/conversantBidAdapter.json index e9fdd2b0dd4..1d6083f65d5 100644 --- a/metadata/modules/conversantBidAdapter.json +++ b/metadata/modules/conversantBidAdapter.json @@ -1,8 +1,8 @@ { "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { - "https://s-usweb.dotomi.com/assets/js/taggy-js/2.18.1/device_storage_disclosure.json": { - "timestamp": "2025-11-10T11:41:21.759Z", + "https://s-usweb.dotomi.com/assets/js/taggy-js/2.18.9/device_storage_disclosure.json": { + "timestamp": "2026-01-15T16:37:25.119Z", "disclosures": [ { "identifier": "dtm_status", @@ -554,6 +554,25 @@ 10, 11 ] + }, + { + "identifier": "hConversionEventId", + "type": "cookie", + "maxAgeSeconds": 2592000, + "cookieRefresh": true, + "purposes": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ] } ] } @@ -564,7 +583,7 @@ "componentName": "conversant", "aliasOf": null, "gvlid": 24, - "disclosureURL": "https://s-usweb.dotomi.com/assets/js/taggy-js/2.18.1/device_storage_disclosure.json" + "disclosureURL": "https://s-usweb.dotomi.com/assets/js/taggy-js/2.18.9/device_storage_disclosure.json" }, { "componentType": "bidder", diff --git a/metadata/modules/copper6sspBidAdapter.json b/metadata/modules/copper6sspBidAdapter.json index 70692327512..f7fe4bae141 100644 --- a/metadata/modules/copper6sspBidAdapter.json +++ b/metadata/modules/copper6sspBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ssp.copper6.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:21.777Z", + "timestamp": "2026-01-15T16:37:25.142Z", "disclosures": [] } }, diff --git a/metadata/modules/cpmstarBidAdapter.json b/metadata/modules/cpmstarBidAdapter.json index b773d5a3c8d..5afe41d0cb1 100644 --- a/metadata/modules/cpmstarBidAdapter.json +++ b/metadata/modules/cpmstarBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.aditude.com/storageaccess.json": { - "timestamp": "2025-11-10T11:41:21.865Z", + "timestamp": "2026-01-15T16:37:25.177Z", "disclosures": [] } }, diff --git a/metadata/modules/criteoBidAdapter.json b/metadata/modules/criteoBidAdapter.json index e1cc053fbaa..23009b06d2d 100644 --- a/metadata/modules/criteoBidAdapter.json +++ b/metadata/modules/criteoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://privacy.criteo.com/iab-europe/tcfv2/disclosure": { - "timestamp": "2025-11-10T11:41:21.908Z", + "timestamp": "2026-01-15T16:37:25.210Z", "disclosures": [ { "identifier": "criteo_fast_bid", diff --git a/metadata/modules/criteoIdSystem.json b/metadata/modules/criteoIdSystem.json index e463e89f3ee..481e0b4d105 100644 --- a/metadata/modules/criteoIdSystem.json +++ b/metadata/modules/criteoIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://privacy.criteo.com/iab-europe/tcfv2/disclosure": { - "timestamp": "2025-11-10T11:41:21.930Z", + "timestamp": "2026-01-15T16:37:25.227Z", "disclosures": [ { "identifier": "criteo_fast_bid", diff --git a/metadata/modules/cwireBidAdapter.json b/metadata/modules/cwireBidAdapter.json index d2437db0a0b..35bdc0d3bad 100644 --- a/metadata/modules/cwireBidAdapter.json +++ b/metadata/modules/cwireBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.cwi.re/artifacts/iab/iab.json": { - "timestamp": "2025-11-10T11:41:21.931Z", + "timestamp": "2026-01-15T16:37:25.227Z", "disclosures": [] } }, diff --git a/metadata/modules/czechAdIdSystem.json b/metadata/modules/czechAdIdSystem.json index e8b6b59b485..70d4f766db0 100644 --- a/metadata/modules/czechAdIdSystem.json +++ b/metadata/modules/czechAdIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cpex.cz/storagedisclosure.json": { - "timestamp": "2025-11-10T11:41:22.260Z", + "timestamp": "2026-01-15T16:37:25.255Z", "disclosures": [] } }, diff --git a/metadata/modules/dailymotionBidAdapter.json b/metadata/modules/dailymotionBidAdapter.json index 0be58872c96..a4b480a7f52 100644 --- a/metadata/modules/dailymotionBidAdapter.json +++ b/metadata/modules/dailymotionBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://statics.dmcdn.net/a/vds.json": { - "timestamp": "2025-11-10T11:41:24.173Z", + "timestamp": "2026-01-15T16:37:25.655Z", "disclosures": [ { "identifier": "uid_dm", diff --git a/metadata/modules/dasBidAdapter.json b/metadata/modules/dasBidAdapter.json new file mode 100644 index 00000000000..1ca1c72523f --- /dev/null +++ b/metadata/modules/dasBidAdapter.json @@ -0,0 +1,20 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": {}, + "components": [ + { + "componentType": "bidder", + "componentName": "das", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "ringieraxelspringer", + "aliasOf": "das", + "gvlid": null, + "disclosureURL": null + } + ] +} \ No newline at end of file diff --git a/metadata/modules/debugging.json b/metadata/modules/debugging.json index 5165051f5e6..be1d24bff85 100644 --- a/metadata/modules/debugging.json +++ b/metadata/modules/debugging.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/prebid/debugging.json": { - "timestamp": "2025-11-10T11:41:10.869Z", + "timestamp": "2026-01-15T16:36:37.464Z", "disclosures": [ { "identifier": "__*_debugging__", diff --git a/metadata/modules/deepintentBidAdapter.json b/metadata/modules/deepintentBidAdapter.json index 2f6b7aa0581..caadad2e64b 100644 --- a/metadata/modules/deepintentBidAdapter.json +++ b/metadata/modules/deepintentBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.deepintent.com/iabeurope_vendor_disclosures.json": { - "timestamp": "2025-11-10T11:41:24.267Z", + "timestamp": "2026-01-15T16:37:25.681Z", "disclosures": [] } }, diff --git a/metadata/modules/defineMediaBidAdapter.json b/metadata/modules/defineMediaBidAdapter.json index 170201aaa23..6f08662645e 100644 --- a/metadata/modules/defineMediaBidAdapter.json +++ b/metadata/modules/defineMediaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://definemedia.de/tcf/deviceStorageDisclosureURL.json": { - "timestamp": "2025-11-10T11:41:24.364Z", + "timestamp": "2026-01-15T16:37:25.762Z", "disclosures": [ { "identifier": "conative$dataGathering$Adex", diff --git a/metadata/modules/deltaprojectsBidAdapter.json b/metadata/modules/deltaprojectsBidAdapter.json index 0fade06c854..2ecd1687865 100644 --- a/metadata/modules/deltaprojectsBidAdapter.json +++ b/metadata/modules/deltaprojectsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.de17a.com/policy/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:24.769Z", + "timestamp": "2026-01-15T16:37:26.176Z", "disclosures": [] } }, diff --git a/metadata/modules/dianomiBidAdapter.json b/metadata/modules/dianomiBidAdapter.json index a8a62254cba..784840e9a2b 100644 --- a/metadata/modules/dianomiBidAdapter.json +++ b/metadata/modules/dianomiBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.dianomi.com/device_storage.json": { - "timestamp": "2025-11-10T11:41:24.833Z", + "timestamp": "2026-01-15T16:37:26.601Z", "disclosures": [] } }, diff --git a/metadata/modules/digitalMatterBidAdapter.json b/metadata/modules/digitalMatterBidAdapter.json index 38a1f678bdf..fc99fe60759 100644 --- a/metadata/modules/digitalMatterBidAdapter.json +++ b/metadata/modules/digitalMatterBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://digitalmatter.ai/disclosures.json": { - "timestamp": "2025-11-10T11:41:24.834Z", + "timestamp": "2026-01-15T16:37:26.601Z", "disclosures": [] } }, diff --git a/metadata/modules/distroscaleBidAdapter.json b/metadata/modules/distroscaleBidAdapter.json index bc880da4280..058b5dd309c 100644 --- a/metadata/modules/distroscaleBidAdapter.json +++ b/metadata/modules/distroscaleBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://a.jsrdn.com/tcf/tcf-vendor-disclosure.json": { - "timestamp": "2025-11-10T11:41:25.200Z", + "timestamp": "2026-01-15T16:37:26.960Z", "disclosures": [] } }, diff --git a/metadata/modules/docereeAdManagerBidAdapter.json b/metadata/modules/docereeAdManagerBidAdapter.json index 913c3357893..21c2a2ee183 100644 --- a/metadata/modules/docereeAdManagerBidAdapter.json +++ b/metadata/modules/docereeAdManagerBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://doceree.com/.well-known/iab/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:25.215Z", + "timestamp": "2026-01-15T16:37:26.987Z", "disclosures": [] } }, diff --git a/metadata/modules/docereeBidAdapter.json b/metadata/modules/docereeBidAdapter.json index 99841e87e21..0b1ddd6b45b 100644 --- a/metadata/modules/docereeBidAdapter.json +++ b/metadata/modules/docereeBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://doceree.com/.well-known/iab/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:25.967Z", + "timestamp": "2026-01-15T16:37:27.735Z", "disclosures": [] } }, diff --git a/metadata/modules/dspxBidAdapter.json b/metadata/modules/dspxBidAdapter.json index 976f2a0c178..326d5c3c5e2 100644 --- a/metadata/modules/dspxBidAdapter.json +++ b/metadata/modules/dspxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tcf.adtech.app/gen/deviceStorageDisclosure/os.json": { - "timestamp": "2025-11-10T11:41:25.968Z", + "timestamp": "2026-01-15T16:37:27.736Z", "disclosures": [] } }, diff --git a/metadata/modules/e_volutionBidAdapter.json b/metadata/modules/e_volutionBidAdapter.json index 071e64c0c9e..9e92ef262a2 100644 --- a/metadata/modules/e_volutionBidAdapter.json +++ b/metadata/modules/e_volutionBidAdapter.json @@ -2,8 +2,8 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://e-volution.ai/file.json": { - "timestamp": "2025-11-10T11:41:26.620Z", - "disclosures": null + "timestamp": "2026-01-15T16:37:28.380Z", + "disclosures": [] } }, "components": [ diff --git a/metadata/modules/edge226BidAdapter.json b/metadata/modules/edge226BidAdapter.json index b44dfdece4d..60092247096 100644 --- a/metadata/modules/edge226BidAdapter.json +++ b/metadata/modules/edge226BidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.serveteck.com/cdn_storage/tcf/tcf.json?a=1.io": { - "timestamp": "2025-11-10T11:41:26.928Z", + "timestamp": "2026-01-15T16:37:28.706Z", "disclosures": [] } }, diff --git a/metadata/modules/empowerBidAdapter.json b/metadata/modules/empowerBidAdapter.json index fdc8da20c3d..7e5a855b078 100644 --- a/metadata/modules/empowerBidAdapter.json +++ b/metadata/modules/empowerBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.empower.net/vendor/vendor.json": { - "timestamp": "2025-11-10T11:41:26.970Z", + "timestamp": "2026-01-15T16:37:28.720Z", "disclosures": [] } }, diff --git a/metadata/modules/equativBidAdapter.json b/metadata/modules/equativBidAdapter.json index 195b1b775e3..4d8ad974ec3 100644 --- a/metadata/modules/equativBidAdapter.json +++ b/metadata/modules/equativBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://apps.smartadserver.com/device-storage-disclosures/equativDeviceStorageDisclosures.json": { - "timestamp": "2025-11-10T11:41:26.993Z", + "timestamp": "2026-01-15T16:37:28.760Z", "disclosures": [] } }, diff --git a/metadata/modules/eskimiBidAdapter.json b/metadata/modules/eskimiBidAdapter.json index f9043e75efc..d520ec21d6a 100644 --- a/metadata/modules/eskimiBidAdapter.json +++ b/metadata/modules/eskimiBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://dsp-media.eskimi.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:27.023Z", + "timestamp": "2026-01-15T16:37:28.839Z", "disclosures": [] } }, diff --git a/metadata/modules/etargetBidAdapter.json b/metadata/modules/etargetBidAdapter.json index 044cf8516d8..ccd6df2c4e6 100644 --- a/metadata/modules/etargetBidAdapter.json +++ b/metadata/modules/etargetBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.etarget.sk/cookies3.json": { - "timestamp": "2025-11-10T11:41:27.042Z", + "timestamp": "2026-01-15T16:37:28.864Z", "disclosures": [] } }, diff --git a/metadata/modules/euidIdSystem.json b/metadata/modules/euidIdSystem.json index 12ad2d0df71..4940af0fea3 100644 --- a/metadata/modules/euidIdSystem.json +++ b/metadata/modules/euidIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ttd-misc-public-assets.s3.us-west-2.amazonaws.com/deviceStorageDisclosureURL.json": { - "timestamp": "2025-11-10T11:41:27.617Z", + "timestamp": "2026-01-15T16:37:29.395Z", "disclosures": [] } }, diff --git a/metadata/modules/exadsBidAdapter.json b/metadata/modules/exadsBidAdapter.json index 7fc5c12d5a9..2dc16c3c173 100644 --- a/metadata/modules/exadsBidAdapter.json +++ b/metadata/modules/exadsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://a.native7.com/tcf/deviceStorage.php": { - "timestamp": "2025-11-10T11:41:27.849Z", + "timestamp": "2026-01-15T16:37:29.597Z", "disclosures": [ { "identifier": "pn-zone-*", diff --git a/metadata/modules/feedadBidAdapter.json b/metadata/modules/feedadBidAdapter.json index 90b53a0c5f6..336bce9c92a 100644 --- a/metadata/modules/feedadBidAdapter.json +++ b/metadata/modules/feedadBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://api.feedad.com/tcf-device-disclosures.json": { - "timestamp": "2025-11-10T11:41:28.014Z", + "timestamp": "2026-01-15T16:37:29.770Z", "disclosures": [ { "identifier": "__fad_data", diff --git a/metadata/modules/fwsspBidAdapter.json b/metadata/modules/fwsspBidAdapter.json index 498eefda557..adc6898cfd2 100644 --- a/metadata/modules/fwsspBidAdapter.json +++ b/metadata/modules/fwsspBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://iab.fwmrm.net/g/devicedisclosure.json": { - "timestamp": "2025-11-10T11:41:28.153Z", + "timestamp": "2026-01-15T16:37:29.972Z", "disclosures": [] } }, diff --git a/metadata/modules/gamoshiBidAdapter.json b/metadata/modules/gamoshiBidAdapter.json index 29e4a1d0a94..1840831491b 100644 --- a/metadata/modules/gamoshiBidAdapter.json +++ b/metadata/modules/gamoshiBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.gamoshi.com/disclosures-client-storage.json": { - "timestamp": "2025-11-10T11:41:28.242Z", + "timestamp": "2026-01-15T16:37:30.056Z", "disclosures": [] } }, diff --git a/metadata/modules/gemiusIdSystem.json b/metadata/modules/gemiusIdSystem.json index 9fb71ca2df9..41725086443 100644 --- a/metadata/modules/gemiusIdSystem.json +++ b/metadata/modules/gemiusIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://gemius.com/media/documents/Gemius_SA_Vendor_Device_Storage.json": { - "timestamp": "2025-11-10T11:41:28.507Z", + "timestamp": "2026-01-15T16:37:30.137Z", "disclosures": [ { "identifier": "__gsyncs_gdpr", @@ -142,7 +142,8 @@ "cookieRefresh": true, "purposes": [ 1 - ] + ], + "optOut": true }, { "identifier": "__gfp_s_dnt", @@ -151,7 +152,8 @@ "cookieRefresh": true, "purposes": [ 1 - ] + ], + "optOut": true }, { "identifier": "__gfp_ruid", @@ -246,7 +248,8 @@ "cookieRefresh": false, "purposes": [ 1 - ] + ], + "optOut": true }, { "identifier": "_ao_consent_data", diff --git a/metadata/modules/glomexBidAdapter.json b/metadata/modules/glomexBidAdapter.json index 059c04ff011..12f4f3af4c1 100644 --- a/metadata/modules/glomexBidAdapter.json +++ b/metadata/modules/glomexBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://player.glomex.com/.well-known/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:28.946Z", + "timestamp": "2026-01-15T16:37:30.590Z", "disclosures": [ { "identifier": "glomexUser", diff --git a/metadata/modules/goldbachBidAdapter.json b/metadata/modules/goldbachBidAdapter.json index 7124d6bf67f..017d03eab2f 100644 --- a/metadata/modules/goldbachBidAdapter.json +++ b/metadata/modules/goldbachBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://gb-next.ch/TcfGoldbachDeviceStorage.json": { - "timestamp": "2025-11-10T11:41:28.965Z", + "timestamp": "2026-01-15T16:37:30.612Z", "disclosures": [ { "identifier": "dakt_2_session_id", diff --git a/metadata/modules/gridBidAdapter.json b/metadata/modules/gridBidAdapter.json index 92d76383cdd..16a1fb5eba9 100644 --- a/metadata/modules/gridBidAdapter.json +++ b/metadata/modules/gridBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.themediagrid.com/devicestorage.json": { - "timestamp": "2025-11-10T11:41:29.081Z", + "timestamp": "2026-01-15T16:37:30.631Z", "disclosures": [] } }, @@ -34,13 +34,6 @@ "aliasOf": "grid", "gvlid": null, "disclosureURL": null - }, - { - "componentType": "bidder", - "componentName": "trustx", - "aliasOf": "grid", - "gvlid": null, - "disclosureURL": null } ] } \ No newline at end of file diff --git a/metadata/modules/gumgumBidAdapter.json b/metadata/modules/gumgumBidAdapter.json index 868301bc3f3..401128e8ec4 100644 --- a/metadata/modules/gumgumBidAdapter.json +++ b/metadata/modules/gumgumBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://marketing.gumgum.com/devicestoragedisclosures.json": { - "timestamp": "2025-11-10T11:41:29.232Z", + "timestamp": "2026-01-15T16:37:30.777Z", "disclosures": [] } }, diff --git a/metadata/modules/hadronIdSystem.json b/metadata/modules/hadronIdSystem.json index 1a737f16ce4..5be05f97ac5 100644 --- a/metadata/modules/hadronIdSystem.json +++ b/metadata/modules/hadronIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://p.ad.gt/static/iab_tcf.json": { - "timestamp": "2025-11-10T11:41:29.297Z", + "timestamp": "2026-01-15T16:37:30.846Z", "disclosures": [ { "identifier": "au/sid", diff --git a/metadata/modules/hadronRtdProvider.json b/metadata/modules/hadronRtdProvider.json index 3fb00e9192e..cf519af2d4b 100644 --- a/metadata/modules/hadronRtdProvider.json +++ b/metadata/modules/hadronRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://p.ad.gt/static/iab_tcf.json": { - "timestamp": "2025-11-10T11:41:29.445Z", + "timestamp": "2026-01-15T16:37:31.022Z", "disclosures": [ { "identifier": "au/sid", diff --git a/metadata/modules/holidBidAdapter.json b/metadata/modules/holidBidAdapter.json index a6b3edb8e19..cd7d445a7b5 100644 --- a/metadata/modules/holidBidAdapter.json +++ b/metadata/modules/holidBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ads.holid.io/devicestorage.json": { - "timestamp": "2025-11-10T11:41:29.445Z", + "timestamp": "2026-01-15T16:37:31.022Z", "disclosures": [ { "identifier": "uids", diff --git a/metadata/modules/hybridBidAdapter.json b/metadata/modules/hybridBidAdapter.json index aec5ee521ae..6e4431fea64 100644 --- a/metadata/modules/hybridBidAdapter.json +++ b/metadata/modules/hybridBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://st.hybrid.ai/policy/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:29.711Z", + "timestamp": "2026-01-15T16:37:31.310Z", "disclosures": [] } }, diff --git a/metadata/modules/id5IdSystem.json b/metadata/modules/id5IdSystem.json index 6321ecb8927..2f26fea8f78 100644 --- a/metadata/modules/id5IdSystem.json +++ b/metadata/modules/id5IdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://id5-sync.com/tcf/disclosures.json": { - "timestamp": "2025-11-10T11:41:29.866Z", + "timestamp": "2026-01-15T16:37:31.527Z", "disclosures": [] } }, diff --git a/metadata/modules/identityLinkIdSystem.json b/metadata/modules/identityLinkIdSystem.json index 8ffc8facede..72ec6d90f38 100644 --- a/metadata/modules/identityLinkIdSystem.json +++ b/metadata/modules/identityLinkIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tcf.ats.rlcdn.com/device-storage-disclosure.json": { - "timestamp": "2025-11-10T11:41:30.175Z", + "timestamp": "2026-01-15T16:37:31.841Z", "disclosures": [ { "identifier": "_lr_retry_request", diff --git a/metadata/modules/illuminBidAdapter.json b/metadata/modules/illuminBidAdapter.json index f4fe982f1b2..22e2cb0cd27 100644 --- a/metadata/modules/illuminBidAdapter.json +++ b/metadata/modules/illuminBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://admanmedia.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:30.195Z", + "timestamp": "2026-01-15T16:37:31.859Z", "disclosures": [] } }, diff --git a/metadata/modules/impactifyBidAdapter.json b/metadata/modules/impactifyBidAdapter.json index f848e17d7b2..ae26f59c0fe 100644 --- a/metadata/modules/impactifyBidAdapter.json +++ b/metadata/modules/impactifyBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ad.impactify.io/tcfvendors.json": { - "timestamp": "2025-11-10T11:41:30.557Z", + "timestamp": "2026-01-15T16:37:32.142Z", "disclosures": [ { "identifier": "_im*", diff --git a/metadata/modules/improvedigitalBidAdapter.json b/metadata/modules/improvedigitalBidAdapter.json index 8125c15088a..4a70169e53e 100644 --- a/metadata/modules/improvedigitalBidAdapter.json +++ b/metadata/modules/improvedigitalBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://sellers.improvedigital.com/tcf-cookies.json": { - "timestamp": "2025-11-10T11:41:30.908Z", + "timestamp": "2026-01-15T16:37:32.482Z", "disclosures": [ { "identifier": "tuuid", diff --git a/metadata/modules/inmobiBidAdapter.json b/metadata/modules/inmobiBidAdapter.json index 46f8a0bfce3..ecc19209245 100644 --- a/metadata/modules/inmobiBidAdapter.json +++ b/metadata/modules/inmobiBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://publisher.inmobi.com/public/disclosure": { - "timestamp": "2025-11-10T11:41:30.908Z", + "timestamp": "2026-01-15T16:37:32.482Z", "disclosures": [] } }, diff --git a/metadata/modules/insticatorBidAdapter.json b/metadata/modules/insticatorBidAdapter.json index dcfa27640b4..b916aed5900 100644 --- a/metadata/modules/insticatorBidAdapter.json +++ b/metadata/modules/insticatorBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.insticator.com/iab/device-storage-disclosure.json": { - "timestamp": "2025-11-10T11:41:30.939Z", + "timestamp": "2026-01-15T16:37:32.560Z", "disclosures": [ { "identifier": "visitorGeo", diff --git a/metadata/modules/intentIqIdSystem.json b/metadata/modules/intentIqIdSystem.json index 7205568930a..939508257b8 100644 --- a/metadata/modules/intentIqIdSystem.json +++ b/metadata/modules/intentIqIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://agent.intentiq.com/GDPR/gdpr.json": { - "timestamp": "2025-11-10T11:41:30.977Z", + "timestamp": "2026-01-15T16:37:32.588Z", "disclosures": [] } }, diff --git a/metadata/modules/invibesBidAdapter.json b/metadata/modules/invibesBidAdapter.json index 0e2e07daf1d..1f5d4e4fa3e 100644 --- a/metadata/modules/invibesBidAdapter.json +++ b/metadata/modules/invibesBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tcf.invibes.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:31.029Z", + "timestamp": "2026-01-15T16:37:32.647Z", "disclosures": [ { "identifier": "ivvcap", diff --git a/metadata/modules/ipromBidAdapter.json b/metadata/modules/ipromBidAdapter.json index 769c49d3467..87571cc0bd1 100644 --- a/metadata/modules/ipromBidAdapter.json +++ b/metadata/modules/ipromBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://core.iprom.net/info/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:31.367Z", + "timestamp": "2026-01-15T16:37:33.007Z", "disclosures": [] } }, diff --git a/metadata/modules/ixBidAdapter.json b/metadata/modules/ixBidAdapter.json index 9443e8792b5..819efd5f012 100644 --- a/metadata/modules/ixBidAdapter.json +++ b/metadata/modules/ixBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.indexexchange.com/device_storage_disclosure.json": { - "timestamp": "2025-11-10T11:41:31.933Z", + "timestamp": "2026-01-15T16:37:33.453Z", "disclosures": [ { "identifier": "ix_features", diff --git a/metadata/modules/justIdSystem.json b/metadata/modules/justIdSystem.json index 3d7154320a1..d5da7be5c2a 100644 --- a/metadata/modules/justIdSystem.json +++ b/metadata/modules/justIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://audience-solutions.com/.well-known/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:32.110Z", + "timestamp": "2026-01-15T16:37:33.505Z", "disclosures": [ { "identifier": "__jtuid", diff --git a/metadata/modules/justpremiumBidAdapter.json b/metadata/modules/justpremiumBidAdapter.json index ee71cf8b7ec..dcadb4c351b 100644 --- a/metadata/modules/justpremiumBidAdapter.json +++ b/metadata/modules/justpremiumBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.justpremium.com/devicestoragedisclosures.json": { - "timestamp": "2025-11-10T11:41:32.583Z", + "timestamp": "2026-01-15T16:37:34.004Z", "disclosures": [] } }, diff --git a/metadata/modules/jwplayerBidAdapter.json b/metadata/modules/jwplayerBidAdapter.json index 4ad0327dd16..805c198ba02 100644 --- a/metadata/modules/jwplayerBidAdapter.json +++ b/metadata/modules/jwplayerBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.jwplayer.com/devicestorage.json": { - "timestamp": "2025-11-10T11:41:32.610Z", + "timestamp": "2026-01-15T16:37:34.050Z", "disclosures": [] } }, diff --git a/metadata/modules/kargoBidAdapter.json b/metadata/modules/kargoBidAdapter.json index 922c986ea68..1fdc5fba8a0 100644 --- a/metadata/modules/kargoBidAdapter.json +++ b/metadata/modules/kargoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://storage.cloud.kargo.com/device_storage_disclosure.json": { - "timestamp": "2025-11-10T11:41:32.808Z", + "timestamp": "2026-01-15T16:37:34.212Z", "disclosures": [ { "identifier": "krg_crb", diff --git a/metadata/modules/kueezRtbBidAdapter.json b/metadata/modules/kueezRtbBidAdapter.json index f9b8a4048ea..2152cc568c4 100644 --- a/metadata/modules/kueezRtbBidAdapter.json +++ b/metadata/modules/kueezRtbBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://en.kueez.com/tcf.json": { - "timestamp": "2025-11-10T11:41:32.830Z", + "timestamp": "2026-01-15T16:37:34.231Z", "disclosures": [ { "identifier": "ck48wz12sqj7", diff --git a/metadata/modules/limelightDigitalBidAdapter.json b/metadata/modules/limelightDigitalBidAdapter.json index 5531e1fbfb0..3ff9690a4f1 100644 --- a/metadata/modules/limelightDigitalBidAdapter.json +++ b/metadata/modules/limelightDigitalBidAdapter.json @@ -2,11 +2,11 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://policy.iion.io/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:32.862Z", + "timestamp": "2026-01-15T16:37:34.324Z", "disclosures": [] }, "https://orangeclickmedia.com/device_storage_disclosure.json": { - "timestamp": "2025-11-10T11:41:32.918Z", + "timestamp": "2026-01-15T16:37:34.350Z", "disclosures": [] } }, @@ -129,6 +129,20 @@ "aliasOf": "limelightDigital", "gvlid": null, "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "performist", + "aliasOf": "limelightDigital", + "gvlid": null, + "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "oveeo", + "aliasOf": "limelightDigital", + "gvlid": null, + "disclosureURL": null } ] } \ No newline at end of file diff --git a/metadata/modules/liveIntentIdSystem.json b/metadata/modules/liveIntentIdSystem.json index c02fb7130e4..44c5014b55e 100644 --- a/metadata/modules/liveIntentIdSystem.json +++ b/metadata/modules/liveIntentIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://b-code.liadm.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:32.919Z", + "timestamp": "2026-01-15T16:37:34.351Z", "disclosures": [ { "identifier": "_lc2_fpi", diff --git a/metadata/modules/liveIntentRtdProvider.json b/metadata/modules/liveIntentRtdProvider.json index d47673113cf..a09843ec6ea 100644 --- a/metadata/modules/liveIntentRtdProvider.json +++ b/metadata/modules/liveIntentRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://b-code.liadm.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:32.934Z", + "timestamp": "2026-01-15T16:37:34.363Z", "disclosures": [ { "identifier": "_lc2_fpi", diff --git a/metadata/modules/livewrappedBidAdapter.json b/metadata/modules/livewrappedBidAdapter.json index 015d56e0e41..b79c8ff6a99 100644 --- a/metadata/modules/livewrappedBidAdapter.json +++ b/metadata/modules/livewrappedBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://content.lwadm.com/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:32.935Z", + "timestamp": "2026-01-15T16:37:34.363Z", "disclosures": [ { "identifier": "uid", diff --git a/metadata/modules/loopmeBidAdapter.json b/metadata/modules/loopmeBidAdapter.json index ba28fc75e84..6c0ad174f78 100644 --- a/metadata/modules/loopmeBidAdapter.json +++ b/metadata/modules/loopmeBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://co.loopme.com/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:32.961Z", + "timestamp": "2026-01-15T16:37:34.382Z", "disclosures": [] } }, diff --git a/metadata/modules/lotamePanoramaIdSystem.json b/metadata/modules/lotamePanoramaIdSystem.json index 2a67b8a156e..0582b693bb3 100644 --- a/metadata/modules/lotamePanoramaIdSystem.json +++ b/metadata/modules/lotamePanoramaIdSystem.json @@ -2,19 +2,42 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tags.crwdcntrl.net/privacy/tcf-purposes.json": { - "timestamp": "2025-11-10T11:41:33.059Z", + "timestamp": "2026-01-15T16:37:34.548Z", "disclosures": [ + { + "identifier": "lotame_domain_check", + "type": "cookie", + "maxAgeSeconds": 10, + "cookieRefresh": true, + "purposes": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ] + }, { "identifier": "panoramaId", "type": "web", "purposes": [ 1, + 2, 3, + 4, 5, + 6, 7, 8, 9, - 10 + 10, + 11 ] }, { @@ -22,12 +45,16 @@ "type": "web", "purposes": [ 1, + 2, 3, + 4, 5, + 6, 7, 8, 9, - 10 + 10, + 11 ] }, { @@ -35,12 +62,16 @@ "type": "web", "purposes": [ 1, + 2, 3, + 4, 5, + 6, 7, 8, 9, - 10 + 10, + 11 ] }, { @@ -48,12 +79,16 @@ "type": "web", "purposes": [ 1, + 2, 3, + 4, 5, + 6, 7, 8, 9, - 10 + 10, + 11 ] }, { @@ -61,12 +96,16 @@ "type": "web", "purposes": [ 1, + 2, 3, + 4, 5, + 6, 7, 8, 9, - 10 + 10, + 11 ] }, { @@ -74,12 +113,16 @@ "type": "web", "purposes": [ 1, + 2, 3, + 4, 5, + 6, 7, 8, 9, - 10 + 10, + 11 ] } ] diff --git a/metadata/modules/luponmediaBidAdapter.json b/metadata/modules/luponmediaBidAdapter.json index a37937e5db2..e8c92f62c8a 100644 --- a/metadata/modules/luponmediaBidAdapter.json +++ b/metadata/modules/luponmediaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://luponmedia.com/vendor_device_storage.json": { - "timestamp": "2025-11-10T11:41:33.072Z", + "timestamp": "2026-01-15T16:37:34.564Z", "disclosures": [] } }, diff --git a/metadata/modules/madvertiseBidAdapter.json b/metadata/modules/madvertiseBidAdapter.json index 6208c5458f5..ea6dfa2a956 100644 --- a/metadata/modules/madvertiseBidAdapter.json +++ b/metadata/modules/madvertiseBidAdapter.json @@ -1,8 +1,8 @@ { "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { - "https://mobile.mng-ads.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:33.477Z", + "https://adserver.bluestack.app/deviceStorage.json": { + "timestamp": "2026-01-15T16:37:34.979Z", "disclosures": [] } }, @@ -12,7 +12,7 @@ "componentName": "madvertise", "aliasOf": null, "gvlid": 153, - "disclosureURL": "https://mobile.mng-ads.com/deviceStorage.json" + "disclosureURL": "https://adserver.bluestack.app/deviceStorage.json" } ] } \ No newline at end of file diff --git a/metadata/modules/marsmediaBidAdapter.json b/metadata/modules/marsmediaBidAdapter.json index 05800612bff..e0274e10bd1 100644 --- a/metadata/modules/marsmediaBidAdapter.json +++ b/metadata/modules/marsmediaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://mars.media/apis/tcf-v2.json": { - "timestamp": "2025-11-10T11:41:33.822Z", + "timestamp": "2026-01-15T16:37:35.326Z", "disclosures": [] } }, diff --git a/metadata/modules/mediaConsortiumBidAdapter.json b/metadata/modules/mediaConsortiumBidAdapter.json index 09f223a4d28..1fc1ae2ea50 100644 --- a/metadata/modules/mediaConsortiumBidAdapter.json +++ b/metadata/modules/mediaConsortiumBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.hubvisor.io/assets/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:33.942Z", + "timestamp": "2026-01-15T16:37:35.430Z", "disclosures": [ { "identifier": "hbv:turbo-cmp", diff --git a/metadata/modules/mediaforceBidAdapter.json b/metadata/modules/mediaforceBidAdapter.json index e3f07de5ac6..ebb8d723bb9 100644 --- a/metadata/modules/mediaforceBidAdapter.json +++ b/metadata/modules/mediaforceBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://comparisons.org/privacy.json": { - "timestamp": "2025-11-10T11:41:34.076Z", + "timestamp": "2026-01-15T16:37:35.443Z", "disclosures": [] } }, diff --git a/metadata/modules/mediafuseBidAdapter.json b/metadata/modules/mediafuseBidAdapter.json index b2ba1b7a26e..25195acc2e7 100644 --- a/metadata/modules/mediafuseBidAdapter.json +++ b/metadata/modules/mediafuseBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://acdn.adnxs.com/gvl/1d/xandrdevicestoragedisclosures.json": { - "timestamp": "2025-11-10T11:41:34.094Z", + "timestamp": "2026-01-15T16:37:35.466Z", "disclosures": [] } }, diff --git a/metadata/modules/mediagoBidAdapter.json b/metadata/modules/mediagoBidAdapter.json index e2eaed9c22c..9cb27cd78c3 100644 --- a/metadata/modules/mediagoBidAdapter.json +++ b/metadata/modules/mediagoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.mediago.io/js/tcf.json": { - "timestamp": "2025-11-10T11:41:34.094Z", + "timestamp": "2026-01-15T16:37:35.466Z", "disclosures": [] } }, diff --git a/metadata/modules/mediakeysBidAdapter.json b/metadata/modules/mediakeysBidAdapter.json index 55c10e96d76..794d657bd64 100644 --- a/metadata/modules/mediakeysBidAdapter.json +++ b/metadata/modules/mediakeysBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://s3.eu-west-3.amazonaws.com/adserving.resourcekeys.com/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:34.195Z", + "timestamp": "2026-01-15T16:37:35.488Z", "disclosures": [] } }, diff --git a/metadata/modules/medianetBidAdapter.json b/metadata/modules/medianetBidAdapter.json index dd6e514609f..0ff5cbb42c6 100644 --- a/metadata/modules/medianetBidAdapter.json +++ b/metadata/modules/medianetBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.media.net/tcfv2/gvl/deviceStorage.json": { - "timestamp": "2025-10-31T17:47:02.069Z", + "timestamp": "2026-01-15T16:37:35.768Z", "disclosures": [ { "identifier": "_mNExInsl", @@ -246,7 +246,7 @@ ] }, "https://trustedstack.com/tcf/gvl/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:34.519Z", + "timestamp": "2026-01-15T16:37:35.937Z", "disclosures": [ { "identifier": "usp_status", diff --git a/metadata/modules/mediasquareBidAdapter.json b/metadata/modules/mediasquareBidAdapter.json index a0bc8fa5610..6966b722014 100644 --- a/metadata/modules/mediasquareBidAdapter.json +++ b/metadata/modules/mediasquareBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://mediasquare.fr/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:34.554Z", + "timestamp": "2026-01-15T16:37:36.063Z", "disclosures": [] } }, diff --git a/metadata/modules/mgidBidAdapter.json b/metadata/modules/mgidBidAdapter.json index 09aad00dbbd..a20123dcb4c 100644 --- a/metadata/modules/mgidBidAdapter.json +++ b/metadata/modules/mgidBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.mgid.com/assets/devicestorage.json": { - "timestamp": "2025-11-10T11:41:35.075Z", + "timestamp": "2026-01-15T16:37:36.664Z", "disclosures": [] } }, diff --git a/metadata/modules/mgidRtdProvider.json b/metadata/modules/mgidRtdProvider.json index 6bad4449997..0e646d04b78 100644 --- a/metadata/modules/mgidRtdProvider.json +++ b/metadata/modules/mgidRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.mgid.com/assets/devicestorage.json": { - "timestamp": "2025-11-10T11:41:35.175Z", + "timestamp": "2026-01-15T16:37:36.702Z", "disclosures": [] } }, diff --git a/metadata/modules/mgidXBidAdapter.json b/metadata/modules/mgidXBidAdapter.json index 646fa4474cf..5b116702798 100644 --- a/metadata/modules/mgidXBidAdapter.json +++ b/metadata/modules/mgidXBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.mgid.com/assets/devicestorage.json": { - "timestamp": "2025-11-10T11:41:35.175Z", + "timestamp": "2026-01-15T16:37:36.702Z", "disclosures": [] } }, diff --git a/metadata/modules/minutemediaBidAdapter.json b/metadata/modules/minutemediaBidAdapter.json index dbcfcc91f22..0eb9ee64c1b 100644 --- a/metadata/modules/minutemediaBidAdapter.json +++ b/metadata/modules/minutemediaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://disclosures.mmctsvc.com/device-storage.json": { - "timestamp": "2025-11-10T11:41:35.175Z", + "timestamp": "2026-01-15T16:37:36.703Z", "disclosures": [] } }, diff --git a/metadata/modules/missenaBidAdapter.json b/metadata/modules/missenaBidAdapter.json index 237dfc6c35e..1252aff990a 100644 --- a/metadata/modules/missenaBidAdapter.json +++ b/metadata/modules/missenaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ad.missena.io/iab.json": { - "timestamp": "2025-11-10T11:41:35.192Z", + "timestamp": "2026-01-15T16:37:36.726Z", "disclosures": [] } }, diff --git a/metadata/modules/mobianRtdProvider.json b/metadata/modules/mobianRtdProvider.json index 29d9fbd6480..a2fc13fb1e0 100644 --- a/metadata/modules/mobianRtdProvider.json +++ b/metadata/modules/mobianRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://js.outcomes.net/tcf.json": { - "timestamp": "2025-11-10T11:41:35.247Z", + "timestamp": "2026-01-15T16:37:36.778Z", "disclosures": [] } }, diff --git a/metadata/modules/mobkoiBidAdapter.json b/metadata/modules/mobkoiBidAdapter.json index 3682c594f08..d5e26a184ac 100644 --- a/metadata/modules/mobkoiBidAdapter.json +++ b/metadata/modules/mobkoiBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.maximus.mobkoi.com/tcf/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:35.262Z", + "timestamp": "2026-01-15T16:37:36.797Z", "disclosures": [] } }, diff --git a/metadata/modules/mobkoiIdSystem.json b/metadata/modules/mobkoiIdSystem.json index 959dbad1ba6..2ae98dfd905 100644 --- a/metadata/modules/mobkoiIdSystem.json +++ b/metadata/modules/mobkoiIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.maximus.mobkoi.com/tcf/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:35.279Z", + "timestamp": "2026-01-15T16:37:36.820Z", "disclosures": [] } }, diff --git a/metadata/modules/msftBidAdapter.json b/metadata/modules/msftBidAdapter.json index d3e1eec7f2c..ae4cab1562e 100644 --- a/metadata/modules/msftBidAdapter.json +++ b/metadata/modules/msftBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://acdn.adnxs.com/gvl/1d/xandrdevicestoragedisclosures.json": { - "timestamp": "2025-11-10T11:41:35.280Z", + "timestamp": "2026-01-15T16:37:36.821Z", "disclosures": [] } }, diff --git a/metadata/modules/mycodemediaBidAdapter.json b/metadata/modules/mycodemediaBidAdapter.json new file mode 100644 index 00000000000..6fdf70e5b1f --- /dev/null +++ b/metadata/modules/mycodemediaBidAdapter.json @@ -0,0 +1,13 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": {}, + "components": [ + { + "componentType": "bidder", + "componentName": "mycodemedia", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + } + ] +} \ No newline at end of file diff --git a/metadata/modules/nativeryBidAdapter.json b/metadata/modules/nativeryBidAdapter.json index a7877ff3ab9..7bfb26c39a3 100644 --- a/metadata/modules/nativeryBidAdapter.json +++ b/metadata/modules/nativeryBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdnimg.nativery.com/widget/js/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:35.281Z", + "timestamp": "2026-01-15T16:37:36.821Z", "disclosures": [] } }, diff --git a/metadata/modules/nativoBidAdapter.json b/metadata/modules/nativoBidAdapter.json index c5b513dc3d2..a923f44873c 100644 --- a/metadata/modules/nativoBidAdapter.json +++ b/metadata/modules/nativoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://iab.nativo.com/tcf-disclosures.json": { - "timestamp": "2025-11-10T11:41:35.625Z", + "timestamp": "2026-01-15T16:37:37.150Z", "disclosures": [] } }, diff --git a/metadata/modules/newspassidBidAdapter.json b/metadata/modules/newspassidBidAdapter.json index d824160fc42..597655a3adb 100644 --- a/metadata/modules/newspassidBidAdapter.json +++ b/metadata/modules/newspassidBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.aditude.com/storageaccess.json": { - "timestamp": "2025-11-10T11:41:35.649Z", + "timestamp": "2026-01-15T16:37:37.193Z", "disclosures": [] } }, diff --git a/metadata/modules/nextMillenniumBidAdapter.json b/metadata/modules/nextMillenniumBidAdapter.json index 1578563981a..c7cf8a19060 100644 --- a/metadata/modules/nextMillenniumBidAdapter.json +++ b/metadata/modules/nextMillenniumBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://nextmillennium.io/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:35.649Z", + "timestamp": "2026-01-15T16:37:37.194Z", "disclosures": [] } }, diff --git a/metadata/modules/nextrollBidAdapter.json b/metadata/modules/nextrollBidAdapter.json index f9d610f2ecd..58b70f38288 100644 --- a/metadata/modules/nextrollBidAdapter.json +++ b/metadata/modules/nextrollBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://s.adroll.com/shares/device_storage.json": { - "timestamp": "2025-11-10T11:41:35.707Z", + "timestamp": "2026-01-15T16:37:37.234Z", "disclosures": [ { "identifier": "__adroll_fpc", diff --git a/metadata/modules/nexx360BidAdapter.json b/metadata/modules/nexx360BidAdapter.json index 9d9287dac73..ca5d45f6224 100644 --- a/metadata/modules/nexx360BidAdapter.json +++ b/metadata/modules/nexx360BidAdapter.json @@ -2,19 +2,19 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://fast.nexx360.io/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:35.910Z", + "timestamp": "2026-01-15T16:37:38.067Z", "disclosures": [] }, "https://static.first-id.fr/tcf/cookie.json": { - "timestamp": "2025-11-10T11:41:35.772Z", + "timestamp": "2026-01-15T16:37:37.502Z", "disclosures": [] }, "https://i.plug.it/banners/js/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:35.794Z", + "timestamp": "2026-01-15T16:37:37.537Z", "disclosures": [] }, "https://player.glomex.com/.well-known/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:35.910Z", + "timestamp": "2026-01-15T16:37:37.670Z", "disclosures": [ { "identifier": "glomexUser", @@ -45,12 +45,8 @@ } ] }, - "https://mediafuse.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:35.910Z", - "disclosures": [] - }, "https://gdpr.pubx.ai/devicestoragedisclosure.json": { - "timestamp": "2025-11-10T11:41:35.989Z", + "timestamp": "2026-01-15T16:37:37.670Z", "disclosures": [ { "identifier": "pubx:defaults", @@ -63,6 +59,10 @@ ] } ] + }, + "https://yieldbird.com/devicestorage.json": { + "timestamp": "2026-01-15T16:37:37.690Z", + "disclosures": [] } }, "components": [ @@ -173,17 +173,24 @@ }, { "componentType": "bidder", - "componentName": "revnew", + "componentName": "pubxai", "aliasOf": "nexx360", - "gvlid": 1468, - "disclosureURL": "https://mediafuse.com/deviceStorage.json" + "gvlid": 1485, + "disclosureURL": "https://gdpr.pubx.ai/devicestoragedisclosure.json" }, { "componentType": "bidder", - "componentName": "pubxai", + "componentName": "ybidder", "aliasOf": "nexx360", - "gvlid": 1485, - "disclosureURL": "https://gdpr.pubx.ai/devicestoragedisclosure.json" + "gvlid": 1253, + "disclosureURL": "https://yieldbird.com/devicestorage.json" + }, + { + "componentType": "bidder", + "componentName": "netads", + "aliasOf": "nexx360", + "gvlid": 965, + "disclosureURL": "https://fast.nexx360.io/deviceStorage.json" } ] } \ No newline at end of file diff --git a/metadata/modules/nobidBidAdapter.json b/metadata/modules/nobidBidAdapter.json index cec52faf113..b239da5bb93 100644 --- a/metadata/modules/nobidBidAdapter.json +++ b/metadata/modules/nobidBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://public.servenobid.com/gdpr_tcf/vendor_device_storage_operational_disclosures.json": { - "timestamp": "2025-11-10T11:41:36.013Z", + "timestamp": "2026-01-15T16:37:38.068Z", "disclosures": [] } }, diff --git a/metadata/modules/nodalsAiRtdProvider.json b/metadata/modules/nodalsAiRtdProvider.json index 4674e5efa3d..deaed20650e 100644 --- a/metadata/modules/nodalsAiRtdProvider.json +++ b/metadata/modules/nodalsAiRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://static.nodals.ai/vendor.json": { - "timestamp": "2025-11-10T11:41:36.029Z", + "timestamp": "2026-01-15T16:37:38.080Z", "disclosures": [ { "identifier": "localStorage", diff --git a/metadata/modules/novatiqIdSystem.json b/metadata/modules/novatiqIdSystem.json index 77eb8ef2b81..4c5a6d1875f 100644 --- a/metadata/modules/novatiqIdSystem.json +++ b/metadata/modules/novatiqIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://novatiq.com/privacy/iab/novatiq.json": { - "timestamp": "2025-11-10T11:41:37.212Z", + "timestamp": "2026-01-15T16:37:39.860Z", "disclosures": [ { "identifier": "novatiq", diff --git a/metadata/modules/oguryBidAdapter.json b/metadata/modules/oguryBidAdapter.json index 57c42400301..540937d4b66 100644 --- a/metadata/modules/oguryBidAdapter.json +++ b/metadata/modules/oguryBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://privacy.ogury.co/disclosure.json": { - "timestamp": "2025-11-10T11:41:37.545Z", + "timestamp": "2026-01-15T16:37:40.186Z", "disclosures": [] } }, diff --git a/metadata/modules/omnidexBidAdapter.json b/metadata/modules/omnidexBidAdapter.json index 6c4187335bf..d445aca9249 100644 --- a/metadata/modules/omnidexBidAdapter.json +++ b/metadata/modules/omnidexBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.omni-dex.io/devicestorage.json": { - "timestamp": "2025-11-10T11:41:37.597Z", + "timestamp": "2026-01-15T16:37:40.254Z", "disclosures": [ { "identifier": "ck48wz12sqj7", diff --git a/metadata/modules/omsBidAdapter.json b/metadata/modules/omsBidAdapter.json index 7b5cc5eee49..10ec0a22ddc 100644 --- a/metadata/modules/omsBidAdapter.json +++ b/metadata/modules/omsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.marphezis.com/tcf-vendor-disclosures.json": { - "timestamp": "2025-11-10T11:41:37.637Z", + "timestamp": "2026-01-15T16:37:40.311Z", "disclosures": [] } }, diff --git a/metadata/modules/onetagBidAdapter.json b/metadata/modules/onetagBidAdapter.json index 08783d6ac2a..c2ea0a6753b 100644 --- a/metadata/modules/onetagBidAdapter.json +++ b/metadata/modules/onetagBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://onetag-cdn.com/privacy/tcf_storage.json": { - "timestamp": "2025-11-10T11:41:37.638Z", + "timestamp": "2026-01-15T16:37:40.312Z", "disclosures": [ { "identifier": "onetag_sid", diff --git a/metadata/modules/openwebBidAdapter.json b/metadata/modules/openwebBidAdapter.json index 3bd00fc710a..9e0ee96cbc7 100644 --- a/metadata/modules/openwebBidAdapter.json +++ b/metadata/modules/openwebBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://spotim-prd-static-assets.s3.amazonaws.com/iab/device-storage.json": { - "timestamp": "2025-11-10T11:41:37.930Z", + "timestamp": "2026-01-15T16:37:40.635Z", "disclosures": [] } }, diff --git a/metadata/modules/openxBidAdapter.json b/metadata/modules/openxBidAdapter.json index e37eba3c4eb..429188497e6 100644 --- a/metadata/modules/openxBidAdapter.json +++ b/metadata/modules/openxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.openx.com/device-storage.json": { - "timestamp": "2025-11-10T11:41:37.968Z", + "timestamp": "2026-01-15T16:37:40.673Z", "disclosures": [] } }, diff --git a/metadata/modules/operaadsBidAdapter.json b/metadata/modules/operaadsBidAdapter.json index fec62ac5fa1..9ac3b618f22 100644 --- a/metadata/modules/operaadsBidAdapter.json +++ b/metadata/modules/operaadsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://res.adx.opera.com/dsd.json": { - "timestamp": "2025-11-10T11:41:37.996Z", + "timestamp": "2026-01-15T16:37:40.701Z", "disclosures": [] } }, diff --git a/metadata/modules/optidigitalBidAdapter.json b/metadata/modules/optidigitalBidAdapter.json index a82f6527f99..9b008afc432 100644 --- a/metadata/modules/optidigitalBidAdapter.json +++ b/metadata/modules/optidigitalBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://scripts.opti-digital.com/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:38.012Z", + "timestamp": "2026-01-15T16:37:40.811Z", "disclosures": [] } }, diff --git a/metadata/modules/optoutBidAdapter.json b/metadata/modules/optoutBidAdapter.json index 108b7e5c33b..ce2ca2bf6ca 100644 --- a/metadata/modules/optoutBidAdapter.json +++ b/metadata/modules/optoutBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adserving.optoutadvertising.com/dsd": { - "timestamp": "2025-11-10T11:41:38.068Z", + "timestamp": "2026-01-15T16:37:40.875Z", "disclosures": [] } }, diff --git a/metadata/modules/orbidderBidAdapter.json b/metadata/modules/orbidderBidAdapter.json index e91a209b091..eedd4242b17 100644 --- a/metadata/modules/orbidderBidAdapter.json +++ b/metadata/modules/orbidderBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://orbidder.otto.de/disclosure/dsd.json": { - "timestamp": "2025-11-10T11:41:38.322Z", + "timestamp": "2026-01-15T16:37:41.133Z", "disclosures": [] } }, diff --git a/metadata/modules/outbrainBidAdapter.json b/metadata/modules/outbrainBidAdapter.json index 49e8280d179..3492a09df2e 100644 --- a/metadata/modules/outbrainBidAdapter.json +++ b/metadata/modules/outbrainBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.outbrain.com/privacy/wp-json/privacy/v2/devicestorage.json": { - "timestamp": "2025-11-10T11:41:38.597Z", + "timestamp": "2026-01-15T16:37:41.441Z", "disclosures": [ { "identifier": "dicbo_id", diff --git a/metadata/modules/ozoneBidAdapter.json b/metadata/modules/ozoneBidAdapter.json index 290b550797a..392d823eab5 100644 --- a/metadata/modules/ozoneBidAdapter.json +++ b/metadata/modules/ozoneBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://prebid.the-ozone-project.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:38.800Z", + "timestamp": "2026-01-15T16:37:41.554Z", "disclosures": [] } }, diff --git a/metadata/modules/pairIdSystem.json b/metadata/modules/pairIdSystem.json index ca0f34de279..2b37fcb7b00 100644 --- a/metadata/modules/pairIdSystem.json +++ b/metadata/modules/pairIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.gstatic.com/iabtcf/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:38.960Z", + "timestamp": "2026-01-15T16:37:41.738Z", "disclosures": [ { "identifier": "__gads", diff --git a/metadata/modules/performaxBidAdapter.json b/metadata/modules/performaxBidAdapter.json index 64c9b50f6ed..3bb6bec21e2 100644 --- a/metadata/modules/performaxBidAdapter.json +++ b/metadata/modules/performaxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.performax.cz/device_storage.json": { - "timestamp": "2025-11-10T11:41:38.991Z", + "timestamp": "2026-01-15T16:37:41.770Z", "disclosures": [ { "identifier": "px2uid", diff --git a/metadata/modules/permutiveIdentityManagerIdSystem.json b/metadata/modules/permutiveIdentityManagerIdSystem.json index e8fc0cd1fac..24aa3516648 100644 --- a/metadata/modules/permutiveIdentityManagerIdSystem.json +++ b/metadata/modules/permutiveIdentityManagerIdSystem.json @@ -1,12 +1,285 @@ { "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", - "disclosures": {}, + "disclosures": { + "https://assets.permutive.app/tcf/tcf.json": { + "timestamp": "2026-01-15T16:37:42.260Z", + "disclosures": [ + { + "identifier": "_pdfps", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-models", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-queries", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_prubicons", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_psegs", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pclmc", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pnativo", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-pvc", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-enrichers", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-session", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-misc", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-id", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_psmart", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_paols", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_papns", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pcrdbs", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pcrprs", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pdem-state", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pfwqp", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_ppam", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_prps", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-events-cache", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-loaded", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pclmc", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-tpd", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-consent", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "__permutiveConfigQueryParams", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "events_*", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "keys_*", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-prebid-*", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-id", + "type": "cookie", + "maxAgeSeconds": 15770000, + "cookieRefresh": false, + "purposes": [ + 1 + ] + }, + { + "identifier": "_papns", + "type": "cookie", + "maxAgeSeconds": 15770000, + "cookieRefresh": false, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pdfps", + "type": "cookie", + "maxAgeSeconds": 15770000, + "cookieRefresh": false, + "purposes": [ + 1 + ] + } + ] + } + }, "components": [ { "componentType": "userId", "componentName": "permutiveIdentityManagerId", "gvlid": null, - "disclosureURL": null, + "disclosureURL": "https://assets.permutive.app/tcf/tcf.json", "aliasOf": null } ] diff --git a/metadata/modules/permutiveRtdProvider.json b/metadata/modules/permutiveRtdProvider.json index 0e675450fa8..7f1f84962cc 100644 --- a/metadata/modules/permutiveRtdProvider.json +++ b/metadata/modules/permutiveRtdProvider.json @@ -1,12 +1,285 @@ { "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", - "disclosures": {}, + "disclosures": { + "https://assets.permutive.app/tcf/tcf.json": { + "timestamp": "2026-01-15T16:37:42.449Z", + "disclosures": [ + { + "identifier": "_pdfps", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-models", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-queries", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_prubicons", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_psegs", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pclmc", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pnativo", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-pvc", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-enrichers", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-session", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-misc", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-id", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_psmart", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_paols", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_papns", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pcrdbs", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pcrprs", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pdem-state", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pfwqp", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_ppam", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_prps", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-events-cache", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-loaded", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pclmc", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-tpd", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-consent", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "__permutiveConfigQueryParams", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "events_*", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "keys_*", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-prebid-*", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-id", + "type": "cookie", + "maxAgeSeconds": 15770000, + "cookieRefresh": false, + "purposes": [ + 1 + ] + }, + { + "identifier": "_papns", + "type": "cookie", + "maxAgeSeconds": 15770000, + "cookieRefresh": false, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pdfps", + "type": "cookie", + "maxAgeSeconds": 15770000, + "cookieRefresh": false, + "purposes": [ + 1 + ] + } + ] + } + }, "components": [ { "componentType": "rtd", "componentName": "permutive", "gvlid": null, - "disclosureURL": null + "disclosureURL": "https://assets.permutive.app/tcf/tcf.json" } ] } \ No newline at end of file diff --git a/metadata/modules/pixfutureBidAdapter.json b/metadata/modules/pixfutureBidAdapter.json index 30c66bc2800..b0bd85f10a5 100644 --- a/metadata/modules/pixfutureBidAdapter.json +++ b/metadata/modules/pixfutureBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.pixfuture.com/vendor-disclosures.json": { - "timestamp": "2025-11-10T11:41:39.392Z", + "timestamp": "2026-01-15T16:37:42.450Z", "disclosures": [] } }, diff --git a/metadata/modules/playdigoBidAdapter.json b/metadata/modules/playdigoBidAdapter.json index ed0b3a18556..7c895b7afe8 100644 --- a/metadata/modules/playdigoBidAdapter.json +++ b/metadata/modules/playdigoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://playdigo.com/file.json": { - "timestamp": "2025-11-10T11:41:39.437Z", + "timestamp": "2026-01-15T16:37:42.521Z", "disclosures": [] } }, diff --git a/metadata/modules/prebid-core.json b/metadata/modules/prebid-core.json index 43ccd4b3233..baf381e37d2 100644 --- a/metadata/modules/prebid-core.json +++ b/metadata/modules/prebid-core.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/prebid/probes.json": { - "timestamp": "2025-11-10T11:41:10.866Z", + "timestamp": "2026-01-15T16:36:37.462Z", "disclosures": [ { "identifier": "_rdc*", @@ -23,7 +23,7 @@ ] }, "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/prebid/debugging.json": { - "timestamp": "2025-11-10T11:41:10.868Z", + "timestamp": "2026-01-15T16:36:37.464Z", "disclosures": [ { "identifier": "__*_debugging__", diff --git a/metadata/modules/precisoBidAdapter.json b/metadata/modules/precisoBidAdapter.json index b9c2a45cb76..f10656d416e 100644 --- a/metadata/modules/precisoBidAdapter.json +++ b/metadata/modules/precisoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://preciso.net/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:39.604Z", + "timestamp": "2026-01-15T16:37:42.702Z", "disclosures": [ { "identifier": "XXXXX_viewnew", diff --git a/metadata/modules/prismaBidAdapter.json b/metadata/modules/prismaBidAdapter.json index 3486d61b3d0..16b2425e2b1 100644 --- a/metadata/modules/prismaBidAdapter.json +++ b/metadata/modules/prismaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://fast.nexx360.io/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:39.826Z", + "timestamp": "2026-01-15T16:37:42.737Z", "disclosures": [] } }, diff --git a/metadata/modules/programmaticXBidAdapter.json b/metadata/modules/programmaticXBidAdapter.json index 1c1646f6a3f..134da3c579e 100644 --- a/metadata/modules/programmaticXBidAdapter.json +++ b/metadata/modules/programmaticXBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://progrtb.com/tcf-vendor-disclosures.json": { - "timestamp": "2025-11-10T11:41:39.826Z", + "timestamp": "2026-01-15T16:37:42.737Z", "disclosures": [] } }, diff --git a/metadata/modules/proxistoreBidAdapter.json b/metadata/modules/proxistoreBidAdapter.json index 6ef24660931..360ddff4c0b 100644 --- a/metadata/modules/proxistoreBidAdapter.json +++ b/metadata/modules/proxistoreBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://abs.proxistore.com/assets/json/proxistore_device_storage_disclosure.json": { - "timestamp": "2025-11-10T11:41:39.874Z", + "timestamp": "2026-01-15T16:37:42.815Z", "disclosures": [] } }, diff --git a/metadata/modules/publicGoodBidAdapter.json b/metadata/modules/publicGoodBidAdapter.json new file mode 100644 index 00000000000..a492629c705 --- /dev/null +++ b/metadata/modules/publicGoodBidAdapter.json @@ -0,0 +1,13 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": {}, + "components": [ + { + "componentType": "bidder", + "componentName": "publicgood", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + } + ] +} \ No newline at end of file diff --git a/metadata/modules/publinkIdSystem.json b/metadata/modules/publinkIdSystem.json index de86e2023ec..8defad27662 100644 --- a/metadata/modules/publinkIdSystem.json +++ b/metadata/modules/publinkIdSystem.json @@ -1,8 +1,8 @@ { "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { - "https://s-usweb.dotomi.com/assets/js/taggy-js/2.18.1/device_storage_disclosure.json": { - "timestamp": "2025-11-10T11:41:40.414Z", + "https://s-usweb.dotomi.com/assets/js/taggy-js/2.18.9/device_storage_disclosure.json": { + "timestamp": "2026-01-15T16:37:43.276Z", "disclosures": [ { "identifier": "dtm_status", @@ -554,6 +554,25 @@ 10, 11 ] + }, + { + "identifier": "hConversionEventId", + "type": "cookie", + "maxAgeSeconds": 2592000, + "cookieRefresh": true, + "purposes": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ] } ] } @@ -563,7 +582,7 @@ "componentType": "userId", "componentName": "publinkId", "gvlid": 24, - "disclosureURL": "https://s-usweb.dotomi.com/assets/js/taggy-js/2.18.1/device_storage_disclosure.json", + "disclosureURL": "https://s-usweb.dotomi.com/assets/js/taggy-js/2.18.9/device_storage_disclosure.json", "aliasOf": null } ] diff --git a/metadata/modules/pubmaticBidAdapter.json b/metadata/modules/pubmaticBidAdapter.json index 27509eff221..34d5432788b 100644 --- a/metadata/modules/pubmaticBidAdapter.json +++ b/metadata/modules/pubmaticBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.pubmatic.com/devicestorage.json": { - "timestamp": "2025-11-10T11:41:40.415Z", + "timestamp": "2026-01-15T16:37:43.277Z", "disclosures": [] } }, diff --git a/metadata/modules/pubmaticIdSystem.json b/metadata/modules/pubmaticIdSystem.json index dd65259b412..f5dcca4eaec 100644 --- a/metadata/modules/pubmaticIdSystem.json +++ b/metadata/modules/pubmaticIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.pubmatic.com/devicestorage.json": { - "timestamp": "2025-11-10T11:41:40.430Z", + "timestamp": "2026-01-15T16:37:43.305Z", "disclosures": [] } }, diff --git a/metadata/modules/pulsepointBidAdapter.json b/metadata/modules/pulsepointBidAdapter.json index 40728b0127a..491cbd138b9 100644 --- a/metadata/modules/pulsepointBidAdapter.json +++ b/metadata/modules/pulsepointBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bh.contextweb.com/tcf/vendorInfo.json": { - "timestamp": "2025-11-10T11:41:40.431Z", + "timestamp": "2026-01-15T16:37:43.308Z", "disclosures": [] } }, diff --git a/metadata/modules/quantcastBidAdapter.json b/metadata/modules/quantcastBidAdapter.json index dc67042d79b..26e032e06b1 100644 --- a/metadata/modules/quantcastBidAdapter.json +++ b/metadata/modules/quantcastBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.quantcast.com/.well-known/devicestorage.json": { - "timestamp": "2025-11-10T11:41:40.447Z", + "timestamp": "2026-01-15T16:37:43.326Z", "disclosures": [ { "identifier": "__qca", diff --git a/metadata/modules/quantcastIdSystem.json b/metadata/modules/quantcastIdSystem.json index 8a2449ad5b3..51e190c21fc 100644 --- a/metadata/modules/quantcastIdSystem.json +++ b/metadata/modules/quantcastIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.quantcast.com/.well-known/devicestorage.json": { - "timestamp": "2025-11-10T11:41:40.733Z", + "timestamp": "2026-01-15T16:37:43.510Z", "disclosures": [ { "identifier": "__qca", diff --git a/metadata/modules/r2b2BidAdapter.json b/metadata/modules/r2b2BidAdapter.json index d5bab440210..53bb5eef4b3 100644 --- a/metadata/modules/r2b2BidAdapter.json +++ b/metadata/modules/r2b2BidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://delivery.r2b2.io/cookie_disclosure": { - "timestamp": "2025-11-10T11:41:40.733Z", + "timestamp": "2026-01-15T16:37:43.511Z", "disclosures": [ { "identifier": "AdTrack-hide-*", diff --git a/metadata/modules/readpeakBidAdapter.json b/metadata/modules/readpeakBidAdapter.json index 0eebde88696..4e352b5b4a2 100644 --- a/metadata/modules/readpeakBidAdapter.json +++ b/metadata/modules/readpeakBidAdapter.json @@ -2,8 +2,26 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://static.readpeak.com/tcf/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:41.447Z", - "disclosures": [] + "timestamp": "2026-01-15T16:37:43.857Z", + "disclosures": [ + { + "identifier": "rp_uidfp", + "type": "cookie", + "maxAgeSeconds": 33696000, + "cookieRefresh": true, + "purposes": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 10 + ] + } + ] } }, "components": [ diff --git a/metadata/modules/relayBidAdapter.json b/metadata/modules/relayBidAdapter.json index edc54bb598e..28e495dfacf 100644 --- a/metadata/modules/relayBidAdapter.json +++ b/metadata/modules/relayBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://relay42.com/hubfs/raw_assets/public/IAB.json": { - "timestamp": "2025-11-10T11:41:41.468Z", + "timestamp": "2026-01-15T16:37:43.882Z", "disclosures": [] } }, diff --git a/metadata/modules/relevantdigitalBidAdapter.json b/metadata/modules/relevantdigitalBidAdapter.json index dcdb649ab70..ec0c3661306 100644 --- a/metadata/modules/relevantdigitalBidAdapter.json +++ b/metadata/modules/relevantdigitalBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.relevant-digital.com/resources/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:41.518Z", + "timestamp": "2026-01-15T16:37:43.957Z", "disclosures": [] } }, diff --git a/metadata/modules/resetdigitalBidAdapter.json b/metadata/modules/resetdigitalBidAdapter.json index b791acf4b35..7120af32094 100644 --- a/metadata/modules/resetdigitalBidAdapter.json +++ b/metadata/modules/resetdigitalBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://resetdigital.co/GDPR-TCF.json": { - "timestamp": "2025-11-10T11:41:41.673Z", + "timestamp": "2026-01-15T16:37:44.236Z", "disclosures": [] } }, diff --git a/metadata/modules/responsiveAdsBidAdapter.json b/metadata/modules/responsiveAdsBidAdapter.json index 1d55a0d2a28..fc6c8ed28e2 100644 --- a/metadata/modules/responsiveAdsBidAdapter.json +++ b/metadata/modules/responsiveAdsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://publish.responsiveads.com/tcf/tcf-v2.json": { - "timestamp": "2025-11-10T11:41:41.715Z", + "timestamp": "2026-01-15T16:37:44.276Z", "disclosures": [] } }, diff --git a/metadata/modules/revcontentBidAdapter.json b/metadata/modules/revcontentBidAdapter.json index c2a349520d1..c2733730712 100644 --- a/metadata/modules/revcontentBidAdapter.json +++ b/metadata/modules/revcontentBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://sothebys.revcontent.com/static/device_storage.json": { - "timestamp": "2025-11-10T11:41:41.728Z", + "timestamp": "2026-01-15T16:37:44.307Z", "disclosures": [ { "identifier": "__ID", diff --git a/metadata/modules/revnewBidAdapter.json b/metadata/modules/revnewBidAdapter.json new file mode 100644 index 00000000000..7e4cc7a0a9c --- /dev/null +++ b/metadata/modules/revnewBidAdapter.json @@ -0,0 +1,18 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": { + "https://mediafuse.com/deviceStorage.json": { + "timestamp": "2026-01-15T16:37:44.324Z", + "disclosures": [] + } + }, + "components": [ + { + "componentType": "bidder", + "componentName": "revnew", + "aliasOf": null, + "gvlid": 1468, + "disclosureURL": "https://mediafuse.com/deviceStorage.json" + } + ] +} \ No newline at end of file diff --git a/metadata/modules/rhythmoneBidAdapter.json b/metadata/modules/rhythmoneBidAdapter.json index dfe1d139f00..bbba31a6732 100644 --- a/metadata/modules/rhythmoneBidAdapter.json +++ b/metadata/modules/rhythmoneBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://video.unrulymedia.com/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:41.756Z", + "timestamp": "2026-01-15T16:37:44.423Z", "disclosures": [] } }, diff --git a/metadata/modules/richaudienceBidAdapter.json b/metadata/modules/richaudienceBidAdapter.json index bb6e6192985..1e00a036717 100644 --- a/metadata/modules/richaudienceBidAdapter.json +++ b/metadata/modules/richaudienceBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdnj.richaudience.com/52a26ab9400b2a9f5aabfa20acf3196g.json": { - "timestamp": "2025-11-10T11:41:41.990Z", + "timestamp": "2026-01-15T16:37:44.674Z", "disclosures": [] } }, diff --git a/metadata/modules/ringieraxelspringerBidAdapter.json b/metadata/modules/ringieraxelspringerBidAdapter.json index 8ad5d4bffce..f6563be8111 100644 --- a/metadata/modules/ringieraxelspringerBidAdapter.json +++ b/metadata/modules/ringieraxelspringerBidAdapter.json @@ -10,4 +10,4 @@ "disclosureURL": null } ] -} \ No newline at end of file +} diff --git a/metadata/modules/riseBidAdapter.json b/metadata/modules/riseBidAdapter.json index aa4aff087d3..de0a9024d9d 100644 --- a/metadata/modules/riseBidAdapter.json +++ b/metadata/modules/riseBidAdapter.json @@ -2,11 +2,11 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://d2pm7iglz0b6eq.cloudfront.net/RiseDeviceStorage.json": { - "timestamp": "2025-11-10T11:41:42.071Z", + "timestamp": "2026-01-15T16:37:44.743Z", "disclosures": [] }, "https://spotim-prd-static-assets.s3.amazonaws.com/iab/device-storage.json": { - "timestamp": "2025-11-10T11:41:42.071Z", + "timestamp": "2026-01-15T16:37:44.743Z", "disclosures": [] } }, diff --git a/metadata/modules/rixengineBidAdapter.json b/metadata/modules/rixengineBidAdapter.json index 572d28eaee4..6bd696c38b8 100644 --- a/metadata/modules/rixengineBidAdapter.json +++ b/metadata/modules/rixengineBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.algorix.co/gdpr-disclosure.json": { - "timestamp": "2025-11-10T11:41:42.072Z", + "timestamp": "2026-01-15T16:37:44.744Z", "disclosures": [] } }, diff --git a/metadata/modules/rtbhouseBidAdapter.json b/metadata/modules/rtbhouseBidAdapter.json index dd4d0abfd72..006ca1b53ea 100644 --- a/metadata/modules/rtbhouseBidAdapter.json +++ b/metadata/modules/rtbhouseBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://rtbhouse.com/DeviceStorage.json": { - "timestamp": "2025-11-10T11:41:42.091Z", + "timestamp": "2026-01-15T16:37:44.763Z", "disclosures": [ { "identifier": "_rtbh.*", diff --git a/metadata/modules/rubiconBidAdapter.json b/metadata/modules/rubiconBidAdapter.json index fc5bfafb4f4..5691f393a9e 100644 --- a/metadata/modules/rubiconBidAdapter.json +++ b/metadata/modules/rubiconBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://gdpr.rubiconproject.com/dvplus/devicestoragedisclosure.json": { - "timestamp": "2025-11-10T11:41:42.297Z", + "timestamp": "2026-01-15T16:37:44.966Z", "disclosures": [] } }, diff --git a/metadata/modules/scaliburBidAdapter.json b/metadata/modules/scaliburBidAdapter.json index e31b7701185..adcfd7e9e9e 100644 --- a/metadata/modules/scaliburBidAdapter.json +++ b/metadata/modules/scaliburBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://legal.overwolf.com/docs/overwolf/website/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:42.546Z", + "timestamp": "2026-01-15T16:37:45.225Z", "disclosures": [ { "identifier": "scluid", diff --git a/metadata/modules/screencoreBidAdapter.json b/metadata/modules/screencoreBidAdapter.json index fd9590502cc..ff8615c9419 100644 --- a/metadata/modules/screencoreBidAdapter.json +++ b/metadata/modules/screencoreBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://screencore.io/tcf.json": { - "timestamp": "2025-11-10T11:41:42.564Z", + "timestamp": "2026-01-15T16:37:45.241Z", "disclosures": null } }, diff --git a/metadata/modules/seedingAllianceBidAdapter.json b/metadata/modules/seedingAllianceBidAdapter.json index 3a18152d93b..4aaa039a8a4 100644 --- a/metadata/modules/seedingAllianceBidAdapter.json +++ b/metadata/modules/seedingAllianceBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://s.nativendo.de/cdn/asset/tcf/purpose-specific-storage-and-access-information.json": { - "timestamp": "2025-11-10T11:41:45.154Z", + "timestamp": "2026-01-15T16:37:47.836Z", "disclosures": [] } }, diff --git a/metadata/modules/seedtagBidAdapter.json b/metadata/modules/seedtagBidAdapter.json index f5be8b79668..fad7b182baf 100644 --- a/metadata/modules/seedtagBidAdapter.json +++ b/metadata/modules/seedtagBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tcf.seedtag.com/vendor.json": { - "timestamp": "2025-11-10T11:41:45.174Z", + "timestamp": "2026-01-15T16:37:47.866Z", "disclosures": [] } }, diff --git a/metadata/modules/semantiqRtdProvider.json b/metadata/modules/semantiqRtdProvider.json index 24dfa79011a..6ccc3e5d212 100644 --- a/metadata/modules/semantiqRtdProvider.json +++ b/metadata/modules/semantiqRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://audienzz.com/device_storage_disclosure_vendor_783.json": { - "timestamp": "2025-11-10T11:41:45.175Z", + "timestamp": "2026-01-15T16:37:47.867Z", "disclosures": [] } }, diff --git a/metadata/modules/setupadBidAdapter.json b/metadata/modules/setupadBidAdapter.json index 2cb10596e42..520b2269345 100644 --- a/metadata/modules/setupadBidAdapter.json +++ b/metadata/modules/setupadBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cookies.stpd.cloud/disclosures.json": { - "timestamp": "2025-11-10T11:41:45.221Z", + "timestamp": "2026-01-15T16:37:47.917Z", "disclosures": [] } }, diff --git a/metadata/modules/sevioBidAdapter.json b/metadata/modules/sevioBidAdapter.json index d1046874f78..654c8dbf9a4 100644 --- a/metadata/modules/sevioBidAdapter.json +++ b/metadata/modules/sevioBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://sevio.com/tcf.json": { - "timestamp": "2025-11-10T11:41:45.322Z", + "timestamp": "2026-01-15T16:37:48.004Z", "disclosures": [] } }, diff --git a/metadata/modules/sharedIdSystem.json b/metadata/modules/sharedIdSystem.json index ca116283654..a9ea050df56 100644 --- a/metadata/modules/sharedIdSystem.json +++ b/metadata/modules/sharedIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/prebid/sharedId-optout.json": { - "timestamp": "2025-11-10T11:41:45.458Z", + "timestamp": "2026-01-15T16:37:48.141Z", "disclosures": [ { "identifier": "_pubcid_optout", diff --git a/metadata/modules/sharethroughBidAdapter.json b/metadata/modules/sharethroughBidAdapter.json index 5953d792d2b..8e18328cdd7 100644 --- a/metadata/modules/sharethroughBidAdapter.json +++ b/metadata/modules/sharethroughBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://assets.sharethrough.com/gvl.json": { - "timestamp": "2025-11-10T11:41:45.458Z", + "timestamp": "2026-01-15T16:37:48.143Z", "disclosures": [] } }, diff --git a/metadata/modules/showheroes-bsBidAdapter.json b/metadata/modules/showheroes-bsBidAdapter.json index cea8077208e..bd8a7a75045 100644 --- a/metadata/modules/showheroes-bsBidAdapter.json +++ b/metadata/modules/showheroes-bsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://static-origin.showheroes.com/gvl_storage_disclosure.json": { - "timestamp": "2025-11-10T11:41:45.481Z", + "timestamp": "2026-01-15T16:37:48.167Z", "disclosures": [] } }, diff --git a/metadata/modules/silvermobBidAdapter.json b/metadata/modules/silvermobBidAdapter.json index 7fbe88aac05..2f606177efd 100644 --- a/metadata/modules/silvermobBidAdapter.json +++ b/metadata/modules/silvermobBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://silvermob.com/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:45.920Z", + "timestamp": "2026-01-15T16:37:48.593Z", "disclosures": [] } }, diff --git a/metadata/modules/sirdataRtdProvider.json b/metadata/modules/sirdataRtdProvider.json index 3c41a87e309..a8db42ebcff 100644 --- a/metadata/modules/sirdataRtdProvider.json +++ b/metadata/modules/sirdataRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.sirdata.eu/sirdata_device_storage_disclosure.json": { - "timestamp": "2025-11-10T11:41:45.937Z", + "timestamp": "2026-01-15T16:37:48.616Z", "disclosures": [] } }, diff --git a/metadata/modules/smaatoBidAdapter.json b/metadata/modules/smaatoBidAdapter.json index 27146fff19f..f780a82bde5 100644 --- a/metadata/modules/smaatoBidAdapter.json +++ b/metadata/modules/smaatoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://resources.smaato.com/hubfs/Smaato/IAB/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:46.266Z", + "timestamp": "2026-01-15T16:37:48.940Z", "disclosures": [] } }, diff --git a/metadata/modules/smartadserverBidAdapter.json b/metadata/modules/smartadserverBidAdapter.json index f117814437b..19610d8eb77 100644 --- a/metadata/modules/smartadserverBidAdapter.json +++ b/metadata/modules/smartadserverBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://apps.smartadserver.com/device-storage-disclosures/equativDeviceStorageDisclosures.json": { - "timestamp": "2025-11-10T11:41:46.317Z", + "timestamp": "2026-01-15T16:37:49.013Z", "disclosures": [] } }, diff --git a/metadata/modules/smarthubBidAdapter.json b/metadata/modules/smarthubBidAdapter.json index 30d325335f5..7aff9b5ebd5 100644 --- a/metadata/modules/smarthubBidAdapter.json +++ b/metadata/modules/smarthubBidAdapter.json @@ -78,6 +78,27 @@ "aliasOf": "smarthub", "gvlid": null, "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "amcom", + "aliasOf": "smarthub", + "gvlid": null, + "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "adastra", + "aliasOf": "smarthub", + "gvlid": null, + "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "radiantfusion", + "aliasOf": "smarthub", + "gvlid": null, + "disclosureURL": null } ] } \ No newline at end of file diff --git a/metadata/modules/smartxBidAdapter.json b/metadata/modules/smartxBidAdapter.json index 07fa1a17ab7..c5e653b6777 100644 --- a/metadata/modules/smartxBidAdapter.json +++ b/metadata/modules/smartxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.smartclip.net/iab/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:46.319Z", + "timestamp": "2026-01-15T16:37:49.014Z", "disclosures": [] } }, diff --git a/metadata/modules/smartyadsBidAdapter.json b/metadata/modules/smartyadsBidAdapter.json index b9e48773cbd..d06600cd59e 100644 --- a/metadata/modules/smartyadsBidAdapter.json +++ b/metadata/modules/smartyadsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://smartyads.com/tcf.json": { - "timestamp": "2025-11-10T11:41:46.336Z", + "timestamp": "2026-01-15T16:37:49.042Z", "disclosures": [] } }, diff --git a/metadata/modules/smilewantedBidAdapter.json b/metadata/modules/smilewantedBidAdapter.json index cc747478779..d8bce380ff2 100644 --- a/metadata/modules/smilewantedBidAdapter.json +++ b/metadata/modules/smilewantedBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://smilewanted.com/vendor-device-storage-disclosures.json": { - "timestamp": "2025-11-10T11:41:46.376Z", + "timestamp": "2026-01-15T16:37:49.113Z", "disclosures": [] } }, diff --git a/metadata/modules/snigelBidAdapter.json b/metadata/modules/snigelBidAdapter.json index 3b61b7c838d..42e0760b1f1 100644 --- a/metadata/modules/snigelBidAdapter.json +++ b/metadata/modules/snigelBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.snigelweb.com/gvl/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:46.841Z", + "timestamp": "2026-01-15T16:37:49.560Z", "disclosures": [] } }, diff --git a/metadata/modules/sonaradsBidAdapter.json b/metadata/modules/sonaradsBidAdapter.json index 60ea9f904a8..c289db6361c 100644 --- a/metadata/modules/sonaradsBidAdapter.json +++ b/metadata/modules/sonaradsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bridgeupp.com/device-storage-disclosure.json": { - "timestamp": "2025-11-10T11:41:46.872Z", + "timestamp": "2026-01-15T16:37:49.595Z", "disclosures": [] } }, diff --git a/metadata/modules/sonobiBidAdapter.json b/metadata/modules/sonobiBidAdapter.json index a46e281974b..4cdd313805f 100644 --- a/metadata/modules/sonobiBidAdapter.json +++ b/metadata/modules/sonobiBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://sonobi.com/tcf2-device-storage-disclosure.json": { - "timestamp": "2025-11-10T11:41:47.090Z", + "timestamp": "2026-01-15T16:37:49.850Z", "disclosures": [] } }, diff --git a/metadata/modules/sovrnBidAdapter.json b/metadata/modules/sovrnBidAdapter.json index 9715b82c6ed..cb8b86f38e1 100644 --- a/metadata/modules/sovrnBidAdapter.json +++ b/metadata/modules/sovrnBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.sovrn.com/tcf-cookie-disclosure/disclosure.json": { - "timestamp": "2025-11-10T11:41:47.321Z", + "timestamp": "2026-01-15T16:37:50.088Z", "disclosures": [] } }, diff --git a/metadata/modules/sparteoBidAdapter.json b/metadata/modules/sparteoBidAdapter.json index fa04ecadb59..47a848b42b8 100644 --- a/metadata/modules/sparteoBidAdapter.json +++ b/metadata/modules/sparteoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bid.bricks-co.com/.well-known/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:47.347Z", + "timestamp": "2026-01-15T16:37:50.111Z", "disclosures": [ { "identifier": "fastCMP-addtlConsent", diff --git a/metadata/modules/ssmasBidAdapter.json b/metadata/modules/ssmasBidAdapter.json index 4ea42a6ab1e..71d6b3a4633 100644 --- a/metadata/modules/ssmasBidAdapter.json +++ b/metadata/modules/ssmasBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://semseoymas.com/iab.json": { - "timestamp": "2025-11-10T11:41:47.624Z", + "timestamp": "2026-01-15T16:37:50.388Z", "disclosures": null } }, diff --git a/metadata/modules/sspBCBidAdapter.json b/metadata/modules/sspBCBidAdapter.json index 910fa68f0ad..79295390096 100644 --- a/metadata/modules/sspBCBidAdapter.json +++ b/metadata/modules/sspBCBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ssp.wp.pl/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:48.258Z", + "timestamp": "2026-01-15T16:37:50.998Z", "disclosures": null } }, diff --git a/metadata/modules/stackadaptBidAdapter.json b/metadata/modules/stackadaptBidAdapter.json index 78bcc92cc5d..1df2f052d7e 100644 --- a/metadata/modules/stackadaptBidAdapter.json +++ b/metadata/modules/stackadaptBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://s3.amazonaws.com/stackadapt_public/disclosures.json": { - "timestamp": "2025-11-10T11:41:48.259Z", + "timestamp": "2026-01-15T16:37:50.999Z", "disclosures": [ { "identifier": "sa-camp-*", diff --git a/metadata/modules/startioBidAdapter.json b/metadata/modules/startioBidAdapter.json index 553db156bb7..8811bbec80f 100644 --- a/metadata/modules/startioBidAdapter.json +++ b/metadata/modules/startioBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://info.startappservice.com/tcf/start.io_domains.json": { - "timestamp": "2025-11-10T11:41:48.290Z", + "timestamp": "2026-01-15T16:37:51.031Z", "disclosures": [] } }, diff --git a/metadata/modules/stroeerCoreBidAdapter.json b/metadata/modules/stroeerCoreBidAdapter.json index 2cd45e7dbff..0fb4f1597c7 100644 --- a/metadata/modules/stroeerCoreBidAdapter.json +++ b/metadata/modules/stroeerCoreBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.stroeer.de/StroeerSSP_deviceStorage.json": { - "timestamp": "2025-11-10T11:41:48.302Z", + "timestamp": "2026-01-15T16:37:51.054Z", "disclosures": [] } }, diff --git a/metadata/modules/stvBidAdapter.json b/metadata/modules/stvBidAdapter.json index 0d3bb5ca220..1f9fc8193a5 100644 --- a/metadata/modules/stvBidAdapter.json +++ b/metadata/modules/stvBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tcf.adtech.app/gen/deviceStorageDisclosure/stv.json": { - "timestamp": "2025-11-10T11:41:48.710Z", + "timestamp": "2026-01-15T16:38:46.504Z", "disclosures": [] } }, diff --git a/metadata/modules/sublimeBidAdapter.json b/metadata/modules/sublimeBidAdapter.json index c60d81765d4..b984f7d1d90 100644 --- a/metadata/modules/sublimeBidAdapter.json +++ b/metadata/modules/sublimeBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://gdpr.ayads.co/cookiepolicy.json": { - "timestamp": "2025-11-10T11:41:49.253Z", + "timestamp": "2026-01-15T16:38:47.144Z", "disclosures": [ { "identifier": "dnt", diff --git a/metadata/modules/taboolaBidAdapter.json b/metadata/modules/taboolaBidAdapter.json index a2cb1f2a552..2981daa8eae 100644 --- a/metadata/modules/taboolaBidAdapter.json +++ b/metadata/modules/taboolaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://accessrequest.taboola.com/iab-tcf-v2-disclosure.json": { - "timestamp": "2025-11-10T11:41:49.275Z", + "timestamp": "2026-01-15T16:38:47.178Z", "disclosures": [ { "identifier": "trc_cookie_storage", @@ -240,17 +240,28 @@ { "identifier": "taboola:shopify:test", "type": "web", - "maxAgeSeconds": null + "maxAgeSeconds": null, + "purposes": [ + 1 + ] }, { "identifier": "taboola:shopify:enable_debug_logging", "type": "web", - "maxAgeSeconds": null + "maxAgeSeconds": null, + "purposes": [ + 1, + 10 + ] }, { "identifier": "taboola:shopify:pixel_allow_checkout_start", "type": "web", - "maxAgeSeconds": null + "maxAgeSeconds": null, + "purposes": [ + 1, + 3 + ] }, { "identifier": "taboola:shopify:page_view", diff --git a/metadata/modules/taboolaIdSystem.json b/metadata/modules/taboolaIdSystem.json index 6a276c7f121..173e6b061aa 100644 --- a/metadata/modules/taboolaIdSystem.json +++ b/metadata/modules/taboolaIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://accessrequest.taboola.com/iab-tcf-v2-disclosure.json": { - "timestamp": "2025-11-10T11:41:49.922Z", + "timestamp": "2026-01-15T16:38:47.840Z", "disclosures": [ { "identifier": "trc_cookie_storage", @@ -240,17 +240,28 @@ { "identifier": "taboola:shopify:test", "type": "web", - "maxAgeSeconds": null + "maxAgeSeconds": null, + "purposes": [ + 1 + ] }, { "identifier": "taboola:shopify:enable_debug_logging", "type": "web", - "maxAgeSeconds": null + "maxAgeSeconds": null, + "purposes": [ + 1, + 10 + ] }, { "identifier": "taboola:shopify:pixel_allow_checkout_start", "type": "web", - "maxAgeSeconds": null + "maxAgeSeconds": null, + "purposes": [ + 1, + 3 + ] }, { "identifier": "taboola:shopify:page_view", diff --git a/metadata/modules/tadvertisingBidAdapter.json b/metadata/modules/tadvertisingBidAdapter.json index ee6e9263f76..18d2400be19 100644 --- a/metadata/modules/tadvertisingBidAdapter.json +++ b/metadata/modules/tadvertisingBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tcf.emetriq.de/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:49.922Z", + "timestamp": "2026-01-15T16:38:47.841Z", "disclosures": [] } }, diff --git a/metadata/modules/tappxBidAdapter.json b/metadata/modules/tappxBidAdapter.json index 05d240778e9..b3d3a8d5324 100644 --- a/metadata/modules/tappxBidAdapter.json +++ b/metadata/modules/tappxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tappx.com/devicestorage.json": { - "timestamp": "2025-11-10T11:41:49.923Z", + "timestamp": "2026-01-15T16:38:47.842Z", "disclosures": [] } }, diff --git a/metadata/modules/targetVideoBidAdapter.json b/metadata/modules/targetVideoBidAdapter.json index 8e27bc2dba4..db43da23ed7 100644 --- a/metadata/modules/targetVideoBidAdapter.json +++ b/metadata/modules/targetVideoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://target-video.com/vendors-device-storage-and-operational-disclosures.json": { - "timestamp": "2025-11-10T11:41:49.956Z", + "timestamp": "2026-01-15T16:38:47.868Z", "disclosures": [ { "identifier": "brid_location", diff --git a/metadata/modules/teadsBidAdapter.json b/metadata/modules/teadsBidAdapter.json index 95500baecbb..f50e5368bc6 100644 --- a/metadata/modules/teadsBidAdapter.json +++ b/metadata/modules/teadsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://iab-cookie-disclosure.teads.tv/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:49.956Z", + "timestamp": "2026-01-15T16:38:47.868Z", "disclosures": [] } }, diff --git a/metadata/modules/teadsIdSystem.json b/metadata/modules/teadsIdSystem.json index 74e7becdbc9..7d8a2c226ed 100644 --- a/metadata/modules/teadsIdSystem.json +++ b/metadata/modules/teadsIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://iab-cookie-disclosure.teads.tv/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:49.972Z", + "timestamp": "2026-01-15T16:38:47.887Z", "disclosures": [] } }, diff --git a/metadata/modules/tealBidAdapter.json b/metadata/modules/tealBidAdapter.json index e00d6c1ff8a..c0f619afc0a 100644 --- a/metadata/modules/tealBidAdapter.json +++ b/metadata/modules/tealBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://c.bids.ws/iab/disclosures.json": { - "timestamp": "2025-11-10T11:41:49.973Z", + "timestamp": "2026-01-15T16:38:47.887Z", "disclosures": [] } }, diff --git a/metadata/modules/tncIdSystem.json b/metadata/modules/tncIdSystem.json index b0c41f66ad0..2fb0f3c1db2 100644 --- a/metadata/modules/tncIdSystem.json +++ b/metadata/modules/tncIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://js.tncid.app/iab-tcf-device-storage-disclosure.json": { - "timestamp": "2025-11-10T11:41:50.001Z", + "timestamp": "2026-01-15T16:38:48.032Z", "disclosures": [] } }, diff --git a/metadata/modules/topicsFpdModule.json b/metadata/modules/topicsFpdModule.json index 4f3af0090f7..dee202714a9 100644 --- a/metadata/modules/topicsFpdModule.json +++ b/metadata/modules/topicsFpdModule.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/prebid/topicsFpdModule.json": { - "timestamp": "2025-11-10T11:41:10.869Z", + "timestamp": "2026-01-15T16:36:37.465Z", "disclosures": [ { "identifier": "prebid:topics", diff --git a/metadata/modules/toponBidAdapter.json b/metadata/modules/toponBidAdapter.json new file mode 100644 index 00000000000..ccc04d95693 --- /dev/null +++ b/metadata/modules/toponBidAdapter.json @@ -0,0 +1,18 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": { + "https://mores.toponad.net/tmp/tpn/toponads_tcf_disclosure.json": { + "timestamp": "2026-01-15T16:38:48.048Z", + "disclosures": [] + } + }, + "components": [ + { + "componentType": "bidder", + "componentName": "topon", + "aliasOf": null, + "gvlid": 1305, + "disclosureURL": "https://mores.toponad.net/tmp/tpn/toponads_tcf_disclosure.json" + } + ] +} \ No newline at end of file diff --git a/metadata/modules/tripleliftBidAdapter.json b/metadata/modules/tripleliftBidAdapter.json index 10eccf8ed29..d58b64b460a 100644 --- a/metadata/modules/tripleliftBidAdapter.json +++ b/metadata/modules/tripleliftBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://triplelift.com/.well-known/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:50.019Z", + "timestamp": "2026-01-15T16:38:48.082Z", "disclosures": [] } }, diff --git a/metadata/modules/trustxBidAdapter.json b/metadata/modules/trustxBidAdapter.json new file mode 100644 index 00000000000..5f4821f2f56 --- /dev/null +++ b/metadata/modules/trustxBidAdapter.json @@ -0,0 +1,13 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": {}, + "components": [ + { + "componentType": "bidder", + "componentName": "trustx", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + } + ] +} \ No newline at end of file diff --git a/metadata/modules/ttdBidAdapter.json b/metadata/modules/ttdBidAdapter.json index 23f05094160..3dd929a01d8 100644 --- a/metadata/modules/ttdBidAdapter.json +++ b/metadata/modules/ttdBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ttd-misc-public-assets.s3.us-west-2.amazonaws.com/deviceStorageDisclosureURL.json": { - "timestamp": "2025-11-10T11:41:50.055Z", + "timestamp": "2026-01-15T16:38:48.122Z", "disclosures": [] } }, diff --git a/metadata/modules/twistDigitalBidAdapter.json b/metadata/modules/twistDigitalBidAdapter.json index baf0cf9234d..a0187d7fd7b 100644 --- a/metadata/modules/twistDigitalBidAdapter.json +++ b/metadata/modules/twistDigitalBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://twistdigital.net/iab.json": { - "timestamp": "2025-11-10T11:41:50.055Z", + "timestamp": "2026-01-15T16:38:48.122Z", "disclosures": [ { "identifier": "vdzj1_{id}", diff --git a/metadata/modules/underdogmediaBidAdapter.json b/metadata/modules/underdogmediaBidAdapter.json index cd930f8c082..131d52a4487 100644 --- a/metadata/modules/underdogmediaBidAdapter.json +++ b/metadata/modules/underdogmediaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bid.underdog.media/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:50.096Z", + "timestamp": "2026-01-15T16:38:48.185Z", "disclosures": [] } }, diff --git a/metadata/modules/undertoneBidAdapter.json b/metadata/modules/undertoneBidAdapter.json index 74ab388c170..c0a24a2a7d8 100644 --- a/metadata/modules/undertoneBidAdapter.json +++ b/metadata/modules/undertoneBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.undertone.com/js/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:50.138Z", + "timestamp": "2026-01-15T16:38:48.215Z", "disclosures": [] } }, diff --git a/metadata/modules/unifiedIdSystem.json b/metadata/modules/unifiedIdSystem.json index ddd4738a1b3..c6b154a8c51 100644 --- a/metadata/modules/unifiedIdSystem.json +++ b/metadata/modules/unifiedIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ttd-misc-public-assets.s3.us-west-2.amazonaws.com/deviceStorageDisclosureURL.json": { - "timestamp": "2025-11-10T11:41:50.150Z", + "timestamp": "2026-01-15T16:38:48.238Z", "disclosures": [] } }, diff --git a/metadata/modules/uniquest_widgetBidAdapter.json b/metadata/modules/uniquest_widgetBidAdapter.json new file mode 100644 index 00000000000..6caf1c1516d --- /dev/null +++ b/metadata/modules/uniquest_widgetBidAdapter.json @@ -0,0 +1,13 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": {}, + "components": [ + { + "componentType": "bidder", + "componentName": "uniquest_widget", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + } + ] +} \ No newline at end of file diff --git a/metadata/modules/unrulyBidAdapter.json b/metadata/modules/unrulyBidAdapter.json index 8c59ce6b2ed..a63ad90c48e 100644 --- a/metadata/modules/unrulyBidAdapter.json +++ b/metadata/modules/unrulyBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://video.unrulymedia.com/deviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:50.151Z", + "timestamp": "2026-01-15T16:38:48.239Z", "disclosures": [] } }, diff --git a/metadata/modules/userId.json b/metadata/modules/userId.json index e458514d045..3534b6b7277 100644 --- a/metadata/modules/userId.json +++ b/metadata/modules/userId.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/prebid/userId-optout.json": { - "timestamp": "2025-11-10T11:41:10.871Z", + "timestamp": "2026-01-15T16:36:37.466Z", "disclosures": [ { "identifier": "_pbjs_id_optout", diff --git a/metadata/modules/utiqIdSystem.json b/metadata/modules/utiqIdSystem.json index a61cd049897..8bbeb65d6c3 100644 --- a/metadata/modules/utiqIdSystem.json +++ b/metadata/modules/utiqIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/modules/utiqDeviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:50.151Z", + "timestamp": "2026-01-15T16:38:48.239Z", "disclosures": [ { "identifier": "utiqPass", diff --git a/metadata/modules/utiqMtpIdSystem.json b/metadata/modules/utiqMtpIdSystem.json index 94368486ebd..c62580098cf 100644 --- a/metadata/modules/utiqMtpIdSystem.json +++ b/metadata/modules/utiqMtpIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/modules/utiqDeviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:50.152Z", + "timestamp": "2026-01-15T16:38:48.240Z", "disclosures": [ { "identifier": "utiqPass", diff --git a/metadata/modules/validationFpdModule.json b/metadata/modules/validationFpdModule.json index 557fba73ab0..887fe2922df 100644 --- a/metadata/modules/validationFpdModule.json +++ b/metadata/modules/validationFpdModule.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/prebid/sharedId-optout.json": { - "timestamp": "2025-11-10T11:41:10.870Z", + "timestamp": "2026-01-15T16:36:37.465Z", "disclosures": [ { "identifier": "_pubcid_optout", diff --git a/metadata/modules/valuadBidAdapter.json b/metadata/modules/valuadBidAdapter.json index 707ff346c82..e2fbe5c76d2 100644 --- a/metadata/modules/valuadBidAdapter.json +++ b/metadata/modules/valuadBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.valuad.cloud/tcfdevice.json": { - "timestamp": "2025-11-10T11:41:50.153Z", + "timestamp": "2026-01-15T16:38:48.240Z", "disclosures": [] } }, diff --git a/metadata/modules/vidazooBidAdapter.json b/metadata/modules/vidazooBidAdapter.json index 3668e9c6ed0..42035967158 100644 --- a/metadata/modules/vidazooBidAdapter.json +++ b/metadata/modules/vidazooBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://vidazoo.com/gdpr-tcf/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:50.374Z", + "timestamp": "2026-01-15T16:38:48.480Z", "disclosures": [ { "identifier": "ck48wz12sqj7", diff --git a/metadata/modules/vidoomyBidAdapter.json b/metadata/modules/vidoomyBidAdapter.json index b1c3730fa2c..a5671abf15a 100644 --- a/metadata/modules/vidoomyBidAdapter.json +++ b/metadata/modules/vidoomyBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://vidoomy.com/storageurl/devicestoragediscurl.json": { - "timestamp": "2025-11-10T11:41:50.428Z", + "timestamp": "2026-01-15T16:38:48.591Z", "disclosures": [] } }, diff --git a/metadata/modules/viouslyBidAdapter.json b/metadata/modules/viouslyBidAdapter.json index 639b38d5fb4..f1c0a00e035 100644 --- a/metadata/modules/viouslyBidAdapter.json +++ b/metadata/modules/viouslyBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bid.bricks-co.com/.well-known/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:50.685Z", + "timestamp": "2026-01-15T16:38:48.726Z", "disclosures": [ { "identifier": "fastCMP-addtlConsent", diff --git a/metadata/modules/visxBidAdapter.json b/metadata/modules/visxBidAdapter.json index 7e245658def..b500aabe29f 100644 --- a/metadata/modules/visxBidAdapter.json +++ b/metadata/modules/visxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.yoc.com/visx/sellers/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:50.686Z", + "timestamp": "2026-01-15T16:38:48.726Z", "disclosures": [ { "identifier": "__vads", diff --git a/metadata/modules/vlybyBidAdapter.json b/metadata/modules/vlybyBidAdapter.json index 48ea4a5f5e3..17b7f554bcb 100644 --- a/metadata/modules/vlybyBidAdapter.json +++ b/metadata/modules/vlybyBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.vlyby.com/conf/iab/gvl.json": { - "timestamp": "2025-11-10T11:41:50.873Z", + "timestamp": "2026-01-15T16:38:48.906Z", "disclosures": [] } }, diff --git a/metadata/modules/voxBidAdapter.json b/metadata/modules/voxBidAdapter.json index b730e79f892..860bc867b2e 100644 --- a/metadata/modules/voxBidAdapter.json +++ b/metadata/modules/voxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://st.hybrid.ai/policy/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:51.151Z", + "timestamp": "2026-01-15T16:38:49.111Z", "disclosures": [] } }, diff --git a/metadata/modules/vrtcalBidAdapter.json b/metadata/modules/vrtcalBidAdapter.json index e399ed2ecfa..a20427f1a9e 100644 --- a/metadata/modules/vrtcalBidAdapter.json +++ b/metadata/modules/vrtcalBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://vrtcal.com/docs/gdpr-tcf-disclosures.json": { - "timestamp": "2025-11-10T11:41:51.151Z", + "timestamp": "2026-01-15T16:38:49.112Z", "disclosures": [] } }, diff --git a/metadata/modules/vuukleBidAdapter.json b/metadata/modules/vuukleBidAdapter.json index 94f8854e5b5..39e10c3551a 100644 --- a/metadata/modules/vuukleBidAdapter.json +++ b/metadata/modules/vuukleBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.vuukle.com/data-privacy/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:51.344Z", + "timestamp": "2026-01-15T16:38:49.128Z", "disclosures": [ { "identifier": "vuukle_token", diff --git a/metadata/modules/weboramaRtdProvider.json b/metadata/modules/weboramaRtdProvider.json index f35724ba33e..5c3db7c2674 100644 --- a/metadata/modules/weboramaRtdProvider.json +++ b/metadata/modules/weboramaRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://weborama.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:51.609Z", + "timestamp": "2026-01-15T16:38:49.424Z", "disclosures": [] } }, diff --git a/metadata/modules/welectBidAdapter.json b/metadata/modules/welectBidAdapter.json index e81651fc5d9..ca3f2aee085 100644 --- a/metadata/modules/welectBidAdapter.json +++ b/metadata/modules/welectBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.welect.de/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:51.862Z", + "timestamp": "2026-01-15T16:38:49.671Z", "disclosures": [] } }, diff --git a/metadata/modules/yahooAdsBidAdapter.json b/metadata/modules/yahooAdsBidAdapter.json index 72477d44571..04a7ec8fb4c 100644 --- a/metadata/modules/yahooAdsBidAdapter.json +++ b/metadata/modules/yahooAdsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://meta.legal.yahoo.com/iab-tcf/v2/device-storage-disclosure.json": { - "timestamp": "2025-11-10T11:41:52.261Z", + "timestamp": "2026-01-15T16:38:50.046Z", "disclosures": [ { "identifier": "vmcid", diff --git a/metadata/modules/yieldlabBidAdapter.json b/metadata/modules/yieldlabBidAdapter.json index 8b463642ed0..e29d2c74115 100644 --- a/metadata/modules/yieldlabBidAdapter.json +++ b/metadata/modules/yieldlabBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ad.yieldlab.net/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:52.262Z", + "timestamp": "2026-01-15T16:38:50.047Z", "disclosures": [] } }, diff --git a/metadata/modules/yieldloveBidAdapter.json b/metadata/modules/yieldloveBidAdapter.json index 0bc1db14bcf..46ad12670e1 100644 --- a/metadata/modules/yieldloveBidAdapter.json +++ b/metadata/modules/yieldloveBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn-a.yieldlove.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:52.368Z", + "timestamp": "2026-01-15T16:38:50.171Z", "disclosures": [ { "identifier": "session_id", diff --git a/metadata/modules/yieldmoBidAdapter.json b/metadata/modules/yieldmoBidAdapter.json index b3c00bee6ba..14184f77daa 100644 --- a/metadata/modules/yieldmoBidAdapter.json +++ b/metadata/modules/yieldmoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://devicestoragedisclosureurl.yieldmo.com/deviceStorage.json": { - "timestamp": "2025-11-10T11:41:52.383Z", + "timestamp": "2026-01-15T16:38:50.194Z", "disclosures": [] } }, diff --git a/metadata/modules/zeotapIdPlusIdSystem.json b/metadata/modules/zeotapIdPlusIdSystem.json index 855820dd6ca..e418d54c7f4 100644 --- a/metadata/modules/zeotapIdPlusIdSystem.json +++ b/metadata/modules/zeotapIdPlusIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://spl.zeotap.com/assets/iab-disclosure.json": { - "timestamp": "2025-11-10T11:41:52.487Z", + "timestamp": "2026-01-15T16:38:50.278Z", "disclosures": [] } }, diff --git a/metadata/modules/zeta_globalBidAdapter.json b/metadata/modules/zeta_globalBidAdapter.json index b0ed82623ce..eb5f832ef38 100644 --- a/metadata/modules/zeta_globalBidAdapter.json +++ b/metadata/modules/zeta_globalBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://zetaglobal.com/ZetaDeviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:52.598Z", + "timestamp": "2026-01-15T16:38:50.392Z", "disclosures": [] } }, diff --git a/metadata/modules/zeta_global_sspBidAdapter.json b/metadata/modules/zeta_global_sspBidAdapter.json index 4b1ff7114f9..e35e3b2d193 100644 --- a/metadata/modules/zeta_global_sspBidAdapter.json +++ b/metadata/modules/zeta_global_sspBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://zetaglobal.com/ZetaDeviceStorageDisclosure.json": { - "timestamp": "2025-11-10T11:41:52.688Z", + "timestamp": "2026-01-15T16:38:50.477Z", "disclosures": [] } }, diff --git a/metadata/overrides.mjs b/metadata/overrides.mjs index bb722cfd4f2..70d29f19722 100644 --- a/metadata/overrides.mjs +++ b/metadata/overrides.mjs @@ -17,5 +17,6 @@ export default { relevadRtdProvider: 'RelevadRTDModule', sirdataRtdProvider: 'SirdataRTDModule', fanBidAdapter: 'freedomadnetwork', - uniquestWidgetBidAdapter: 'uniquest_widget' + uniquestWidgetBidAdapter: 'uniquest_widget', + ringieraxelspringerBidAdapter: 'das' } diff --git a/modules.json b/modules.json index 17aacc47667..5f72b80673f 100644 --- a/modules.json +++ b/modules.json @@ -34,5 +34,7 @@ "criteoIdSystem", "connectadBidAdapter", "gptPreAuction", - "equativBidAdapter" + "equativBidAdapter", + "allegroBidAdapter", + "richaudienceBidAdapter" ] diff --git a/modules/.submodules.json b/modules/.submodules.json index 791f7ed822b..c6274fdcbfd 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -111,6 +111,7 @@ "mobianRtdProvider", "neuwoRtdProvider", "nodalsAiRtdProvider", + "oftmediaRtdProvider", "oneKeyRtdProvider", "optableRtdProvider", "optimeraRtdProvider", diff --git a/modules/51DegreesRtdProvider.js b/modules/51DegreesRtdProvider.js index 44870c97849..f5c76357ffc 100644 --- a/modules/51DegreesRtdProvider.js +++ b/modules/51DegreesRtdProvider.js @@ -227,6 +227,7 @@ export const convert51DegreesDataToOrtb2 = (data51) => { * @param {number} [device.screenpixelsphysicalwidth] Screen physical width in pixels * @param {number} [device.pixelratio] Pixel ratio * @param {number} [device.screeninchesheight] Screen height in inches + * @param {string} [device.thirdpartycookiesenabled] Third-party cookies enabled * * @returns {Object} Enriched ORTB2 object */ @@ -261,7 +262,12 @@ export const convert51DegreesDeviceToOrtb2 = (device) => { deepSetNotEmptyValue(ortb2Device, 'w', device.screenpixelsphysicalwidth || device.screenpixelswidth); deepSetNotEmptyValue(ortb2Device, 'pxratio', device.pixelratio); deepSetNotEmptyValue(ortb2Device, 'ppi', devicePhysicalPPI || devicePPI); + // kept for backward compatibility deepSetNotEmptyValue(ortb2Device, 'ext.fiftyonedegrees_deviceId', device.deviceid); + deepSetNotEmptyValue(ortb2Device, 'ext.fod.deviceId', device.deviceid); + if (['True', 'False'].includes(device.thirdpartycookiesenabled)) { + deepSetValue(ortb2Device, 'ext.fod.tpc', device.thirdpartycookiesenabled === 'True' ? 1 : 0); + } return {device: ortb2Device}; } diff --git a/modules/51DegreesRtdProvider.md b/modules/51DegreesRtdProvider.md index 76fa73803c9..db4b930c25e 100644 --- a/modules/51DegreesRtdProvider.md +++ b/modules/51DegreesRtdProvider.md @@ -8,13 +8,17 @@ ## Description -The 51Degrees module enriches an OpenRTB request with [51Degrees Device Data](https://51degrees.com/documentation/index.html). +51Degrees module enriches an OpenRTB request with [51Degrees Device Data](https://51degrees.com/documentation/index.html). -The 51Degrees module sets the following fields of the device object: `devicetype`, `make`, `model`, `os`, `osv`, `h`, `w`, `ppi`, `pxratio`. Interested bidder adapters may use these fields as needed. In addition, the module sets `device.ext.fiftyonedegrees_deviceId` to a permanent device ID, which can be rapidly looked up in on-premise data, exposing over 250 properties, including device age, chipset, codec support, price, operating system and app/browser versions, age, and embedded features. +51Degrees module sets the following fields of the device object: `devicetype`, `make`, `model`, `os`, `osv`, `h`, `w`, `ppi`, `pxratio`. Interested bidder adapters may use these fields as needed. + +The module also adds a `device.ext.fod` extension object (fod == fifty one degrees) and sets `device.ext.fod.deviceId` to a permanent device ID, which can be rapidly looked up in on-premise data, exposing over 250 properties, including device age, chipset, codec support, price, operating system and app/browser versions, age, and embedded features. + +It also sets `device.ext.fod.tpc` key to a binary value to indicate whether third-party cookies are enabled in the browser (1 if enabled, 0 if disabled). The module supports on-premise and cloud device detection services, with free options for both. -A free resource key for use with 51Degrees cloud service can be obtained from [51Degrees cloud configuration](https://configure.51degrees.com/HNZ75HT1). This is the simplest approach to trial the module. +A free resource key for use with 51Degrees cloud service can be obtained from [51Degrees cloud configuration](https://configure.51degrees.com/7bL8jDGz). This is the simplest approach to trial the module. An interface-compatible self-hosted service can be used with .NET, Java, Node, PHP, and Python. See [51Degrees examples](https://51degrees.com/documentation/_examples__device_detection__getting_started__web__on_premise.html). @@ -36,7 +40,7 @@ gulp build --modules=rtdModule,51DegreesRtdProvider,appnexusBidAdapter,... #### Resource Key -In order to use the module, please first obtain a Resource Key using the [Configurator tool](https://configure.51degrees.com/HNZ75HT1) - choose the following properties: +In order to use the module, please first obtain a Resource Key using the [Configurator tool](https://configure.51degrees.com/7bL8jDGz) - choose the following properties: * DeviceId * DeviceType @@ -52,6 +56,7 @@ In order to use the module, please first obtain a Resource Key using the [Config * ScreenInchesHeight * ScreenInchesWidth * PixelRatio +* ThirdPartyCookiesEnabled The Cloud API is **free** to integrate and use. To increase limits, please check [51Degrees pricing](https://51degrees.com/pricing). @@ -106,7 +111,7 @@ pbjs.setConfig({ waitForIt: true, // should be true, otherwise the auctionDelay will be ignored params: { resourceKey: '', - // Get your resource key from https://configure.51degrees.com/HNZ75HT1 + // Get your resource key from https://configure.51degrees.com/7bL8jDGz // alternatively, you can use the on-premise version of the 51Degrees service and connect to your chosen endpoint // onPremiseJSUrl: 'https://localhost/51Degrees.core.js' }, diff --git a/modules/adgridBidAdapter.ts b/modules/adgridBidAdapter.ts new file mode 100644 index 00000000000..e5ba2c8b672 --- /dev/null +++ b/modules/adgridBidAdapter.ts @@ -0,0 +1,102 @@ +import { deepSetValue, generateUUID } from '../src/utils.js'; +import { getStorageManager, StorageManager } from '../src/storageManager.js'; +import { AdapterRequest, BidderSpec, registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js' +import { BidRequest, ClientBidderRequest } from '../src/adapterManager.js'; +import { interpretResponse, enrichImp, enrichRequest, getAmxId, getLocalStorageFunctionGenerator, getUserSyncs } from '../libraries/nexx360Utils/index.js'; +import { ORTBRequest } from '../src/prebid.public.js'; + +const BIDDER_CODE = 'adgrid'; +const REQUEST_URL = 'https://fast.nexx360.io/adgrid'; +const PAGE_VIEW_ID = generateUUID(); +const BIDDER_VERSION = '2.0'; +const ADGRID_KEY = 'adgrid'; + +type RequireAtLeastOne = + Omit & { + [K in Keys]-?: Required> & + Partial>> + }[Keys]; + +type AdgridBidParams = RequireAtLeastOne<{ + domainId?: string; + placement?: string; + allBids?: boolean; + customId?: string; +}, "domainId" | "placement">; + +declare module '../src/adUnits' { + interface BidderParams { + [BIDDER_CODE]: AdgridBidParams; + } +} + +const ALIASES = []; + +// Define the storage manager for the Adgrid bidder +export const STORAGE: StorageManager = getStorageManager({ + bidderCode: BIDDER_CODE, +}); + +export const getAdgridLocalStorage = getLocalStorageFunctionGenerator<{ adgridId: string }>( + STORAGE, + BIDDER_CODE, + ADGRID_KEY, + 'adgridId' +); + +const converter = ortbConverter({ + context: { + netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false + ttl: 90, // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + }, + imp(buildImp, bidRequest, context) { + let imp = buildImp(bidRequest, context); + imp = enrichImp(imp, bidRequest); + const params = bidRequest.params as AdgridBidParams; + if (params.domainId) deepSetValue(imp, 'ext.adgrid.domainId', params.domainId); + if (params.placement) deepSetValue(imp, 'ext.adgrid.placement', params.placement); + if (params.allBids) deepSetValue(imp, 'ext.adgrid.allBids', params.allBids); + if (params.customId) deepSetValue(imp, 'ext.adgrid.customId', params.customId); + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + let request = buildRequest(imps, bidderRequest, context); + const amxId = getAmxId(STORAGE, BIDDER_CODE); + request = enrichRequest(request, amxId, PAGE_VIEW_ID, BIDDER_VERSION); + return request; + }, +}); + +const isBidRequestValid = (bid:BidRequest): boolean => { + if (!bid || !bid.params) return false; + if (typeof bid.params.domainId !== 'number') return false; + if (typeof bid.params.placement !== 'string') return false; + return true; +} + +const buildRequests = ( + bidRequests: BidRequest[], + bidderRequest: ClientBidderRequest, +): AdapterRequest => { + const data:ORTBRequest = converter.toORTB({bidRequests, bidderRequest}) + const adapterRequest:AdapterRequest = { + method: 'POST', + url: REQUEST_URL, + data, + } + return adapterRequest; +} + +export const spec:BidderSpec = { + code: BIDDER_CODE, + aliases: ALIASES, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, +}; + +registerBidder(spec); diff --git a/modules/adipoloBidAdapter.js b/modules/adipoloBidAdapter.js index ace9fc42c86..51c2a97a1f0 100644 --- a/modules/adipoloBidAdapter.js +++ b/modules/adipoloBidAdapter.js @@ -1,6 +1,7 @@ import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {buildRequests, getUserSyncs, interpretResponse, isBidRequestValid} from '../libraries/xeUtils/bidderUtils.js'; +import { getTimeZone } from '../libraries/timezone/timezone.js'; const BIDDER_CODE = 'adipolo'; const GVL_ID = 1456; @@ -12,8 +13,7 @@ function getSubdomain() { }; try { - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const region = timezone.split('/')[0]; + const region = getTimeZone().split('/')[0]; return regionMap[region] || regionMap.America; } catch (err) { return regionMap.America; diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 4bf011cc12c..d43002fd35e 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -107,7 +107,8 @@ export const spec = { {code: 'smartyexchange'}, {code: 'infinety'}, {code: 'qohere'}, - {code: 'blutonic'} + {code: 'blutonic'}, + {code: 'appmonsta', gvlid: 1283} ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], diff --git a/modules/admaticBidAdapter.js b/modules/admaticBidAdapter.js index d3f28af5f2c..4e75f7e583f 100644 --- a/modules/admaticBidAdapter.js +++ b/modules/admaticBidAdapter.js @@ -364,8 +364,11 @@ function buildRequestObject(bid) { reqObj.mediatype = bid.mediaTypes.native; } + reqObj.ext = reqObj.ext || {}; + if (deepAccess(bid, 'ortb2Imp.ext')) { - reqObj.ext = bid.ortb2Imp.ext; + Object.assign(reqObj.ext, bid.ortb2Imp.ext); + reqObj.ext.ortb2Imp = bid.ortb2Imp; } reqObj.id = getBidIdParameter('bidId', bid); diff --git a/modules/adqueryBidAdapter.js b/modules/adqueryBidAdapter.js index b0770d3e45e..36a7eee206c 100644 --- a/modules/adqueryBidAdapter.js +++ b/modules/adqueryBidAdapter.js @@ -1,6 +1,13 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; -import {buildUrl, logInfo, logMessage, parseSizesInput, triggerPixel} from '../src/utils.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import { + buildUrl, + logInfo, + logMessage, + parseSizesInput, + triggerPixel, + deepSetValue +} from '../src/utils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -25,13 +32,18 @@ const ADQUERY_TTL = 360; export const spec = { code: ADQUERY_BIDDER_CODE, gvlid: ADQUERY_GVLID, - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], /** * @param {object} bid * @return {boolean} */ isBidRequestValid: (bid) => { + const video = bid.mediaTypes && bid.mediaTypes.video; + if (video) { + return !!(video.playerSize && video.context === 'outstream');// Focus on outstream + } + return !!(bid && bid.params && bid.params.placementId && bid.mediaTypes.banner.sizes) }, @@ -47,19 +59,33 @@ export const spec = { protocol: ADQUERY_BIDDER_DOMAIN_PROTOCOL, hostname: ADQUERY_BIDDER_DOMAIN, pathname: '/prebid/bid', - // search: params }); for (let i = 0, len = bidRequests.length; i < len; i++) { + const bid = bidRequests[i]; + const isVideo = bid.mediaTypes && bid.mediaTypes.video && bid.mediaTypes.video.context === 'outstream'; + + let requestUrl = adqueryRequestUrl; + + if (isVideo) { + requestUrl = buildUrl({ + protocol: ADQUERY_BIDDER_DOMAIN_PROTOCOL, + hostname: ADQUERY_BIDDER_DOMAIN, + pathname: '/openrtb2/auction2', + }); + } + const request = { method: 'POST', - url: adqueryRequestUrl, // ADQUERY_BIDDER_DOMAIN_PROTOCOL + '://' + ADQUERY_BIDDER_DOMAIN + '/prebid/bid', - data: buildRequest(bidRequests[i], bidderRequest), + url: requestUrl, + data: buildRequest(bid, bidderRequest, isVideo), options: { withCredentials: false, crossOrigin: true - } + }, + bidId: bid.bidId }; + requests.push(request); } return requests; @@ -71,14 +97,47 @@ export const spec = { * @return {Bid[]} */ interpretResponse: (response, request) => { - logMessage(request); - logMessage(response); + const bidResponses = []; + + if (response?.body?.seatbid) { + response.body.seatbid.forEach(seat => { + seat.bid.forEach(bid => { + logMessage('bidObj', bid); + + const bidResponse = { + requestId: bid.impid, + mediaType: 'video', + cpm: bid.price, + currency: response.body.cur || 'USD', + ttl: 3600, // video żyje dłużej + creativeId: bid.crid || bid.id, + netRevenue: true, + dealId: bid.dealid || undefined, + nurl: bid.nurl || undefined, + + // VAST – priority: inline XML > admurl > nurl as a wrapper + vastXml: bid.adm || null, + vastUrl: bid.admurl || null, + + width: bid.w || 640, + height: bid.h || 360, + + meta: { + advertiserDomains: bid.adomain && bid.adomain.length ? bid.adomain : [], + networkName: seat.seat || undefined, + mediaType: 'video' + } + }; + + bidResponses.push(bidResponse); + }); + }); + } const res = response && response.body && response.body.data; - const bidResponses = []; if (!res) { - return []; + return bidResponses; } const bidResponse = { @@ -134,6 +193,12 @@ export const spec = { */ onBidWon: (bid) => { logInfo('onBidWon', bid); + + if (bid.nurl) { + triggerPixel(bid.nurl) + return + } + const copyOfBid = { ...bid } delete copyOfBid.ad const shortBidString = JSON.stringify(copyOfBid); @@ -222,10 +287,7 @@ export const spec = { } }; -function buildRequest(validBidRequests, bidderRequest) { - const bid = validBidRequests; - logInfo('buildRequest: ', bid); - +function buildRequest(bid, bidderRequest, isVideo = false) { let userId = null; if (window.qid) { userId = window.qid; @@ -235,6 +297,10 @@ function buildRequest(validBidRequests, bidderRequest) { userId = bid.userId.qid } + if (!userId) { + userId = bid.ortb2?.user.ext.eids.find(eid => eid.source === "adquery.io")?.uids[0]?.id; + } + if (!userId) { // onetime User ID const ramdomValues = Array.from(window.crypto.getRandomValues(new Uint32Array(4))); @@ -248,6 +314,41 @@ function buildRequest(validBidRequests, bidderRequest) { pageUrl = bidderRequest.refererInfo.page || ''; } + if (isVideo) { + let baseRequest = bid.ortb2 + let videoRequest = { + ...baseRequest, + imp: [{ + id: bid.bidId, + video: bid.ortb2Imp?.video || {}, + }] + } + + deepSetValue(videoRequest, 'site.ext.bidder', bid.params); + videoRequest.id = bid.bidId + + let currency = bid?.ortb2?.ext?.prebid?.adServerCurrency || "PLN"; + videoRequest.cur = [ currency ] + + let floorInfo; + if (typeof bid.getFloor === 'function') { + floorInfo = bid.getFloor({ + currency: currency, + mediaType: "video", + size: "*" + }); + } + const bidfloor = floorInfo?.floor; + const bidfloorcur = floorInfo?.currency; + + if (bidfloor && bidfloorcur) { + videoRequest.imp[0].video.bidfloor = bidfloor + videoRequest.imp[0].video.bidfloorcur = bidfloorcur + } + + return videoRequest + } + return { v: '$prebid.version$', placementCode: bid.params.placementId, diff --git a/modules/allegroBidAdapter.js b/modules/allegroBidAdapter.js new file mode 100644 index 00000000000..3c42c9f1e60 --- /dev/null +++ b/modules/allegroBidAdapter.js @@ -0,0 +1,257 @@ +// jshint esversion: 6, es3: false, node: true +'use strict'; + +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO, NATIVE} from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {config} from '../src/config.js'; +import {triggerPixel, logInfo, logError} from '../src/utils.js'; + +const BIDDER_CODE = 'allegro'; +const BIDDER_URL = 'https://prebid.rtb.allegrogroup.com/v1/rtb/prebid/bid'; +const GVLID = 1493; + +/** + * Traverses an OpenRTB bid request object and moves any ext objects into + * DoubleClick (Google) style bracketed keys (e.g. ext -> [com.google.doubleclick.site]). + * Also normalizes certain integer flags into booleans (e.g. gdpr: 1 -> true). + * This mutates the provided request object in-place. + * + * @param request OpenRTB bid request being prepared for sending. + */ +function convertExtensionFields(request) { + if (request.imp) { + request.imp.forEach(imp => { + if (imp.banner?.ext) { + moveExt(imp.banner, '[com.google.doubleclick.banner_ext]') + } + if (imp.ext) { + moveExt(imp, '[com.google.doubleclick.imp]') + } + }); + } + + if (request.app?.ext) { + moveExt(request.app, '[com.google.doubleclick.app]') + } + + if (request.site?.ext) { + moveExt(request.site, '[com.google.doubleclick.site]') + } + + if (request.site?.publisher?.ext) { + moveExt(request.site.publisher, '[com.google.doubleclick.publisher]') + } + + if (request.user?.ext) { + moveExt(request.user, '[com.google.doubleclick.user]') + } + + if (request.user?.data) { + request.user.data.forEach(data => { + if (data.ext) { + moveExt(data, '[com.google.doubleclick.data]') + } + }); + } + + if (request.device?.ext) { + moveExt(request.device, '[com.google.doubleclick.device]') + } + + if (request.device?.geo?.ext) { + moveExt(request.device.geo, '[com.google.doubleclick.geo]') + } + + if (request.regs?.ext) { + if (request.regs?.ext?.gdpr !== undefined) { + request.regs.ext.gdpr = request.regs.ext.gdpr === 1; + } + + moveExt(request.regs, '[com.google.doubleclick.regs]') + } + + if (request.source?.ext) { + moveExt(request.source, '[com.google.doubleclick.source]') + } + + if (request.ext) { + moveExt(request, '[com.google.doubleclick.bid_request]') + } +} + +/** + * Moves an `ext` field from a given object to a new bracketed key, cloning its contents. + * If object or ext is missing nothing is done. + * + * @param obj The object potentially containing `ext`. + * @param {string} newKey The destination key name (e.g. '[com.google.doubleclick.site]'). + */ +function moveExt(obj, newKey) { + if (!obj || !obj.ext) { + return; + } + const extCopy = {...obj.ext}; + delete obj.ext; + obj[newKey] = extCopy; +} + +/** + * Custom ORTB converter configuration adjusting request/imp level boolean coercions + * and migrating extension fields depending on config. Provides `toORTB` and `fromORTB` + * helpers used in buildRequests / interpretResponse. + */ +const converter = ortbConverter({ + context: { + mediaType: BANNER, + ttl: 360, + netRevenue: true + }, + + /** + * Builds and post-processes a single impression object, coercing integer flags to booleans. + * + * @param {Function} buildImp Base builder provided by ortbConverter. + * @param bidRequest Individual bid request from Prebid. + * @param context Shared converter context. + * @returns {Object} ORTB impression object. + */ + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + if (imp?.banner?.topframe !== undefined) { + imp.banner.topframe = imp.banner.topframe === 1; + } + if (imp?.secure !== undefined) { + imp.secure = imp.secure === 1; + } + return imp; + }, + + /** + * Builds the full ORTB request and normalizes integer flags. Optionally migrates ext fields + * into Google style bracketed keys unless disabled via `allegro.convertExtensionFields` config. + * + * @param {Function} buildRequest Base builder provided by ortbConverter. + * @param {Object[]} imps Array of impression objects. + * @param bidderRequest Prebid bidderRequest (contains refererInfo, gdpr, etc.). + * @param context Shared converter context. + * @returns {Object} Mutated ORTB request object ready to serialize. + */ + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + + if (request?.device?.dnt !== undefined) { + request.device.dnt = request.device.dnt === 1; + } + + if (request?.device?.sua?.mobile !== undefined) { + request.device.sua.mobile = request.device.sua.mobile === 1; + } + + if (request?.test !== undefined) { + request.test = request.test === 1; + } + + // by default, we convert extension fields unless the config explicitly disables it + const convertExtConfig = config.getConfig('allegro.convertExtensionFields'); + if (convertExtConfig === undefined || convertExtConfig === true) { + convertExtensionFields(request); + } + + if (request?.source?.schain && !isSchainValid(request.source.schain)) { + delete request.source.schain; + } + + return request; + } +}) + +/** + * Validates supply chain object structure + * @param schain - Supply chain object + * @return {boolean} True if valid, false otherwise + */ +function isSchainValid(schain) { + try { + if (!schain || !schain.nodes || !Array.isArray(schain.nodes)) { + return false; + } + const requiredFields = ['asi', 'sid', 'hp']; + return schain.nodes.every(node => + requiredFields.every(field => node.hasOwnProperty(field)) + ); + } catch (error) { + logError('Allegro: Error validating schain:', error); + return false; + } +} + +/** + * Allegro Bid Adapter specification object consumed by Prebid core. + */ +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + gvlid: GVLID, + + /** + * Validates an incoming bid object. + * + * @param bid Prebid bid request params. + * @returns {boolean} True if bid is considered valid. + */ + isBidRequestValid: function (bid) { + return !!(bid); + }, + + /** + * Generates the network request payload for the adapter. + * + * @param bidRequests List of valid bid requests. + * @param bidderRequest Aggregated bidder request data (gdpr, usp, refererInfo, etc.). + * @returns Request details for Prebid to send. + */ + buildRequests: function (bidRequests, bidderRequest) { + const url = config.getConfig('allegro.bidderUrl') || BIDDER_URL; + + return { + method: 'POST', + url: url, + data: converter.toORTB({bidderRequest, bidRequests}), + options: { + contentType: 'text/plain' + }, + } + }, + + /** + * Parses the server response into Prebid bid objects. + * + * @param response Server response wrapper from Prebid XHR (expects `body`). + * @param request Original request object passed to server (contains `data`). + */ + interpretResponse: function (response, request) { + if (!response.body) return; + return converter.fromORTB({response: response.body, request: request.data}).bids; + }, + + /** + * Fires impression tracking pixel when the bid wins if enabled by config. + * + * @param bid The winning bid object. + */ + onBidWon: function (bid) { + const triggerImpressionPixel = config.getConfig('allegro.triggerImpressionPixel'); + + if (triggerImpressionPixel && bid.burl) { + triggerPixel(bid.burl); + } + + if (config.getConfig('debug')) { + logInfo('bid won', bid); + } + } + +} + +registerBidder(spec); diff --git a/modules/allegroBidAdapter.md b/modules/allegroBidAdapter.md new file mode 100644 index 00000000000..75ee1720bb5 --- /dev/null +++ b/modules/allegroBidAdapter.md @@ -0,0 +1,73 @@ +# Overview + +**Module Name**: Allegro Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: the-bidders@allegro.com +**GVLID**: 1493 + +# Description + +Connects to Allegro's demand sources for banner advertising. This adapter uses the OpenRTB 2.5 protocol with support for extension field conversion to Google DoubleClick proto format. + +# Supported Media Types + +- Banner +- Native +- Video + +# Configuration + +The Allegro adapter supports the following configuration options: + +## Global Configuration Parameters + +| Name | Scope | Type | Description | Default | +|----------------------------------|----------|---------|-----------------------------------------------------------------------------|---------------------------------------------------------| +| `allegro.bidderUrl` | optional | String | Custom bidder endpoint URL | `https://prebid.rtb.allegrogroup.com/v1/rtb/prebid/bid` | +| `allegro.convertExtensionFields` | optional | Boolean | Enable/disable conversion of OpenRTB extension fields to DoubleClick format | `true` | +| `allegro.triggerImpressionPixel` | optional | Boolean | Enable/disable triggering impression tracking pixels on bid won event | `false` | + +## Configuration example + +```javascript +pbjs.setConfig({ + allegro: { + triggerImpressionPixel: true + } +}); +``` + +# AdUnit Configuration Example + +## Banner Ads + +```javascript +var adUnits = [{ + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90], + [300, 600] + ] + } + }, + bids: [{ + bidder: 'allegro' + }] +}]; +``` + +# Features +## Impression Tracking + +When `allegro.triggerImpressionPixel` is enabled, the adapter will automatically fire the provided `burl` (billing/impression) tracking URL when a bid wins. + +# Technical Details + +- **Protocol**: OpenRTB 2.5 +- **TTL**: 360 seconds +- **Net Revenue**: true +- **Content Type**: text/plain + diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js index b1b22ec19f9..ef89ecdd40f 100644 --- a/modules/amxBidAdapter.js +++ b/modules/amxBidAdapter.js @@ -251,7 +251,7 @@ function getSyncSettings() { const all = isSyncEnabled(syncConfig.filterSettings, 'all'); if (all) { - settings.t = SYNC_IMAGE & SYNC_IFRAME; + settings.t = SYNC_IMAGE | SYNC_IFRAME; return settings; } diff --git a/modules/apstreamBidAdapter.js b/modules/apstreamBidAdapter.js index 838487925c8..f432c85388f 100644 --- a/modules/apstreamBidAdapter.js +++ b/modules/apstreamBidAdapter.js @@ -1,5 +1,6 @@ import {getDNT} from '../libraries/dnt/index.js'; import { generateUUID, deepAccess, createTrackPixelHtml } from '../src/utils.js'; +import { getDevicePixelRatio } from '../libraries/devicePixelRatio/devicePixelRatio.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; @@ -335,7 +336,7 @@ function injectPixels(ad, pixels, scripts) { } function getScreenParams() { - return `${window.screen.width}x${window.screen.height}@${window.devicePixelRatio}`; + return `${window.screen.width}x${window.screen.height}@${getDevicePixelRatio(window)}`; } function getBids(bids) { diff --git a/modules/axonixBidAdapter.js b/modules/axonixBidAdapter.js index 17b7d9bd8df..c0b3f334c40 100644 --- a/modules/axonixBidAdapter.js +++ b/modules/axonixBidAdapter.js @@ -4,6 +4,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {ajax} from '../src/ajax.js'; +import { getConnectionInfo } from '../libraries/connectionInfo/connectionUtils.js'; const BIDDER_CODE = 'axonix'; const BIDDER_VERSION = '1.0.2'; @@ -81,19 +82,9 @@ export const spec = { buildRequests: function(validBidRequests, bidderRequest) { // device.connectiontype - const connection = window.navigator && (window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection) - let connectionType = 'unknown'; - let effectiveType = ''; - - if (connection) { - if (connection.type) { - connectionType = connection.type; - } - - if (connection.effectiveType) { - effectiveType = connection.effectiveType; - } - } + const connection = getConnectionInfo(); + const connectionType = connection?.type ?? 'unknown'; + const effectiveType = connection?.effectiveType ?? ''; const requests = validBidRequests.map(validBidRequest => { // app/site diff --git a/modules/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index 6cb9b6dfcc8..3f627fe39e0 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -11,6 +11,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {Renderer} from '../src/Renderer.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import { getFirstSize, getOsVersion, getVideoSizes, getBannerSizes, isConnectedTV, getDoNotTrack, isMobile, isBannerBid, isVideoBid, getBannerBidFloor, getVideoBidFloor, getVideoTargetingParams, getTopWindowLocation } from '../libraries/advangUtils/index.js'; +import { getConnectionInfo } from '../libraries/connectionInfo/connectionUtils.js'; const ADAPTER_VERSION = '1.21'; const GVLID = 157; @@ -338,8 +339,8 @@ function createVideoRequestData(bid, bidderRequest) { deepSetValue(payload, 'user.ext.eids', eids); } - const connection = navigator.connection || navigator.webkitConnection; - if (connection && connection.effectiveType) { + const connection = getConnectionInfo(); + if (connection?.effectiveType) { deepSetValue(payload, 'device.connectiontype', connection.effectiveType); } diff --git a/modules/byDataAnalyticsAdapter.js b/modules/byDataAnalyticsAdapter.js index ddc1112796d..265dcf2115b 100644 --- a/modules/byDataAnalyticsAdapter.js +++ b/modules/byDataAnalyticsAdapter.js @@ -11,6 +11,7 @@ import { ajax } from '../src/ajax.js'; import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; import { getViewportSize } from '../libraries/viewport/viewport.js'; import { getOsBrowserInfo } from '../libraries/userAgentUtils/detailed.js'; +import { getTimeZone } from '../libraries/timezone/timezone.js'; const versionCode = '4.4.1' const secretKey = 'bydata@123456' @@ -234,7 +235,7 @@ ascAdapter.getVisitorData = function (data = {}) { ua["brv"] = info.browser.version; ua['ss'] = screenSize; ua['de'] = deviceType; - ua['tz'] = window.Intl.DateTimeFormat().resolvedOptions().timeZone; + ua['tz'] = getTimeZone(); } var signedToken = getJWToken(ua); payload['visitor_data'] = signedToken; diff --git a/modules/carodaBidAdapter.js b/modules/carodaBidAdapter.js index 9c8975542eb..ab1fb8b606c 100644 --- a/modules/carodaBidAdapter.js +++ b/modules/carodaBidAdapter.js @@ -40,6 +40,7 @@ export const spec = { ); }, buildRequests: (validBidRequests, bidderRequest) => { + // TODO: consider using the Prebid-generated page view ID instead of generating a custom one topUsableWindow.carodaPageViewId = topUsableWindow.carodaPageViewId || Math.floor(Math.random() * 1e9); const pageViewId = topUsableWindow.carodaPageViewId; const ortbCommon = getORTBCommon(bidderRequest); diff --git a/modules/chromeAiRtdProvider.js b/modules/chromeAiRtdProvider.js index 98d429af936..9fd2d4c6639 100644 --- a/modules/chromeAiRtdProvider.js +++ b/modules/chromeAiRtdProvider.js @@ -1,7 +1,7 @@ import { submodule } from '../src/hook.js'; import { logError, mergeDeep, logMessage, deepSetValue, deepAccess } from '../src/utils.js'; -import {getStorageManager} from '../src/storageManager.js'; -import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; /* global LanguageDetector, Summarizer */ /** @@ -14,6 +14,7 @@ export const CONSTANTS = Object.freeze({ LOG_PRE_FIX: 'ChromeAI-Rtd-Provider:', STORAGE_KEY: 'chromeAi_detected_data', // Single key for both language and keywords MIN_TEXT_LENGTH: 20, + ACTIVATION_EVENTS: ['click', 'keydown', 'mousedown', 'touchend', 'pointerdown', 'pointerup'], DEFAULT_CONFIG: { languageDetector: { enabled: true, @@ -31,7 +32,7 @@ export const CONSTANTS = Object.freeze({ } }); -export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: CONSTANTS.SUBMODULE_NAME}); +export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: CONSTANTS.SUBMODULE_NAME }); let moduleConfig = JSON.parse(JSON.stringify(CONSTANTS.DEFAULT_CONFIG)); let detectedKeywords = null; // To store generated summary/keywords @@ -299,6 +300,30 @@ const initSummarizer = async () => { return false; } + // If the model is not 'available' (needs download), it typically requires a user gesture. + // We check availability and defer if needed. + try { + const availability = await Summarizer.availability(); + const needsDownload = availability !== 'available' && availability !== 'unavailable'; // 'after-download', 'downloading', etc. + + if (needsDownload && !navigator.userActivation?.isActive) { + logMessage(`${CONSTANTS.LOG_PRE_FIX} Summarizer needs download (${availability}) but user inactive. Deferring init...`); + + const onUserActivation = () => { + CONSTANTS.ACTIVATION_EVENTS.forEach(evt => window.removeEventListener(evt, onUserActivation)); + logMessage(`${CONSTANTS.LOG_PRE_FIX} User activation detected. Retrying initSummarizer...`); + // Retry initialization with fresh gesture + initSummarizer(); + }; + + CONSTANTS.ACTIVATION_EVENTS.forEach(evt => window.addEventListener(evt, onUserActivation, { once: true })); + + return false; // Return false to not block main init, will retry later + } + } catch (e) { + logError(`${CONSTANTS.LOG_PRE_FIX} Error checking Summarizer availability:`, e); + } + const summaryText = await detectSummary(pageText, moduleConfig.summarizer); if (summaryText) { // The API returns a single summary string. We treat this string as a single keyword. diff --git a/modules/clickioBidAdapter.js b/modules/clickioBidAdapter.js new file mode 100644 index 00000000000..3ba5094ffe5 --- /dev/null +++ b/modules/clickioBidAdapter.js @@ -0,0 +1,74 @@ +import {deepSetValue} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'clickio'; +const IAB_GVL_ID = 1500; + +export const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 30 + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + deepSetValue(imp, 'ext.params', bidRequest.params); + return imp; + } +}); + +export const spec = { + code: BIDDER_CODE, + gvlid: IAB_GVL_ID, + supportedMediaTypes: [BANNER], + buildRequests(bidRequests, bidderRequest) { + const data = converter.toORTB({bidRequests, bidderRequest}) + return [{ + method: 'POST', + url: 'https://o.clickiocdn.com/bids', + data + }] + }, + isBidRequestValid(bid) { + return true; + }, + interpretResponse(response, request) { + const bids = converter.fromORTB({response: response.body, request: request.data}).bids; + return bids; + }, + getUserSyncs(syncOptions, _, gdprConsent, uspConsent, gppConsent = {}) { + const { gppString = '', applicableSections = [] } = gppConsent; + const queryParams = []; + + if (gdprConsent) { + if (gdprConsent.gdprApplies !== undefined) { + queryParams.push(`gdpr=${gdprConsent.gdprApplies ? 1 : 0}`); + } + if (gdprConsent.consentString) { + queryParams.push(`gdpr_consent=${gdprConsent.consentString}`); + } + } + if (uspConsent) { + queryParams.push(`us_privacy=${uspConsent}`); + } + queryParams.push(`gpp=${gppString}`); + if (Array.isArray(applicableSections)) { + for (const applicableSection of applicableSections) { + queryParams.push(`gpp_sid=${applicableSection}`); + } + } + if (syncOptions.iframeEnabled) { + return [ + { + type: 'iframe', + url: `https://o.clickiocdn.com/cookie_sync_html?${queryParams.join('&')}` + } + ]; + } else { + return []; + } + } +}; + +registerBidder(spec); diff --git a/modules/clickioBidAdapter.md b/modules/clickioBidAdapter.md new file mode 100644 index 00000000000..7667ebe0ffd --- /dev/null +++ b/modules/clickioBidAdapter.md @@ -0,0 +1,55 @@ +--- +layout: bidder +title: Clickio +description: Clickio Bidder Adapter +biddercode: clickio +media_types: banner +gdpr_supported: true +tcfeu_supported: true +gvl_id: 1500 +usp_supported: true +gpp_supported: true +schain_supported: true +coppa_supported: true +userId: all +--- + +# Overview + +``` +Module Name: Clickio Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@clickio.com +``` + +### Description + +The Clickio bid adapter connects to Clickio's demand platform using OpenRTB 2.5 standard. This adapter supports banner advertising. + +The Clickio bidding adapter requires initial setup before use. Please contact us at [support@clickio.com](mailto:support@clickio.com). +To get started, simply replace the ``said`` with the ID assigned to you. + +### Test Parameters + +```javascript +var adUnits = [ + { + code: 'clickio-banner-ad', + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bids: [ + { + bidder: 'clickio', + params: { + said: 'test', + } + } + ] + } +]; +``` \ No newline at end of file diff --git a/modules/clydoBidAdapter.js b/modules/clydoBidAdapter.js new file mode 100644 index 00000000000..1ffdd3df474 --- /dev/null +++ b/modules/clydoBidAdapter.js @@ -0,0 +1,101 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { deepSetValue, deepAccess, isFn } from '../src/utils.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; + +const BIDDER_CODE = 'clydo'; +const METHOD = 'POST'; +const DEFAULT_CURRENCY = 'USD'; +const params = { + region: "{{region}}", + partnerId: "{{partnerId}}" +} +const BASE_ENDPOINT_URL = `https://${params.region}.clydo.io/${params.partnerId}` + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 30 + }, + bidResponse(buildBidResponse, bid, context) { + context.mediaType = deepAccess(bid, 'ext.mediaType'); + return buildBidResponse(bid, context) + } +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + isBidRequestValid: function(bid) { + if (!bid || !bid.params) return false; + const { partnerId, region } = bid.params; + if (typeof partnerId !== 'string' || partnerId.length === 0) return false; + if (typeof region !== 'string') return false; + const allowedRegions = ['us', 'usw', 'eu', 'apac']; + return allowedRegions.includes(region); + }, + buildRequests: function(validBidRequests, bidderRequest) { + const data = converter.toORTB({bidRequests: validBidRequests, bidderRequest}); + const { partnerId, region } = validBidRequests[0].params; + + if (Array.isArray(data.imp)) { + data.imp.forEach((imp, index) => { + const srcBid = validBidRequests[index] || validBidRequests[0]; + const bidderParams = deepAccess(srcBid, 'params') || {}; + deepSetValue(data, `imp.${index}.ext.clydo`, bidderParams); + + const mediaType = imp.banner ? 'banner' : (imp.video ? 'video' : (imp.native ? 'native' : '*')); + let floor = deepAccess(srcBid, 'floor'); + if (!floor && isFn(srcBid.getFloor)) { + const floorInfo = srcBid.getFloor({currency: DEFAULT_CURRENCY, mediaType, size: '*'}); + if (floorInfo && typeof floorInfo.floor === 'number') { + floor = floorInfo.floor; + } + } + + if (typeof floor === 'number') { + deepSetValue(data, `imp.${index}.bidfloor`, floor); + deepSetValue(data, `imp.${index}.bidfloorcur`, DEFAULT_CURRENCY); + } + }); + } + + const ENDPOINT_URL = BASE_ENDPOINT_URL + .replace(params.partnerId, partnerId) + .replace(params.region, region); + + return [{ + method: METHOD, + url: ENDPOINT_URL, + data + }] + }, + interpretResponse: function(serverResponse, request) { + let bids = []; + let body = serverResponse.body || {}; + if (body) { + const normalized = Array.isArray(body.seatbid) + ? { + ...body, + seatbid: body.seatbid.map(seat => ({ + ...seat, + bid: (seat.bid || []).map(b => { + if (typeof b?.adm === 'string') { + try { + const parsed = JSON.parse(b.adm); + if (parsed && parsed.native && Array.isArray(parsed.native.assets)) { + return {...b, adm: JSON.stringify(parsed.native)}; + } + } catch (e) {} + } + return b; + }) + })) + } + : body; + bids = converter.fromORTB({response: normalized, request: request.data}).bids; + } + return bids; + }, +} +registerBidder(spec); diff --git a/modules/clydoBidAdapter.md b/modules/clydoBidAdapter.md new file mode 100644 index 00000000000..a7ec0b57800 --- /dev/null +++ b/modules/clydoBidAdapter.md @@ -0,0 +1,93 @@ +# Overview + +``` +Module Name: Clydo Bid Adapter +Module Type: Bidder Adapter +Maintainer: cto@clydo.io +``` + +# Description + +The Clydo adapter connects to the Clydo bidding endpoint to request bids using OpenRTB. + +- Supported media types: banner, video, native +- Endpoint is derived from parameters: `https://{region}.clydo.io/{partnerId}` +- Passes GDPR, USP/CCPA, and GPP consent when available +- Propagates `schain` and `userIdAsEids` + +# Bid Params + +- `partnerId` (string, required): Partner identifier provided by Clydo +- `region` (string, required): One of `us`, `usw`, `eu`, `apac` + +# Test Parameters (Banner) +```javascript +var adUnits = [{ + code: '/15185185/prebid_banner_example_1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bids: [{ + bidder: 'clydo', + params: { + partnerId: 'abcdefghij', + region: 'us' + } + }] +}]; +``` + +# Test Parameters (Video) +```javascript +var adUnits = [{ + code: '/15185185/prebid_video_example_1', + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + mimes: ['video/mp4'] + } + }, + bids: [{ + bidder: 'clydo', + params: { + partnerId: 'abcdefghij', + region: 'us' + } + }] +}]; +``` + +# Test Parameters (Native) +```javascript +var adUnits = [{ + code: '/15185185/prebid_native_example_1', + mediaTypes: { + native: { + title: { required: true }, + image: { required: true, sizes: [120, 120] }, + icon: { required: false, sizes: [50, 50] }, + body: { required: false }, + sponsoredBy: { required: false }, + clickUrl: { required: false }, + cta: { required: false } + } + }, + bids: [{ + bidder: 'clydo', + params: { + partnerId: 'abcdefghij', + region: 'us' + } + }] +}]; +``` + +# Notes + +- Floors: If the ad unit implements `getFloor`, the adapter forwards the value as `imp.bidfloor` (USD). +- Consent: When present, the adapter forwards `gdprApplies`/`consentString`, `uspConsent`, and `gpp`/`gpp_sid`. +- Supply Chain and IDs: `schain` is set under `source.ext.schain`; user IDs are forwarded under `user.ext.eids`. + diff --git a/modules/cointrafficBidAdapter.js b/modules/cointrafficBidAdapter.js index 4920a6cc974..d9394253497 100644 --- a/modules/cointrafficBidAdapter.js +++ b/modules/cointrafficBidAdapter.js @@ -3,6 +3,8 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js' import { config } from '../src/config.js' import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; +import { getViewportSize } from '../libraries/viewport/viewport.js' +import { getDNT } from '../libraries/dnt/index.js' /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -12,7 +14,7 @@ import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.j */ const BIDDER_CODE = 'cointraffic'; -const ENDPOINT_URL = 'https://apps-pbd.ctraffic.io/pb/tmp'; +const ENDPOINT_URL = 'https://apps.adsgravity.io/v1/request/prebid'; const DEFAULT_CURRENCY = 'EUR'; const ALLOWED_CURRENCIES = [ 'EUR', 'USD', 'JPY', 'BGN', 'CZK', 'DKK', 'GBP', 'HUF', 'PLN', 'RON', 'SEK', 'CHF', 'ISK', 'NOK', 'HRK', 'RUB', 'TRY', @@ -43,11 +45,24 @@ export const spec = { */ buildRequests: function (validBidRequests, bidderRequest) { return validBidRequests.map(bidRequest => { - const sizes = parseSizesInput(bidRequest.params.size || bidRequest.sizes); - const currency = - config.getConfig(`currency.bidderCurrencyDefault.${BIDDER_CODE}`) || - getCurrencyFromBidderRequest(bidderRequest) || - DEFAULT_CURRENCY; + const sizes = parseSizesInput(bidRequest.params.size || bidRequest.mediaTypes.banner.sizes); + const { width, height } = getViewportSize(); + + const getCurrency = () => { + return config.getConfig(`currency.bidderCurrencyDefault.${BIDDER_CODE}`) || + getCurrencyFromBidderRequest(bidderRequest) || + DEFAULT_CURRENCY; + } + + const getLanguage = () => { + return navigator && navigator.language + ? navigator.language.indexOf('-') !== -1 + ? navigator.language.split('-')[0] + : navigator.language + : ''; + } + + const currency = getCurrency(); if (ALLOWED_CURRENCIES.indexOf(currency) === -1) { logError('Currency is not supported - ' + currency); @@ -60,6 +75,13 @@ export const spec = { sizes: sizes, bidId: bidRequest.bidId, referer: bidderRequest.refererInfo.ref, + device: { + width: width, + height: height, + user_agent: bidRequest.params.ua || navigator.userAgent, + dnt: getDNT() ? 1 : 0, + language: getLanguage(), + }, }; return { diff --git a/modules/connatixBidAdapter.js b/modules/connatixBidAdapter.js index 5ea7d88fa80..4ccb75bdc97 100644 --- a/modules/connatixBidAdapter.js +++ b/modules/connatixBidAdapter.js @@ -31,8 +31,7 @@ const BIDDER_CODE = 'connatix'; const AD_URL = 'https://capi.connatix.com/rtb/hba'; const DEFAULT_MAX_TTL = '3600'; const DEFAULT_CURRENCY = 'USD'; -const CNX_IDS_LOCAL_STORAGE_COOKIES_KEY = 'cnx_user_ids'; -const CNX_IDS_EXPIRY = 24 * 30 * 60 * 60 * 1000; // 30 days +const CNX_IDS_LOCAL_STORAGE_KEY = 'cnx_user_ids'; export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); const ALL_PROVIDERS_RESOLVED_EVENT = 'cnx_all_identity_providers_resolved'; const IDENTITY_PROVIDER_COLLECTION_UPDATED_EVENT = 'cnx_identity_provider_collection_updated'; @@ -197,23 +196,16 @@ export function hasQueryParams(url) { } } -export function saveOnAllStorages(name, value, expirationTimeMs) { - const date = new Date(); - date.setTime(date.getTime() + expirationTimeMs); - const expires = `expires=${date.toUTCString()}`; - storage.setCookie(name, JSON.stringify(value), expires); +export function saveInLocalStorage(name, value) { storage.setDataInLocalStorage(name, JSON.stringify(value)); cnxIdsValues = value; } -export function readFromAllStorages(name) { - const fromCookie = storage.getCookie(name); +export function readFromLocalStorage(name) { const fromLocalStorage = storage.getDataFromLocalStorage(name); - - const parsedCookie = fromCookie ? JSON.parse(fromCookie) : undefined; const parsedLocalStorage = fromLocalStorage ? JSON.parse(fromLocalStorage) : undefined; - return parsedCookie || parsedLocalStorage || undefined; + return parsedLocalStorage || undefined; } export const spec = { @@ -261,7 +253,7 @@ export const spec = { const bidRequests = _getBidRequests(validBidRequests); let userIds; try { - userIds = readFromAllStorages(CNX_IDS_LOCAL_STORAGE_COOKIES_KEY) || cnxIdsValues; + userIds = readFromLocalStorage(CNX_IDS_LOCAL_STORAGE_KEY) || cnxIdsValues; } catch (error) { userIds = cnxIdsValues; } @@ -364,7 +356,7 @@ export const spec = { if (message === ALL_PROVIDERS_RESOLVED_EVENT || message === IDENTITY_PROVIDER_COLLECTION_UPDATED_EVENT) { if (data) { - saveOnAllStorages(CNX_IDS_LOCAL_STORAGE_COOKIES_KEY, data, CNX_IDS_EXPIRY); + saveInLocalStorage(CNX_IDS_LOCAL_STORAGE_KEY, data); } } }, true) diff --git a/modules/connectIdSystem.js b/modules/connectIdSystem.js index 01b7e91961a..006cb06d005 100644 --- a/modules/connectIdSystem.js +++ b/modules/connectIdSystem.js @@ -9,7 +9,7 @@ import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; import {getRefererInfo} from '../src/refererDetection.js'; -import {getStorageManager} from '../src/storageManager.js'; +import {getStorageManager, STORAGE_TYPE_COOKIES, STORAGE_TYPE_LOCALSTORAGE} from '../src/storageManager.js'; import {formatQS, isNumber, isPlainObject, logError, parseUrl} from '../src/utils.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; @@ -45,15 +45,23 @@ const O_AND_O_DOMAINS = [ export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** + * Stores the ConnectID object in browser storage according to storage configuration * @function - * @param {Object} obj + * @param {Object} obj - The ID object to store + * @param {Object} [storageConfig={}] - Storage configuration + * @param {string} [storageConfig.type] - Storage type: 'cookie', 'html5', or 'cookie&html5' */ -function storeObject(obj) { +function storeObject(obj, storageConfig = {}) { const expires = Date.now() + STORAGE_DURATION; - if (storage.cookiesAreEnabled()) { + const storageType = storageConfig.type || ''; + + const useCookie = !storageType || storageType.includes(STORAGE_TYPE_COOKIES); + const useLocalStorage = !storageType || storageType.includes(STORAGE_TYPE_LOCALSTORAGE); + + if (useCookie && storage.cookiesAreEnabled()) { setEtldPlusOneCookie(MODULE_NAME, JSON.stringify(obj), new Date(expires), getSiteHostname()); } - if (storage.localStorageIsEnabled()) { + if (useLocalStorage && storage.localStorageIsEnabled()) { storage.setDataInLocalStorage(MODULE_NAME, JSON.stringify(obj)); } } @@ -110,8 +118,17 @@ function getIdFromLocalStorage() { return null; } -function syncLocalStorageToCookie() { - if (!storage.cookiesAreEnabled()) { +/** + * Syncs ID from localStorage to cookie if storage configuration allows + * @function + * @param {Object} [storageConfig={}] - Storage configuration + * @param {string} [storageConfig.type] - Storage type: 'cookie', 'html5', or 'cookie&html5' + */ +function syncLocalStorageToCookie(storageConfig = {}) { + const storageType = storageConfig.type || ''; + const useCookie = !storageType || storageType.includes(STORAGE_TYPE_COOKIES); + + if (!useCookie || !storage.cookiesAreEnabled()) { return; } const value = getIdFromLocalStorage(); @@ -129,12 +146,19 @@ function isStale(storedIdData) { return false; } -function getStoredId() { +/** + * Retrieves stored ConnectID from cookie or localStorage + * @function + * @param {Object} [storageConfig={}] - Storage configuration + * @param {string} [storageConfig.type] - Storage type: 'cookie', 'html5', or 'cookie&html5' + * @returns {Object|null} The stored ID object or null if not found + */ +function getStoredId(storageConfig = {}) { let storedId = getIdFromCookie(); if (!storedId) { storedId = getIdFromLocalStorage(); if (storedId && !isStale(storedId)) { - syncLocalStorageToCookie(); + syncLocalStorageToCookie(storageConfig); } } return storedId; @@ -191,13 +215,14 @@ export const connectIdSubmodule = { return; } const params = config.params || {}; + const storageConfig = config.storage || {}; if (!params || (typeof params.pixelId === 'undefined' && typeof params.endpoint === 'undefined')) { logError(`${MODULE_NAME} module: configuration requires the 'pixelId'.`); return; } - const storedId = getStoredId(); + const storedId = getStoredId(storageConfig); let shouldResync = isStale(storedId); @@ -213,7 +238,7 @@ export const connectIdSubmodule = { } if (!shouldResync) { storedId.lastUsed = Date.now(); - storeObject(storedId); + storeObject(storedId, storageConfig); return {id: storedId}; } } @@ -274,7 +299,7 @@ export const connectIdSubmodule = { } responseObj.ttl = validTTLMiliseconds; } - storeObject(responseObj); + storeObject(responseObj, storageConfig); } else { logError(`${MODULE_NAME} module: UPS response returned an invalid payload ${response}`); } diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index a656dee0fc1..875dfdedf67 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -2,7 +2,6 @@ import { registerBidder } from "../src/adapters/bidderFactory.js"; import { getStorageManager } from "../src/storageManager.js"; import { BANNER } from "../src/mediaTypes.js"; import { - generateUUID, getParameterByName, isNumber, logError, @@ -27,11 +26,6 @@ export const BID_ENDPOINT = "https://prebid.cwi.re/v1/bid"; export const EVENT_ENDPOINT = "https://prebid.cwi.re/v1/event"; export const GVL_ID = 1081; -/** - * Allows limiting ad impressions per site render. Unique per prebid instance ID. - */ -export const pageViewId = generateUUID(); - export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); /** @@ -248,7 +242,7 @@ export const spec = { slots: processed, httpRef: referrer, // TODO: Verify whether the auctionId and the usage of pageViewId make sense. - pageViewId: pageViewId, + pageViewId: bidderRequest.pageViewId, networkBandwidth: getConnectionDownLink(window.navigator), sdk: { version: "$prebid.version$", diff --git a/modules/datablocksBidAdapter.js b/modules/datablocksBidAdapter.js index 60ad1ffbcea..7f5a4bedd62 100644 --- a/modules/datablocksBidAdapter.js +++ b/modules/datablocksBidAdapter.js @@ -1,4 +1,5 @@ -import {deepAccess, getWinDimensions, getWindowTop, isEmpty, isGptPubadsDefined} from '../src/utils.js'; +import {getDevicePixelRatio} from '../libraries/devicePixelRatio/devicePixelRatio.js'; +import {deepAccess, getWinDimensions, getWindowTop, isGptPubadsDefined} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; @@ -7,91 +8,10 @@ import {ajax} from '../src/ajax.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; import {isWebdriverEnabled} from '../libraries/webdriver/webdriver.js'; +import { buildNativeRequest, parseNativeResponse } from '../libraries/nativeAssetsUtils.js'; export const storage = getStorageManager({bidderCode: 'datablocks'}); -const NATIVE_ID_MAP = {}; -const NATIVE_PARAMS = { - title: { - id: 1, - name: 'title' - }, - icon: { - id: 2, - type: 1, - name: 'img' - }, - image: { - id: 3, - type: 3, - name: 'img' - }, - body: { - id: 4, - name: 'data', - type: 2 - }, - sponsoredBy: { - id: 5, - name: 'data', - type: 1 - }, - cta: { - id: 6, - type: 12, - name: 'data' - }, - body2: { - id: 7, - name: 'data', - type: 10 - }, - rating: { - id: 8, - name: 'data', - type: 3 - }, - likes: { - id: 9, - name: 'data', - type: 4 - }, - downloads: { - id: 10, - name: 'data', - type: 5 - }, - displayUrl: { - id: 11, - name: 'data', - type: 11 - }, - price: { - id: 12, - name: 'data', - type: 6 - }, - salePrice: { - id: 13, - name: 'data', - type: 7 - }, - address: { - id: 14, - name: 'data', - type: 9 - }, - phone: { - id: 15, - name: 'data', - type: 8 - } -}; - -Object.keys(NATIVE_PARAMS).forEach((key) => { - NATIVE_ID_MAP[NATIVE_PARAMS[key].id] = key; -}); - // DEFINE THE PREBID BIDDER SPEC export const spec = { supportedMediaTypes: [BANNER, NATIVE], @@ -208,15 +128,15 @@ export const spec = { return { 'wiw': windowDimensions.innerWidth, 'wih': windowDimensions.innerHeight, - 'saw': windowDimensions.screen.availWidth, - 'sah': windowDimensions.screen.availHeight, - 'scd': windowDimensions.screen.colorDepth, + 'saw': null, + 'sah': null, + 'scd': null, 'sw': windowDimensions.screen.width, 'sh': windowDimensions.screen.height, 'whl': win.history.length, 'wxo': win.pageXOffset, 'wyo': win.pageYOffset, - 'wpr': win.devicePixelRatio, + 'wpr': getDevicePixelRatio(win), 'is_bot': botTest.doTests(), 'is_hid': win.document.hidden, 'vs': win.document.visibilityState @@ -266,46 +186,6 @@ export const spec = { return []; } - // CONVERT PREBID NATIVE REQUEST OBJ INTO RTB OBJ - function createNativeRequest(bid) { - const assets = []; - if (bid.nativeParams) { - Object.keys(bid.nativeParams).forEach((key) => { - if (NATIVE_PARAMS[key]) { - const {name, type, id} = NATIVE_PARAMS[key]; - const assetObj = type ? {type} : {}; - let {len, sizes, required, aspect_ratios: aRatios} = bid.nativeParams[key]; - if (len) { - assetObj.len = len; - } - if (aRatios && aRatios[0]) { - aRatios = aRatios[0]; - const wmin = aRatios.min_width || 0; - const hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; - assetObj.wmin = wmin; - assetObj.hmin = hmin; - } - if (sizes && sizes.length) { - sizes = [].concat(...sizes); - assetObj.w = sizes[0]; - assetObj.h = sizes[1]; - } - const asset = {required: required ? 1 : 0, id}; - asset[name] = assetObj; - assets.push(asset); - } - }); - } - return { - ver: '1.2', - request: { - assets: assets, - context: 1, - plcmttype: 1, - ver: '1.2' - } - } - } const imps = []; // ITERATE THE VALID REQUESTS AND GENERATE IMP OBJECT validRequests.forEach(bidRequest => { @@ -343,7 +223,7 @@ export const spec = { } } else if (deepAccess(bidRequest, `mediaTypes.native`)) { // ADD TO THE LIST OF IMP REQUESTS - imp.native = createNativeRequest(bidRequest); + imp.native = buildNativeRequest(bidRequest.nativeParams); imps.push(imp); } }); @@ -517,37 +397,6 @@ export const spec = { // PARSE THE RTB RESPONSE AND RETURN FINAL RESULTS interpretResponse: function(rtbResponse, bidRequest) { - // CONVERT NATIVE RTB RESPONSE INTO PREBID RESPONSE - function parseNative(native) { - const {assets, link, imptrackers, jstracker} = native; - const result = { - clickUrl: link.url, - clickTrackers: link.clicktrackers || [], - impressionTrackers: imptrackers || [], - javascriptTrackers: jstracker ? [jstracker] : [] - }; - - (assets || []).forEach((asset) => { - const {id, img, data, title} = asset; - const key = NATIVE_ID_MAP[id]; - if (key) { - if (!isEmpty(title)) { - result.title = title.text - } else if (!isEmpty(img)) { - result[key] = { - url: img.url, - height: img.h, - width: img.w - } - } else if (!isEmpty(data)) { - result[key] = data.value; - } - } - }); - - return result; - } - const bids = []; const resBids = deepAccess(rtbResponse, 'body.seatbid') || []; resBids.forEach(bid => { @@ -561,7 +410,7 @@ export const spec = { case 'native': const nativeResult = JSON.parse(bid.adm); - bids.push(Object.assign({}, resultItem, {mediaType: NATIVE, native: parseNative(nativeResult.native)})); + bids.push(Object.assign({}, resultItem, {mediaType: NATIVE, native: parseNativeResponse(nativeResult.native)})); break; default: diff --git a/modules/displayioBidAdapter.js b/modules/displayioBidAdapter.js index 78d5c143f55..69ff56621fd 100644 --- a/modules/displayioBidAdapter.js +++ b/modules/displayioBidAdapter.js @@ -5,6 +5,7 @@ import {Renderer} from '../src/Renderer.js'; import {logWarn} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; +import { getConnectionInfo } from '../libraries/connectionInfo/connectionUtils.js'; const ADAPTER_VERSION = '1.1.0'; const BIDDER_CODE = 'displayio'; @@ -70,7 +71,7 @@ export const spec = { }; function getPayload (bid, bidderRequest) { - const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; + const connection = getConnectionInfo(); const storage = getStorageManager({bidderCode: BIDDER_CODE}); const userSession = (() => { let us = storage.getDataFromLocalStorage(US_KEY); @@ -133,7 +134,7 @@ function getPayload (bid, bidderRequest) { device: { w: window.screen.width, h: window.screen.height, - connection_type: connection ? connection.effectiveType : '', + connection_type: connection?.effectiveType || '', } } } diff --git a/modules/escalaxBidAdapter.js b/modules/escalaxBidAdapter.js index 027e41d7c56..cf8997105b5 100644 --- a/modules/escalaxBidAdapter.js +++ b/modules/escalaxBidAdapter.js @@ -2,6 +2,7 @@ import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { getTimeZone } from '../libraries/timezone/timezone.js'; const BIDDER_CODE = 'escalax'; const ESCALAX_SOURCE_ID_MACRO = '[sourceId]'; @@ -52,8 +53,7 @@ function getSubdomain() { }; try { - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const region = timezone.split('/')[0]; + const region = getTimeZone().split('/')[0]; return regionMap[region] || 'bidder_us'; } catch (err) { return 'bidder_us'; diff --git a/modules/eskimiBidAdapter.js b/modules/eskimiBidAdapter.js index 56079a0a652..b345bf3b9af 100644 --- a/modules/eskimiBidAdapter.js +++ b/modules/eskimiBidAdapter.js @@ -3,6 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import * as utils from '../src/utils.js'; import {getBidIdParameter, logInfo, mergeDeep} from '../src/utils.js'; +import { getTimeZone } from '../libraries/timezone/timezone.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -303,8 +304,7 @@ function getUserSyncUrlByRegion() { */ function getRegionSubdomainSuffix() { try { - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const region = timezone.split('/')[0]; + const region = getTimeZone().split('/')[0]; switch (region) { case 'Europe': diff --git a/modules/flippBidAdapter.js b/modules/flippBidAdapter.js index 95fd67c779b..d1e8048be85 100644 --- a/modules/flippBidAdapter.js +++ b/modules/flippBidAdapter.js @@ -18,7 +18,7 @@ const AD_TYPES = [4309, 641]; const DTX_TYPES = [5061]; const TARGET_NAME = 'inline'; const BIDDER_CODE = 'flipp'; -const ENDPOINT = 'https://gateflipp.flippback.com/flyer-locator-service/client_bidding'; +const ENDPOINT = 'https://ads-flipp.com/flyer-locator-service/client_bidding'; const DEFAULT_TTL = 30; const DEFAULT_CURRENCY = 'USD'; const DEFAULT_CREATIVE_TYPE = 'NativeX'; diff --git a/modules/fluctBidAdapter.js b/modules/fluctBidAdapter.js index a500e06941c..e1be0488885 100644 --- a/modules/fluctBidAdapter.js +++ b/modules/fluctBidAdapter.js @@ -1,4 +1,4 @@ -import { _each, deepAccess, deepSetValue, isEmpty } from '../src/utils.js'; +import { _each, deepAccess, deepSetValue, isEmpty, isFn, isPlainObject } from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; @@ -10,9 +10,83 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'fluct'; const END_POINT = 'https://hb.adingo.jp/prebid'; -const VERSION = '1.2'; +const VERSION = '1.3'; const NET_REVENUE = true; const TTL = 300; +const DEFAULT_CURRENCY = 'JPY'; + +/** + * Get bid floor price for a specific size + * @param {BidRequest} bid + * @param {Array} size - [width, height] + * @returns {{floor: number, currency: string}|null} floor price data + */ +function getBidFloorForSize(bid, size) { + if (!isFn(bid.getFloor)) { + return null; + } + + const floorInfo = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: '*', + size: size + }); + + if (isPlainObject(floorInfo) && !isNaN(floorInfo.floor) && floorInfo.currency === DEFAULT_CURRENCY) { + return { + floor: floorInfo.floor, + currency: floorInfo.currency + }; + } + + return null; +} + +/** + * Get the highest bid floor price across all sizes + * @param {BidRequest} bid + * @returns {{floor: number, currency: string}|null} floor price data + */ +function getHighestBidFloor(bid) { + const sizes = bid.sizes || []; + let highestFloor = 0; + let floorCurrency = DEFAULT_CURRENCY; + + if (sizes.length > 0) { + sizes.forEach(size => { + const floorData = getBidFloorForSize(bid, size); + if (floorData && floorData.floor > highestFloor) { + highestFloor = floorData.floor; + floorCurrency = floorData.currency; + } + }); + + if (highestFloor > 0) { + return { + floor: highestFloor, + currency: floorCurrency + }; + } + } + + // Final fallback: use params.bidfloor if available + if (bid.params.bidfloor) { + // Check currency if specified - only JPY is supported + if (bid.params.currency && bid.params.currency !== DEFAULT_CURRENCY) { + return null; + } + const floorValue = parseFloat(bid.params.bidfloor); + if (isNaN(floorValue)) { + return null; + } + return { + floor: floorValue, + currency: DEFAULT_CURRENCY + }; + } + + return null; +} export const spec = { code: BIDDER_CODE, @@ -88,10 +162,20 @@ export const spec = { } data.sizes = []; _each(request.sizes, (size) => { - data.sizes.push({ + const sizeObj = { w: size[0], h: size[1] - }); + }; + + // Get floor price for this specific size + const floorData = getBidFloorForSize(request, size); + if (floorData) { + sizeObj.ext = { + floor: floorData.floor + }; + } + + data.sizes.push(sizeObj); }); data.params = request.params; @@ -103,6 +187,13 @@ export const spec = { data.instl = deepAccess(request, 'ortb2Imp.instl') === 1 || request.params.instl === 1 ? 1 : 0; + // Set top-level bidfloor to the highest floor across all sizes + const highestFloorData = getHighestBidFloor(request); + if (highestFloorData) { + data.bidfloor = highestFloorData.floor; + data.bidfloorcur = highestFloorData.currency; + } + const searchParams = new URLSearchParams({ dfpUnitCode: request.params.dfpUnitCode, tagId: request.params.tagId, diff --git a/modules/fwsspBidAdapter.js b/modules/fwsspBidAdapter.js index 42f649d618b..f64c21e71c4 100644 --- a/modules/fwsspBidAdapter.js +++ b/modules/fwsspBidAdapter.js @@ -22,7 +22,7 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid(bid) { - return !!(bid.params.serverUrl && bid.params.networkId && bid.params.profile && bid.params.siteSectionId && bid.params.videoAssetId); + return !!(bid.params.serverUrl && bid.params.networkId && bid.params.profile && bid.params.siteSectionId); }, /** @@ -47,9 +47,9 @@ export const spec = { const buildRequest = (currentBidRequest, bidderRequest) => { const globalParams = constructGlobalParams(currentBidRequest); const keyValues = constructKeyValues(currentBidRequest, bidderRequest); - const slotParams = constructSlotParams(currentBidRequest); - const dataString = constructDataString(globalParams, keyValues, slotParams); + const serializedSChain = constructSupplyChain(currentBidRequest, bidderRequest); + const dataString = constructDataString(globalParams, keyValues, serializedSChain, slotParams); return { method: 'GET', url: currentBidRequest.params.serverUrl, @@ -58,6 +58,19 @@ export const spec = { }; } + const constructSupplyChain = (currentBidRequest, bidderRequest) => { + // Add schain object + let schain = deepAccess(bidderRequest, 'ortb2.source.schain'); + if (!schain) { + schain = deepAccess(bidderRequest, 'ortb2.source.ext.schain'); + } + if (!schain) { + schain = currentBidRequest.schain; + } + + return this.serializeSupplyChain(schain) + } + const constructGlobalParams = currentBidRequest => { const sdkVersion = getSDKVersion(currentBidRequest); const prebidVersion = getGlobal().version; @@ -66,7 +79,7 @@ export const spec = { resp: 'vast4', prof: currentBidRequest.params.profile, csid: currentBidRequest.params.siteSectionId, - caid: currentBidRequest.params.videoAssetId, + caid: currentBidRequest.params.videoAssetId ? currentBidRequest.params.videoAssetId : 0, pvrn: getRandomNumber(), vprn: getRandomNumber(), flag: setFlagParameter(currentBidRequest.params.flags), @@ -97,7 +110,7 @@ export const spec = { if (bidderRequest && bidderRequest.gdprConsent) { keyValues._fw_gdpr_consent = bidderRequest.gdprConsent.consentString; if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { - keyValues._fw_gdpr = bidderRequest.gdprConsent.gdprApplies; + keyValues._fw_gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; } } @@ -128,23 +141,6 @@ export const spec = { } } - // Add schain object - let schain = deepAccess(bidderRequest, 'ortb2.source.schain'); - if (!schain) { - schain = deepAccess(bidderRequest, 'ortb2.source.ext.schain'); - } - if (!schain) { - schain = currentBidRequest.schain; - } - - if (schain) { - try { - keyValues.schain = JSON.stringify(schain); - } catch (error) { - logWarn('PREBID - ' + BIDDER_CODE + ': Unable to stringify the schain: ' + error); - } - } - // Add 3rd party user ID if (currentBidRequest.userIdAsEids && currentBidRequest.userIdAsEids.length > 0) { try { @@ -261,9 +257,10 @@ export const spec = { return slotParams } - const constructDataString = (globalParams, keyValues, slotParams) => { + const constructDataString = (globalParams, keyValues, serializedSChain, slotParams) => { const globalParamsString = appendParams(globalParams) + ';'; - const keyValuesString = appendParams(keyValues) + ';'; + // serializedSChain requires special encoding logic as outlined in the ORTB spec https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/supplychainobject.md + const keyValuesString = appendParams(keyValues) + serializedSChain + ';'; const slotParamsString = appendParams(slotParams) + ';'; return globalParamsString + keyValuesString + slotParamsString; @@ -274,6 +271,21 @@ export const spec = { }); }, + /** + * Serialize a supply chain object to a string uri encoded + * + * @param {*} schain object + */ + serializeSupplyChain: function(schain) { + if (!schain || !schain.nodes) return ''; + const nodesProperties = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; + return `&schain=${schain.ver},${schain.complete}!` + + schain.nodes.map(node => nodesProperties.map(prop => + node[prop] ? encodeURIComponent(node[prop]) : '') + .join(',')) + .join('!'); + }, + /** * Unpack the response from the server into a list of bids. * diff --git a/modules/fwsspBidAdapter.md b/modules/fwsspBidAdapter.md index 498897b676a..fb44dd279b2 100644 --- a/modules/fwsspBidAdapter.md +++ b/modules/fwsspBidAdapter.md @@ -8,6 +8,37 @@ Maintainer: vis@freewheel.com Module that connects to Freewheel MRM's demand sources +# Basic Test Request +``` +const adUnits = [{ + code: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480], + minduration: 30, + maxduration: 60 + } + }, + bids: [{ + bidder: 'fwssp', // or use alias 'freewheel-mrm' + params: { + serverUrl: 'https://example.com/ad/g/1', + networkId: '42015', + profile: '42015:js_allinone_profile', + siteSectionId: 'js_allinone_demo_site_section', + videoAssetId: '1', // optional: default value of 0 will used if not included + flags: '+play-uapl' // optional: users may include capability if needed + mode: 'live', + adRequestKeyValues: { // optional: users may include adRequestKeyValues if needed + _fw_player_width: '1920', + _fw_player_height: '1080' + }, + format: 'inbanner' + } + }] +}]; +``` + # Example Inbanner Ad Request ``` { @@ -19,6 +50,17 @@ Module that connects to Freewheel MRM's demand sources }, bids: [{ bidder: 'fwssp', + schain: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'example.com', + sid: '0', + hp: 1, + rid: 'bidrequestid', + domain: 'example.com' + }] + }, params: { bidfloor: 2.00, serverUrl: 'https://example.com/ad/g/1', @@ -26,7 +68,7 @@ Module that connects to Freewheel MRM's demand sources profile: '42015:js_allinone_profile', siteSectionId: 'js_allinone_demo_site_section', flags: '+play', - videoAssetId: '0', + videoAssetId: '1`, // optional: default value of 0 will used if not included timePosition: 120, adRequestKeyValues: { _fw_player_width: '1920', @@ -51,6 +93,17 @@ Module that connects to Freewheel MRM's demand sources }, bids: [{ bidder: 'fwssp', + schain: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'example.com', + sid: '0', + hp: 1, + rid: 'bidrequestid', + domain: 'example.com' + }] + }, params: { bidfloor: 2.00, serverUrl: 'https://example.com/ad/g/1', @@ -58,7 +111,7 @@ Module that connects to Freewheel MRM's demand sources profile: '42015:js_allinone_profile', siteSectionId: 'js_allinone_demo_site_section', flags: '+play', - videoAssetId: '0', + videoAssetId: '1', // optional: default value of 0 will used if not included mode: 'live', timePosition: 120, tpos: 300, diff --git a/modules/gammaBidAdapter.js b/modules/gammaBidAdapter.js index 640a871e654..a260dfa6ed7 100644 --- a/modules/gammaBidAdapter.js +++ b/modules/gammaBidAdapter.js @@ -1,3 +1,4 @@ +import { getTimeZone } from '../libraries/timezone/timezone.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; /** @@ -81,8 +82,7 @@ function getAdUrlByRegion(bid) { ENDPOINT = ENDPOINTS[bid.params.region]; } else { try { - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const region = timezone.split('/')[0]; + const region = getTimeZone().split('/')[0]; switch (region) { case 'Europe': diff --git a/modules/greenbidsBidAdapter.js b/modules/greenbidsBidAdapter.js index 2b5a0790459..58aa8608194 100644 --- a/modules/greenbidsBidAdapter.js +++ b/modules/greenbidsBidAdapter.js @@ -1,4 +1,5 @@ import { getValue, logError, deepAccess, parseSizesInput, getBidIdParameter, logInfo, getWinDimensions, getScreenOrientation } from '../src/utils.js'; +import { getDevicePixelRatio } from '../libraries/devicePixelRatio/devicePixelRatio.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { getHLen } from '../libraries/navigatorData/navigatorData.js'; @@ -10,7 +11,7 @@ import { getReferrerInfo, getPageTitle, getPageDescription, getConnectionDownLin */ const BIDDER_CODE = 'greenbids'; -const ENDPOINT_URL = 'https://hb.greenbids.ai'; +export const ENDPOINT_URL = 'https://hb.greenbids.ai'; export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); export const spec = { @@ -65,7 +66,7 @@ export const spec = { device: bidderRequest?.ortb2?.device || {}, deviceWidth: screen.width, deviceHeight: screen.height, - devicePixelRatio: topWindow.devicePixelRatio, + devicePixelRatio: getDevicePixelRatio(topWindow), screenOrientation: getScreenOrientation(), historyLength: getHLen(), viewportHeight: getWinDimensions().visualViewport.height, diff --git a/modules/gridBidAdapter.js b/modules/gridBidAdapter.js index 46989610baf..e894154e6d1 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -47,13 +47,6 @@ const LOG_ERROR_MESS = { }; const ALIAS_CONFIG = { - 'trustx': { - endpoint: 'https://grid.bidswitch.net/hbjson?sp=trustx', - syncurl: 'https://x.bidswitch.net/sync?ssp=themediagrid', - bidResponseExternal: { - netRevenue: false - } - }, 'gridNM': { defaultParams: { multiRequest: true @@ -66,7 +59,7 @@ let hasSynced = false; export const spec = { code: BIDDER_CODE, gvlid: GVLID, - aliases: ['playwire', 'adlivetech', 'gridNM', { code: 'trustx', skipPbsAliasing: true }], + aliases: ['playwire', 'adlivetech', 'gridNM'], supportedMediaTypes: [ BANNER, VIDEO ], /** * Determines whether or not the given bid request is valid. diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index 8bfb5b841d0..3f7339a4f08 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -1,10 +1,12 @@ import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {_each, deepAccess, getWinDimensions, logError, logWarn, parseSizesInput} from '../src/utils.js'; +import {getDevicePixelRatio} from '../libraries/devicePixelRatio/devicePixelRatio.js'; import {config} from '../src/config.js'; import {getStorageManager} from '../src/storageManager.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { getConnectionInfo } from '../libraries/connectionInfo/connectionUtils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -42,8 +44,8 @@ function _getBrowserParams(topWindowUrl, mosttopLocation) { let ns; function getNetworkSpeed () { - const connection = window.navigator && (window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection); - const Mbps = connection && (connection.downlink || connection.bandwidth); + const connection = getConnectionInfo(); + const Mbps = connection?.downlink ?? connection?.bandwidth; return Mbps ? Math.round(Mbps * 1024) : null; } @@ -89,7 +91,7 @@ function _getBrowserParams(topWindowUrl, mosttopLocation) { pu: stripGGParams(topUrl), tpl: mosttopURL, ce: storage.cookiesAreEnabled(), - dpr: topWindow.devicePixelRatio || 1, + dpr: getDevicePixelRatio(topWindow), jcsi: JSON.stringify(JCSI), ogu: getOgURL() }; diff --git a/modules/hypelabBidAdapter.js b/modules/hypelabBidAdapter.js index 9982af84cc9..bc562b84cb3 100644 --- a/modules/hypelabBidAdapter.js +++ b/modules/hypelabBidAdapter.js @@ -1,6 +1,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { generateUUID, isFn, isPlainObject, getWinDimensions } from '../src/utils.js'; +import { getDevicePixelRatio } from '../libraries/devicePixelRatio/devicePixelRatio.js'; import { ajax } from '../src/ajax.js'; import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; import { getWalletPresence, getWalletProviderFlags } from '../libraries/hypelabUtils/hypelabUtils.js'; @@ -40,7 +41,7 @@ function buildRequests(validBidRequests, bidderRequest) { const uuid = uids[0] ? uids[0] : generateTemporaryUUID(); const floor = getBidFloor(request, request.sizes || []); - const dpr = typeof window !== 'undefined' ? window.devicePixelRatio : 1; + const dpr = typeof window !== 'undefined' ? getDevicePixelRatio(window) : 1; const wp = getWalletPresence(); const wpfs = getWalletProviderFlags(); const winDimensions = getWinDimensions(); diff --git a/modules/id5AnalyticsAdapter.js b/modules/id5AnalyticsAdapter.js index f1db7890871..dca244bb317 100644 --- a/modules/id5AnalyticsAdapter.js +++ b/modules/id5AnalyticsAdapter.js @@ -2,7 +2,7 @@ import buildAdapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import {EVENTS} from '../src/constants.js'; import adapterManager from '../src/adapterManager.js'; import {ajax} from '../src/ajax.js'; -import {logError, logInfo} from '../src/utils.js'; +import {compressDataWithGZip, isGzipCompressionSupported, logError, logInfo} from '../src/utils.js'; import * as events from '../src/events.js'; const { @@ -12,6 +12,7 @@ const { } = EVENTS const GVLID = 131; +const COMPRESSION_THRESHOLD = 2048; const STANDARD_EVENTS_TO_TRACK = [ AUCTION_END, @@ -44,8 +45,20 @@ const id5Analytics = Object.assign(buildAdapter({analyticsType: 'endpoint'}), { }, sendEvent: (eventToSend) => { - // By giving some content this will be automatically a POST - ajax(id5Analytics.options.ingestUrl, null, JSON.stringify(eventToSend)); + const serializedEvent = JSON.stringify(eventToSend); + if (!id5Analytics.options.compressionDisabled && isGzipCompressionSupported() && serializedEvent.length > COMPRESSION_THRESHOLD) { + compressDataWithGZip(serializedEvent).then(compressedData => { + ajax(id5Analytics.options.ingestUrl, null, compressedData, { + contentType: 'application/json', + customHeaders: { + 'Content-Encoding': 'gzip' + } + }); + }) + } else { + // By giving some content this will be automatically a POST + ajax(id5Analytics.options.ingestUrl, null, serializedEvent); + } }, makeEvent: (event, payload) => { diff --git a/modules/impactifyBidAdapter.js b/modules/impactifyBidAdapter.js index 9819a1587e9..49ffe99245d 100644 --- a/modules/impactifyBidAdapter.js +++ b/modules/impactifyBidAdapter.js @@ -22,7 +22,6 @@ const DEFAULT_VIDEO_WIDTH = 640; const DEFAULT_VIDEO_HEIGHT = 360; const ORIGIN = 'https://sonic.impactify.media'; const LOGGER_URI = 'https://logger.impactify.media'; -const LOGGER_JS_URI = 'https://log.impactify.it' const AUCTION_URI = '/bidder'; const COOKIE_SYNC_URI = '/static/cookie_sync.html'; const GVL_ID = 606; @@ -387,18 +386,5 @@ export const spec = { return true; }, - - /** - * Register bidder specific code, which will execute if the bid request failed - * @param {*} param0 - */ - onBidderError: function ({ error, bidderRequest }) { - ajax(`${LOGGER_JS_URI}/logger`, null, JSON.stringify({ error, bidderRequest }), { - method: 'POST', - contentType: 'application/json' - }); - - return true; - }, }; registerBidder(spec); diff --git a/modules/incrementxBidAdapter.js b/modules/incrementxBidAdapter.js index e7f8ff51fa9..07eed9efd7f 100644 --- a/modules/incrementxBidAdapter.js +++ b/modules/incrementxBidAdapter.js @@ -14,6 +14,33 @@ const ENDPOINT_URL = 'https://hb.incrementxserv.com/vzhbidder/bid'; const DEFAULT_CURRENCY = 'USD'; const CREATIVE_TTL = 300; +// OUTSTREAM RENDERER +function createRenderer(bid, rendererOptions = {}) { + const renderer = Renderer.install({ + id: bid.slotBidId, + url: bid.rUrl, + config: rendererOptions, + adUnitCode: bid.adUnitCode, + loaded: false + }); + try { + renderer.setRender(({ renderer, width, height, vastXml, adUnitCode }) => { + renderer.push(() => { + window.onetag.Player.init({ + ...bid, + width, + height, + vastXml, + nodeId: adUnitCode, + config: renderer.getConfig() + }); + }); + }); + } catch (e) { } + + return renderer; +} + export const spec = { code: BIDDER_CODE, aliases: ['incrx'], @@ -26,7 +53,7 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - return !!(bid.params.placementId); + return !!(bid.params && bid.params.placementId); }, hasTypeVideo(bid) { return typeof bid.mediaTypes !== 'undefined' && typeof bid.mediaTypes.video !== 'undefined'; @@ -41,12 +68,8 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { return validBidRequests.map(bidRequest => { const sizes = parseSizesInput(bidRequest.params.size || bidRequest.sizes); - let mdType = 0; - if (bidRequest.mediaTypes[BANNER]) { - mdType = 1; - } else { - mdType = 2; - } + let mdType = bidRequest.mediaTypes[BANNER] ? 1 : 2; + const requestParams = { _vzPlacementId: bidRequest.params.placementId, sizes: sizes, @@ -54,15 +77,19 @@ export const spec = { _rqsrc: bidderRequest.refererInfo.page, mChannel: mdType }; + let payload; - if (mdType === 1) { // BANNER + + if (mdType === 1) { + // BANNER payload = { - q: encodeURI(JSON.stringify(requestParams)) + q: encodeURIComponent(JSON.stringify(requestParams)) }; - } else { // VIDEO or other types + } else { + // VIDEO payload = { - q: encodeURI(JSON.stringify(requestParams)), - bidderRequestData: encodeURI(JSON.stringify(bidderRequest)) + q: encodeURIComponent(JSON.stringify(requestParams)), + bidderRequestData: encodeURIComponent(JSON.stringify(bidderRequest)) }; } @@ -80,19 +107,13 @@ export const spec = { * @param {ServerResponse} serverResponse A successful response from the server. * @return {Bid[]} An array of bids which were nested inside the server. */ - interpretResponse: function (serverResponse, bidderRequest) { + interpretResponse: function (serverResponse, request) { const response = serverResponse.body; - const bids = []; - if (isEmpty(response)) { - return bids; - } - let decodedBidderRequestData; - if (typeof bidderRequest.data.bidderRequestData === 'string') { - decodedBidderRequestData = JSON.parse(decodeURI(bidderRequest.data.bidderRequestData)); - } else { - decodedBidderRequestData = bidderRequest.data.bidderRequestData; - } - const responseBid = { + if (isEmpty(response)) return []; + + const ixReq = request.data || {}; + + const bid = { requestId: response.slotBidId, cpm: response.cpm > 0 ? response.cpm : 0, currency: response.currency || DEFAULT_CURRENCY, @@ -107,57 +128,63 @@ export const spec = { meta: { mediaType: response.mediaType, advertiserDomains: response.advertiserDomains || [] - }, - + } }; - if (response.mediaType === BANNER) { - responseBid.ad = response.ad || ''; - } else if (response.mediaType === VIDEO) { - let context, adUnitCode; - for (let i = 0; i < decodedBidderRequestData.bids.length; i++) { - const item = decodedBidderRequestData.bids[i]; - if (item.bidId === response.slotBidId) { - context = item.mediaTypes.video.context; - adUnitCode = item.adUnitCode; - break; + + // BANNER + const ixMt = response.mediaType; + if (ixMt === BANNER || ixMt === "banner" || ixMt === 1) { + bid.ad = response.ad || ''; + return [bid]; + } + + // VIDEO + let context; + let adUnitCode; + + if (ixReq.videoContext) { + context = ixReq.videoContext; + adUnitCode = ixReq.adUnitCode; + } + + if (!context && ixReq.bidderRequestData) { + let ixDecoded = ixReq.bidderRequestData; + + if (typeof ixDecoded === 'string') { + try { + ixDecoded = JSON.parse(decodeURIComponent(ixDecoded)); + } catch (e) { + ixDecoded = null; } } - if (context === INSTREAM) { - responseBid.vastUrl = response.ad || ''; - } else if (context === OUTSTREAM) { - responseBid.vastXml = response.ad || ''; - if (response.rUrl) { - responseBid.renderer = createRenderer({ ...response, adUnitCode }); + + if (ixDecoded?.bids?.length) { + for (const item of ixDecoded.bids) { + if (item.bidId === response.slotBidId) { + context = item.mediaTypes?.video?.context; + adUnitCode = item.adUnitCode; + break; + } } } } - bids.push(responseBid); - function createRenderer(bid, rendererOptions = {}) { - const renderer = Renderer.install({ - id: bid.slotBidId, - url: bid.rUrl, - config: rendererOptions, - adUnitCode: bid.adUnitCode, - loaded: false - }); - try { - renderer.setRender(({ renderer, width, height, vastXml, adUnitCode }) => { - renderer.push(() => { - window.onetag.Player.init({ - ...bid, - width, - height, - vastXml, - nodeId: adUnitCode, - config: renderer.getConfig() - }); - }); + + // INSTREAM + if (context === INSTREAM) { + bid.vastUrl = response.ad || ''; + } else if (context === OUTSTREAM) { + // OUTSTREAM + bid.vastXml = response.ad || ''; + + if (response.rUrl) { + bid.renderer = createRenderer({ + ...response, + adUnitCode }); - } catch (e) { } - return renderer; } - return bids; + + return [bid]; } }; diff --git a/modules/intentIqAnalyticsAdapter.js b/modules/intentIqAnalyticsAdapter.js index 06c9bcb28b4..946a13ae174 100644 --- a/modules/intentIqAnalyticsAdapter.js +++ b/modules/intentIqAnalyticsAdapter.js @@ -2,38 +2,31 @@ import { isPlainObject, logError, logInfo } from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import { ajax } from '../src/ajax.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { config } from '../src/config.js'; import { EVENTS } from '../src/constants.js'; -import { MODULE_TYPE_ANALYTICS } from '../src/activities/modules.js'; import { detectBrowser } from '../libraries/intentIqUtils/detectBrowserUtils.js'; import { appendSPData } from '../libraries/intentIqUtils/urlUtils.js'; import { appendVrrefAndFui, getReferrer } from '../libraries/intentIqUtils/getRefferer.js'; import { getCmpData } from '../libraries/intentIqUtils/getCmpData.js'; import { - CLIENT_HINTS_KEY, - FIRST_PARTY_KEY, VERSION, - PREBID + PREBID, + WITH_IIQ } from '../libraries/intentIqConstants/intentIqConstants.js'; -import { readData, defineStorageType } from '../libraries/intentIqUtils/storageUtils.js'; import { reportingServerAddress } from '../libraries/intentIqUtils/intentIqConfig.js'; import { handleAdditionalParams } from '../libraries/intentIqUtils/handleAdditionalParams.js'; import { gamPredictionReport } from '../libraries/intentIqUtils/gamPredictionReport.js'; +import { defineABTestingGroup } from '../libraries/intentIqUtils/defineABTestingGroupUtils.js'; const MODULE_NAME = 'iiqAnalytics'; const analyticsType = 'endpoint'; -const storage = getStorageManager({ - moduleType: MODULE_TYPE_ANALYTICS, - moduleName: MODULE_NAME -}); const prebidVersion = '$prebid.version$'; export const REPORTER_ID = Date.now() + '_' + getRandom(0, 1000); -const allowedStorage = defineStorageType(config.enabledStorageTypes); let globalName; +let identityGlobalName; let alreadySubscribedOnGAM = false; let reportList = {}; let cleanReportsID; +let iiqConfig; const PARAMS_NAMES = { abTestGroup: 'abGroup', @@ -70,13 +63,10 @@ const PARAMS_NAMES = { partnerId: 'partnerId', firstPartyId: 'pcid', placementId: 'placementId', - adType: 'adType' + adType: 'adType', + abTestUuid: 'abTestUuid', }; -function getIntentIqConfig() { - return config.getConfig('userSync.userIds')?.find((m) => m.name === 'intentIqId'); -} - const DEFAULT_URL = 'https://reports.intentiq.com/report'; const getDataForDefineURL = () => { @@ -86,34 +76,37 @@ const getDataForDefineURL = () => { return [iiqAnalyticsAnalyticsAdapter.initOptions.reportingServerAddress, gdprDetected]; }; -const iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({ url: DEFAULT_URL, analyticsType }), { - initOptions: { - lsValueInitialized: false, +const getDefaultInitOptions = () => { + return { + adapterConfigInitialized: false, partner: null, fpid: null, currentGroup: null, dataInLs: null, eidl: null, - lsIdsInitialized: false, + dataIdsInitialized: false, manualWinReportEnabled: false, domainName: null, siloEnabled: false, reportMethod: null, + abPercentage: null, + abTestUuid: null, additionalParams: null, reportingServerAddress: '' - }, + } +} + +const iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({ url: DEFAULT_URL, analyticsType }), { + initOptions: getDefaultInitOptions(), track({ eventType, args }) { switch (eventType) { case BID_WON: bidWon(args); break; case BID_REQUESTED: - checkAndInitConfig(); - defineGlobalVariableName(); if (!alreadySubscribedOnGAM && shouldSubscribeOnGAM()) { alreadySubscribedOnGAM = true; - const iiqConfig = getIntentIqConfig(); - gamPredictionReport(iiqConfig?.params?.gamObjectReference, bidWon); + gamPredictionReport(iiqConfig?.gamObjectReference, bidWon); } break; default: @@ -126,90 +119,71 @@ const iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({ url: DEFAULT_URL, a const { BID_WON, BID_REQUESTED } = EVENTS; function initAdapterConfig(config) { - if (iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized) return; - const iiqIdSystemConfig = getIntentIqConfig(); - - if (iiqIdSystemConfig) { - const { manualWinReportEnabled, gamPredictReporting, reportMethod, reportingServerAddress: reportEndpoint, adUnitConfig } = config?.options || {} - iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized = true; - iiqAnalyticsAnalyticsAdapter.initOptions.partner = - iiqIdSystemConfig.params?.partner && !isNaN(iiqIdSystemConfig.params.partner) ? iiqIdSystemConfig.params.partner : -1; - - iiqAnalyticsAnalyticsAdapter.initOptions.browserBlackList = - typeof iiqIdSystemConfig.params?.browserBlackList === 'string' - ? iiqIdSystemConfig.params.browserBlackList.toLowerCase() - : ''; - iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = + if (iiqAnalyticsAnalyticsAdapter.initOptions.adapterConfigInitialized) return; + + const options = config?.options || {} + iiqConfig = options + const { manualWinReportEnabled, gamPredictReporting, reportMethod, reportingServerAddress, adUnitConfig, partner, ABTestingConfigurationSource, browserBlackList, domainName, additionalParams } = options + iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = manualWinReportEnabled || false; - iiqAnalyticsAnalyticsAdapter.initOptions.domainName = iiqIdSystemConfig.params?.domainName || ''; - iiqAnalyticsAnalyticsAdapter.initOptions.siloEnabled = - typeof iiqIdSystemConfig.params?.siloEnabled === 'boolean' ? iiqIdSystemConfig.params.siloEnabled : false; - iiqAnalyticsAnalyticsAdapter.initOptions.reportMethod = parseReportingMethod(reportMethod); - iiqAnalyticsAnalyticsAdapter.initOptions.additionalParams = iiqIdSystemConfig.params?.additionalParams || null; - iiqAnalyticsAnalyticsAdapter.initOptions.gamPredictReporting = typeof gamPredictReporting === 'boolean' ? gamPredictReporting : false; - iiqAnalyticsAnalyticsAdapter.initOptions.reportingServerAddress = typeof reportEndpoint === 'string' ? reportEndpoint : ''; - iiqAnalyticsAnalyticsAdapter.initOptions.adUnitConfig = typeof adUnitConfig === 'number' ? adUnitConfig : 1; - } else { - logError('IIQ ANALYTICS -> there is no initialized intentIqIdSystem module') - iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized = false; - iiqAnalyticsAnalyticsAdapter.initOptions.partner = -1; - iiqAnalyticsAnalyticsAdapter.initOptions.reportMethod = 'GET'; - } + iiqAnalyticsAnalyticsAdapter.initOptions.reportMethod = parseReportingMethod(reportMethod); + iiqAnalyticsAnalyticsAdapter.initOptions.gamPredictReporting = typeof gamPredictReporting === 'boolean' ? gamPredictReporting : false; + iiqAnalyticsAnalyticsAdapter.initOptions.reportingServerAddress = typeof reportingServerAddress === 'string' ? reportingServerAddress : ''; + iiqAnalyticsAnalyticsAdapter.initOptions.adUnitConfig = typeof adUnitConfig === 'number' ? adUnitConfig : 1; + iiqAnalyticsAnalyticsAdapter.initOptions.configSource = ABTestingConfigurationSource; + iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup = defineABTestingGroup(options); + iiqAnalyticsAnalyticsAdapter.initOptions.idModuleConfigInitialized = true; + iiqAnalyticsAnalyticsAdapter.initOptions.browserBlackList = + typeof browserBlackList === 'string' + ? browserBlackList.toLowerCase() + : ''; + iiqAnalyticsAnalyticsAdapter.initOptions.domainName = domainName || ''; + iiqAnalyticsAnalyticsAdapter.initOptions.additionalParams = additionalParams || null; + if (!partner) { + logError('IIQ ANALYTICS -> partner ID is missing'); + iiqAnalyticsAnalyticsAdapter.initOptions.partner = -1 + } else iiqAnalyticsAnalyticsAdapter.initOptions.partner = partner + defineGlobalVariableName(); + iiqAnalyticsAnalyticsAdapter.initOptions.adapterConfigInitialized = true } -function initReadLsIds() { +function receivePartnerData() { try { iiqAnalyticsAnalyticsAdapter.initOptions.dataInLs = null; - iiqAnalyticsAnalyticsAdapter.initOptions.fpid = JSON.parse( - readData( - `${FIRST_PARTY_KEY}${ - iiqAnalyticsAnalyticsAdapter.initOptions.siloEnabled - ? '_p_' + iiqAnalyticsAnalyticsAdapter.initOptions.partner - : '' - }`, - allowedStorage, - storage - ) - ); - if (iiqAnalyticsAnalyticsAdapter.initOptions.fpid) { - iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup = iiqAnalyticsAnalyticsAdapter.initOptions.fpid.group; + const FPD = window[identityGlobalName]?.firstPartyData + if (!window[identityGlobalName] || !FPD) { + return false } - const partnerData = readData( - FIRST_PARTY_KEY + '_' + iiqAnalyticsAnalyticsAdapter.initOptions.partner, - allowedStorage, - storage - ); - const clientsHints = readData(CLIENT_HINTS_KEY, allowedStorage, storage) || ''; + iiqAnalyticsAnalyticsAdapter.initOptions.fpid = FPD + const { partnerData, clientsHints = '', actualABGroup } = window[identityGlobalName] if (partnerData) { - iiqAnalyticsAnalyticsAdapter.initOptions.lsIdsInitialized = true; - const pData = JSON.parse(partnerData); - iiqAnalyticsAnalyticsAdapter.initOptions.terminationCause = pData.terminationCause; - iiqAnalyticsAnalyticsAdapter.initOptions.dataInLs = pData.data; - iiqAnalyticsAnalyticsAdapter.initOptions.eidl = pData.eidl || -1; - iiqAnalyticsAnalyticsAdapter.initOptions.clientType = pData.clientType || null; - iiqAnalyticsAnalyticsAdapter.initOptions.siteId = pData.siteId || null; - iiqAnalyticsAnalyticsAdapter.initOptions.wsrvcll = pData.wsrvcll || false; - iiqAnalyticsAnalyticsAdapter.initOptions.rrtt = pData.rrtt || null; + iiqAnalyticsAnalyticsAdapter.initOptions.dataIdsInitialized = true; + iiqAnalyticsAnalyticsAdapter.initOptions.terminationCause = partnerData.terminationCause; + iiqAnalyticsAnalyticsAdapter.initOptions.abTestUuid = partnerData.abTestUuid; + iiqAnalyticsAnalyticsAdapter.initOptions.dataInLs = partnerData.data; + iiqAnalyticsAnalyticsAdapter.initOptions.eidl = partnerData.eidl || -1; + iiqAnalyticsAnalyticsAdapter.initOptions.clientType = partnerData.clientType || null; + iiqAnalyticsAnalyticsAdapter.initOptions.siteId = partnerData.siteId || null; + iiqAnalyticsAnalyticsAdapter.initOptions.wsrvcll = partnerData.wsrvcll || false; + iiqAnalyticsAnalyticsAdapter.initOptions.rrtt = partnerData.rrtt || null; } + if (actualABGroup) { + iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup = actualABGroup; + } iiqAnalyticsAnalyticsAdapter.initOptions.clientsHints = clientsHints; } catch (e) { logError(e); + return false; } } function shouldSubscribeOnGAM() { - const iiqConfig = getIntentIqConfig(); - if (!iiqConfig?.params?.gamObjectReference || !isPlainObject(iiqConfig.params.gamObjectReference)) return false; - const partnerDataFromLS = readData( - FIRST_PARTY_KEY + '_' + iiqAnalyticsAnalyticsAdapter.initOptions.partner, - allowedStorage, - storage - ); + if (!iiqConfig?.gamObjectReference || !isPlainObject(iiqConfig.gamObjectReference)) return false; + const partnerData = window[identityGlobalName]?.partnerData - if (partnerDataFromLS) { - const partnerData = JSON.parse(partnerDataFromLS); + if (partnerData) { return partnerData.gpr || (!('gpr' in partnerData) && iiqAnalyticsAnalyticsAdapter.initOptions.gamPredictReporting); } return false; @@ -228,20 +202,11 @@ export function restoreReportList() { reportList = {}; } -function checkAndInitConfig() { - if (!iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized) { - initAdapterConfig(); - } -} - function bidWon(args, isReportExternal) { - checkAndInitConfig(); - if ( - isNaN(iiqAnalyticsAnalyticsAdapter.initOptions.partner) || - iiqAnalyticsAnalyticsAdapter.initOptions.partner === -1 + isNaN(iiqAnalyticsAnalyticsAdapter.initOptions.partner) ) { - return; + iiqAnalyticsAnalyticsAdapter.initOptions.partner = -1; } const currentBrowserLowerCase = detectBrowser(); if (iiqAnalyticsAnalyticsAdapter.initOptions.browserBlackList?.includes(currentBrowserLowerCase)) { @@ -249,15 +214,13 @@ function bidWon(args, isReportExternal) { return; } - if ( - iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized && - !iiqAnalyticsAnalyticsAdapter.initOptions.lsIdsInitialized - ) { - initReadLsIds(); - } if (shouldSendReport(isReportExternal)) { - const preparedPayload = preparePayload(args, true); + const success = receivePartnerData(); + const preparedPayload = preparePayload(args); if (!preparedPayload) return false; + if (success === false) { + preparedPayload[PARAMS_NAMES.terminationCause] = -1 + } const { url, method, payload } = constructFullUrl(preparedPayload); if (method === 'POST') { ajax(url, undefined, payload, { @@ -292,9 +255,9 @@ function defineGlobalVariableName() { return bidWon(args, true); } - const iiqConfig = getIntentIqConfig(); - const partnerId = iiqConfig?.params?.partner || 0; + const partnerId = iiqConfig?.partner || 0; globalName = `intentIqAnalyticsAdapter_${partnerId}`; + identityGlobalName = `iiq_identity_${partnerId}` window[globalName] = { reportExternalWin }; } @@ -305,39 +268,47 @@ function getRandom(start, end) { export function preparePayload(data) { const result = getDefaultDataObject(); - result[PARAMS_NAMES.partnerId] = iiqAnalyticsAnalyticsAdapter.initOptions.partner; result[PARAMS_NAMES.prebidVersion] = prebidVersion; result[PARAMS_NAMES.referrer] = getReferrer(); result[PARAMS_NAMES.terminationCause] = iiqAnalyticsAnalyticsAdapter.initOptions.terminationCause; - result[PARAMS_NAMES.abTestGroup] = iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup; result[PARAMS_NAMES.clientType] = iiqAnalyticsAnalyticsAdapter.initOptions.clientType; result[PARAMS_NAMES.siteId] = iiqAnalyticsAnalyticsAdapter.initOptions.siteId; result[PARAMS_NAMES.wasServerCalled] = iiqAnalyticsAnalyticsAdapter.initOptions.wsrvcll; result[PARAMS_NAMES.requestRtt] = iiqAnalyticsAnalyticsAdapter.initOptions.rrtt; + result[PARAMS_NAMES.isInTestGroup] = iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup === WITH_IIQ; - result[PARAMS_NAMES.isInTestGroup] = iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup === 'A'; - + if (iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup) { + result[PARAMS_NAMES.abTestGroup] = iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup; + } result[PARAMS_NAMES.agentId] = REPORTER_ID; + if (iiqAnalyticsAnalyticsAdapter.initOptions.abTestUuid) { + result[PARAMS_NAMES.abTestUuid] = iiqAnalyticsAnalyticsAdapter.initOptions.abTestUuid; + } if (iiqAnalyticsAnalyticsAdapter.initOptions.fpid?.pcid) { result[PARAMS_NAMES.firstPartyId] = encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.fpid.pcid); } if (iiqAnalyticsAnalyticsAdapter.initOptions.fpid?.pid) { result[PARAMS_NAMES.profile] = encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.fpid.pid); } + if (iiqAnalyticsAnalyticsAdapter.initOptions.configSource) { + result[PARAMS_NAMES.ABTestingConfigurationSource] = iiqAnalyticsAnalyticsAdapter.initOptions.configSource + } prepareData(data, result); - if (!reportList[result.placementId] || !reportList[result.placementId][result.prebidAuctionId]) { - reportList[result.placementId] = reportList[result.placementId] - ? { ...reportList[result.placementId], [result.prebidAuctionId]: 1 } - : { [result.prebidAuctionId]: 1 }; - cleanReportsID = setTimeout(() => { - if (cleanReportsID) clearTimeout(cleanReportsID); - restoreReportList(); - }, 1500); // clear object in 1.5 second after defining reporting list - } else { - logError('Duplication detected, report will be not sent'); - return; + if (shouldSubscribeOnGAM()) { + if (!reportList[result.placementId] || !reportList[result.placementId][result.prebidAuctionId]) { + reportList[result.placementId] = reportList[result.placementId] + ? { ...reportList[result.placementId], [result.prebidAuctionId]: 1 } + : { [result.prebidAuctionId]: 1 }; + cleanReportsID = setTimeout(() => { + if (cleanReportsID) clearTimeout(cleanReportsID); + restoreReportList(); + }, 1500); // clear object in 1.5 second after defining reporting list + } else { + logError('Duplication detected, report will be not sent'); + return; + } } fillEidsData(result); @@ -346,7 +317,7 @@ export function preparePayload(data) { } function fillEidsData(result) { - if (iiqAnalyticsAnalyticsAdapter.initOptions.lsIdsInitialized) { + if (iiqAnalyticsAnalyticsAdapter.initOptions.dataIdsInitialized) { result[PARAMS_NAMES.hadEidsInLocalStorage] = iiqAnalyticsAnalyticsAdapter.initOptions.eidl && iiqAnalyticsAnalyticsAdapter.initOptions.eidl > 0; result[PARAMS_NAMES.auctionEidsLength] = iiqAnalyticsAnalyticsAdapter.initOptions.eidl || -1; @@ -427,7 +398,6 @@ function getDefaultDataObject() { pbjsver: prebidVersion, partnerAuctionId: 'BW', reportSource: 'pbjs', - abGroup: 'U', jsversion: VERSION, partnerId: -1, biddingPlatformId: 1, @@ -488,6 +458,18 @@ iiqAnalyticsAnalyticsAdapter.enableAnalytics = function (myConfig) { iiqAnalyticsAnalyticsAdapter.originEnableAnalytics(myConfig); // call the base class function initAdapterConfig(myConfig) }; + +iiqAnalyticsAnalyticsAdapter.originDisableAnalytics = iiqAnalyticsAnalyticsAdapter.disableAnalytics; +iiqAnalyticsAnalyticsAdapter.disableAnalytics = function() { + globalName = undefined; + identityGlobalName = undefined; + alreadySubscribedOnGAM = false; + reportList = {}; + cleanReportsID = undefined; + iiqConfig = undefined; + iiqAnalyticsAnalyticsAdapter.initOptions = getDefaultInitOptions() + iiqAnalyticsAnalyticsAdapter.originDisableAnalytics() +}; adapterManager.registerAnalyticsAdapter({ adapter: iiqAnalyticsAnalyticsAdapter, code: MODULE_NAME diff --git a/modules/intentIqAnalyticsAdapter.md b/modules/intentIqAnalyticsAdapter.md index 9389cb7d8ee..42f6167c33b 100644 --- a/modules/intentIqAnalyticsAdapter.md +++ b/modules/intentIqAnalyticsAdapter.md @@ -21,11 +21,22 @@ No registration for this module is required. {: .table .table-bordered .table-striped } | Parameter | Scope | Type | Description | Example | | --- | --- | --- | --- | --- | +| options.partner| Required | Number | This is the partner ID value obtained from registering with IntentIQ. | `1177538` | | options.manualWinReportEnabled | Optional | Boolean | This variable determines whether the bidWon event is triggered automatically. If set to false, the event will occur automatically, and manual reporting with reportExternalWin will be disabled. If set to true, the event will not occur automatically, allowing manual reporting through reportExternalWin. The default value is false. | `false` | | options.reportMethod | Optional | String | Defines the HTTP method used to send the analytics report. If set to `"POST"`, the report payload will be sent in the body of the request. If set to `"GET"` (default), the payload will be included as a query parameter in the request URL. | `"GET"` | | options.reportingServerAddress | Optional | String | The base URL for the IntentIQ reporting server. If parameter is provided in `configParams`, it will be used. | `"https://domain.com"` | | options.adUnitConfig | Optional | Number | Determines how the `placementId` parameter is extracted in the report (default is 1). Possible values: 1 – adUnitCode first, 2 – placementId first, 3 – only adUnitCode, 4 – only placementId. | `1` | | options.gamPredictReporting | Optional | Boolean | This variable controls whether the GAM prediction logic is enabled or disabled. The main purpose of this logic is to extract information from a rendered GAM slot when no Prebid bidWon event is available. In that case, we take the highest CPM from the current auction and add 0.01 to that value. | `false` | +| options.ABTestingConfigurationSource | Optional | String | Determines how AB group will be defined. Possible values: `"IIQServer"` – group defined by IIQ server, `"percentage"` – generated group based on abPercentage, `"group"` – define group based on value provided by partner. | `IIQServer` | +| options.abPercentage | Optional | Number | Percentage for A/B testing group. Default value is `95` | `95` | +| options.group | Optional | String | Define group provided by partner, possible values: `"A"`, `"B"` | `"A"` | +| options.gamObjectReference | Optional | Object | This is a reference to the Google Ad Manager (GAM) object, which will be used to set targeting. If this parameter is not provided, the group reporting will not be configured.| `googletag`| +| options.browserBlackList | Optional | String | This is the name of a browser that can be added to a blacklist.| `"chrome"`| +| options.domainName | Optional | String | Specifies the domain of the page in which the IntentIQ object is currently running and serving the impression. This domain will be used later in the revenue reporting breakdown by domain. For example, cnn.com. It identifies the primary source of requests to the IntentIQ servers, even within nested web pages.| `"currentDomain.com"`| +| options. additionalParams | Optional | Array | This parameter allows sending additional custom key-value parameters with specific destination logic (sync, VR, winreport). Each custom parameter is defined as an object in the array. | `[ { parameterName: “abc”, parameterValue: 123, destination: [1,1,0] } ]` | +| options. additionalParams[0].parameterName | Required | String | Name of the custom parameter. This will be sent as a query parameter. | `"abc"` | +| options. additionalParams[0].parameterValue | Required | String / Number | Value to assign to the parameter. | `123` | +| options. additionalParams[0].destination | Required | Array | Array of numbers either `1` or `0`. Controls where this parameter is sent `[sendWithSync, sendWithVr, winreport]`. | `[1, 0, 0]` | #### Example Configuration @@ -33,9 +44,11 @@ No registration for this module is required. pbjs.enableAnalytics({ provider: 'iiqAnalytics', options: { + partner: 1177538, manualWinReportEnabled: false, reportMethod: "GET", adUnitConfig: 1, + domainName: "currentDomain.com", gamPredictReporting: false } }); diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 53755afa050..e10c19f1888 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -16,8 +16,6 @@ import { getCmpData } from '../libraries/intentIqUtils/getCmpData.js'; import {readData, storeData, defineStorageType, removeDataByKey, tryParse} from '../libraries/intentIqUtils/storageUtils.js'; import { FIRST_PARTY_KEY, - WITH_IIQ, WITHOUT_IIQ, - NOT_YET_DEFINED, CLIENT_HINTS_KEY, EMPTY, GVLID, @@ -28,6 +26,7 @@ import {SYNC_KEY} from '../libraries/intentIqUtils/getSyncKey.js'; import {iiqPixelServerAddress, iiqServerAddress} from '../libraries/intentIqUtils/intentIqConfig.js'; import { handleAdditionalParams } from '../libraries/intentIqUtils/handleAdditionalParams.js'; import { decryptData, encryptData } from '../libraries/intentIqUtils/cryptionUtils.js'; +import { defineABTestingGroup } from '../libraries/intentIqUtils/defineABTestingGroupUtils.js'; /** * @typedef {import('../modules/userId/index.js').Submodule} Submodule @@ -50,6 +49,7 @@ const encoderCH = { }; let sourceMetaData; let sourceMetaDataExternal; +let globalName = '' let FIRST_PARTY_KEY_FINAL = FIRST_PARTY_KEY; let PARTNER_DATA_KEY; @@ -58,6 +58,9 @@ let failCount = 0; let noDataCount = 0; export let firstPartyData; +let partnerData; +let clientHints; +let actualABGroup /** * Generate standard UUID string @@ -143,6 +146,15 @@ function addMetaData(url, data) { return url + '&fbp=' + data; } +export function initializeGlobalIIQ (partnerId) { + if (!globalName || !window[globalName]) { + globalName = `iiq_identity_${partnerId}` + window[globalName] = {} + return true + } + return false +} + export function createPixelUrl(firstPartyData, clientHints, configParams, partnerData, cmpData) { const browser = detectBrowser(); @@ -198,7 +210,7 @@ export function setGamReporting(gamObjectReference, gamParameterName, userGroup, gamObjectReference.cmd.push(() => { gamObjectReference .pubads() - .setTargeting(gamParameterName, userGroup || NOT_YET_DEFINED); + .setTargeting(gamParameterName, userGroup); }); } } @@ -289,9 +301,11 @@ export const intentIqIdSubmodule = { if (configParams.callback && !callbackFired) { callbackFired = true; if (callbackTimeoutID) clearTimeout(callbackTimeoutID); - if (isGroupB) runtimeEids = { eids: [] }; - configParams.callback(runtimeEids); + let data = runtimeEids; + if (data?.eids?.length === 1 && typeof data.eids[0] === 'string') data = data.eids[0]; + configParams.callback(data); } + updateGlobalObj() } if (typeof configParams.partner !== 'number') { @@ -300,6 +314,8 @@ export const intentIqIdSubmodule = { return; } + initializeGlobalIIQ(configParams.partner) + let decryptedData, callbackTimeoutID; let callbackFired = false; let runtimeEids = { eids: [] }; @@ -315,23 +331,23 @@ export const intentIqIdSubmodule = { PARTNER_DATA_KEY = `${FIRST_PARTY_KEY}_${configParams.partner}`; const allowedStorage = defineStorageType(config.enabledStorageTypes); + partnerData = tryParse(readData(PARTNER_DATA_KEY, allowedStorage)) || {}; let rrttStrtTime = 0; - let partnerData = {}; let shouldCallServer = false; FIRST_PARTY_KEY_FINAL = `${FIRST_PARTY_KEY}${siloEnabled ? '_p_' + configParams.partner : ''}`; const cmpData = getCmpData(); const gdprDetected = cmpData.gdprString; firstPartyData = tryParse(readData(FIRST_PARTY_KEY_FINAL, allowedStorage)); - const isGroupB = firstPartyData?.group === WITHOUT_IIQ; + actualABGroup = defineABTestingGroup(configParams, partnerData?.terminationCause); const currentBrowserLowerCase = detectBrowser(); const browserBlackList = typeof configParams.browserBlackList === 'string' ? configParams.browserBlackList.toLowerCase() : ''; const isBlacklisted = browserBlackList?.includes(currentBrowserLowerCase); let newUser = false; - setGamReporting(gamObjectReference, gamParameterName, firstPartyData?.group, isBlacklisted); + setGamReporting(gamObjectReference, gamParameterName, actualABGroup, isBlacklisted); - if (groupChanged) groupChanged(firstPartyData?.group || NOT_YET_DEFINED); + if (groupChanged) groupChanged(actualABGroup, partnerData?.terminationCause); callbackTimeoutID = setTimeout(() => { firePartnerCallback(); @@ -343,7 +359,6 @@ export const intentIqIdSubmodule = { firstPartyData = { pcid: firstPartyId, pcidDate: Date.now(), - group: NOT_YET_DEFINED, uspString: EMPTY, gppString: EMPTY, gdprString: EMPTY, @@ -361,7 +376,7 @@ export const intentIqIdSubmodule = { } // Read client hints from storage - let clientHints = readData(CLIENT_HINTS_KEY, allowedStorage); + clientHints = readData(CLIENT_HINTS_KEY, allowedStorage); const chSupported = isCHSupported(); let chPromise = null; @@ -401,18 +416,12 @@ export const intentIqIdSubmodule = { return Promise.race([chPromise, timeout]); } - const savedData = tryParse(readData(PARTNER_DATA_KEY, allowedStorage)) - if (savedData) { - partnerData = savedData; - - if (typeof partnerData.callCount === 'number') callCount = partnerData.callCount; - if (typeof partnerData.failCount === 'number') failCount = partnerData.failCount; - if (typeof partnerData.noDataCounter === 'number') noDataCount = partnerData.noDataCounter; - - if (partnerData.wsrvcll) { - partnerData.wsrvcll = false; - storeData(PARTNER_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); - } + if (typeof partnerData.callCount === 'number') callCount = partnerData.callCount; + if (typeof partnerData.failCount === 'number') failCount = partnerData.failCount; + if (typeof partnerData.noDataCounter === 'number') noDataCount = partnerData.noDataCounter; + if (partnerData.wsrvcll) { + partnerData.wsrvcll = false; + storeData(PARTNER_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); } if (partnerData.data) { @@ -422,9 +431,19 @@ export const intentIqIdSubmodule = { } } + function updateGlobalObj () { + if (globalName) { + window[globalName].partnerData = partnerData + window[globalName].firstPartyData = firstPartyData + window[globalName].clientHints = clientHints + window[globalName].actualABGroup = actualABGroup + } + } + + let hasPartnerData = !!Object.keys(partnerData).length; if (!isCMPStringTheSame(firstPartyData, cmpData) || !firstPartyData.sCal || - (savedData && (!partnerData.cttl || !partnerData.date || Date.now() - partnerData.date > partnerData.cttl))) { + (hasPartnerData && (!partnerData.cttl || !partnerData.date || Date.now() - partnerData.date > partnerData.cttl))) { firstPartyData.uspString = cmpData.uspString; firstPartyData.gppString = cmpData.gppString; firstPartyData.gdprString = cmpData.gdprString; @@ -433,7 +452,7 @@ export const intentIqIdSubmodule = { storeData(PARTNER_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); } if (!shouldCallServer) { - if (!savedData && !firstPartyData.isOptedOut) { + if (!hasPartnerData && !firstPartyData.isOptedOut) { shouldCallServer = true; } else shouldCallServer = Date.now() > firstPartyData.sCal + HOURS_24; } @@ -443,7 +462,7 @@ export const intentIqIdSubmodule = { firePartnerCallback() } - if (firstPartyData.group === WITHOUT_IIQ || (firstPartyData.group !== WITHOUT_IIQ && runtimeEids?.eids?.length)) { + if (runtimeEids?.eids?.length) { firePartnerCallback() } @@ -471,12 +490,13 @@ export const intentIqIdSubmodule = { } if (!shouldCallServer) { - if (isGroupB) runtimeEids = { eids: [] }; firePartnerCallback(); updateCountersAndStore(runtimeEids, allowedStorage, partnerData); return { id: runtimeEids.eids }; } + updateGlobalObj() // update global object before server request, to make sure analytical adapter will have it even if the server is "not in time" + // use protocol relative urls for http or https let url = `${iiqServerAddress(configParams, gdprDetected)}/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=${configParams.partner}&pt=17&dpn=1`; url += configParams.pai ? '&pai=' + encodeURIComponent(configParams.pai) : ''; @@ -488,11 +508,13 @@ export const intentIqIdSubmodule = { url += '&japs=' + encodeURIComponent(configParams.siloEnabled === true); url = appendCounters(url); url += VERSION ? '&jsver=' + VERSION : ''; - url += firstPartyData?.group ? '&testGroup=' + encodeURIComponent(firstPartyData.group) : ''; + url += actualABGroup ? '&testGroup=' + encodeURIComponent(actualABGroup) : ''; url = addMetaData(url, sourceMetaDataExternal || sourceMetaData); url = handleAdditionalParams(currentBrowserLowerCase, url, 1, additionalParams); url = appendSPData(url, firstPartyData) url += '&source=' + PREBID; + url += '&ABTestingConfigurationSource=' + configParams.ABTestingConfigurationSource + url += '&abtg=' + encodeURIComponent(actualABGroup) // Add vrref and fui to the URL url = appendVrrefAndFui(url, configParams.domainName); @@ -524,18 +546,10 @@ export const intentIqIdSubmodule = { if ('tc' in respJson) { partnerData.terminationCause = respJson.tc; - if (Number(respJson.tc) === 41) { - firstPartyData.group = WITHOUT_IIQ; - storeData(FIRST_PARTY_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); - if (groupChanged) groupChanged(firstPartyData.group); - defineEmptyDataAndFireCallback(); - if (gamObjectReference) setGamReporting(gamObjectReference, gamParameterName, firstPartyData.group); - return - } else { - firstPartyData.group = WITH_IIQ; - if (gamObjectReference) setGamReporting(gamObjectReference, gamParameterName, firstPartyData.group); - if (groupChanged) groupChanged(firstPartyData.group); - } + actualABGroup = defineABTestingGroup(configParams, respJson.tc,); + + if (gamObjectReference) setGamReporting(gamObjectReference, gamParameterName, actualABGroup); + if (groupChanged) groupChanged(actualABGroup, partnerData?.terminationCause); } if ('isOptedOut' in respJson) { if (respJson.isOptedOut !== firstPartyData.isOptedOut) { @@ -592,6 +606,13 @@ export const intentIqIdSubmodule = { // server provided data firstPartyData.spd = respJson.spd; } + + if ('abTestUuid' in respJson) { + if ('ls' in respJson && respJson.ls === true) { + partnerData.abTestUuid = respJson.abTestUuid; + } + } + if ('gpr' in respJson) { // GAM prediction reporting partnerData.gpr = respJson.gpr; diff --git a/modules/intentIqIdSystem.md b/modules/intentIqIdSystem.md index bf561649566..7597ba90fbf 100644 --- a/modules/intentIqIdSystem.md +++ b/modules/intentIqIdSystem.md @@ -53,6 +53,9 @@ Please find below list of parameters that could be used in configuring Intent IQ | params.siloEnabled | Optional | Boolean | Determines if first-party data is stored in a siloed storage key. When set to `true`, first-party data is stored under a modified key that appends `_p_` plus the partner value rather than using the default storage key. The default value is `false`. | `true` | | params.groupChanged | Optional | Function | A callback that is triggered every time the user’s A/B group is set or updated. |`(group) => console.log('Group changed:', group)` | | params.chTimeout | Optional | Number | Maximum time (in milliseconds) to wait for Client Hints from the browser before sending request. Default value is `10ms` | `30` | +| params. ABTestingConfigurationSource| Optional | String | Determines how AB group will be defined. Possible values: `"IIQServer"` – group defined by IIQ server, `"percentage"` – generated group based on abPercentage, `"group"` – define group based on value provided by partner. | `IIQServer` | +| params.abPercentage | Optional | Number | Percentage for A/B testing group. Default value is `95` | `95` | +| params.group | Optional | String | Define group provided by partner, possible values: `"A"`, `"B"` | `"A"` | | params.additionalParams | Optional | Array | This parameter allows sending additional custom key-value parameters with specific destination logic (sync, VR, winreport). Each custom parameter is defined as an object in the array. | `[ { parameterName: “abc”, parameterValue: 123, destination: [1,1,0] } ]` | | params.additionalParams [0].parameterName | Required | String | Name of the custom parameter. This will be sent as a query parameter. | `"abc"` | | params.additionalParams [0].parameterValue | Required | String / Number | Value to assign to the parameter. | `123` | @@ -77,6 +80,7 @@ pbjs.setConfig({ sourceMetaData: "123.123.123.123", // Optional parameter sourceMetaDataExternal: 123456, // Optional parameter chTimeout: 10, // Optional parameter + abPercentage: 95 //Optional parameter additionalParams: [ // Optional parameter { parameterName: "abc", diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index 1146ea77692..1909112d36d 100644 --- a/modules/kargoBidAdapter.js +++ b/modules/kargoBidAdapter.js @@ -172,6 +172,7 @@ function buildRequests(validBidRequests, bidderRequest) { const page = {} if (validPageId) { + // TODO: consider using the Prebid-generated page view ID instead of generating a custom one page.id = getLocalStorageSafely(CERBERUS.PAGE_VIEW_ID); } if (validPageTimestamp) { diff --git a/modules/koblerBidAdapter.js b/modules/koblerBidAdapter.js index fcb9637f166..2b0c55b2fb9 100644 --- a/modules/koblerBidAdapter.js +++ b/modules/koblerBidAdapter.js @@ -1,6 +1,5 @@ import { deepAccess, - generateUUID, getWindowSelf, isArray, isStr, @@ -15,8 +14,6 @@ import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.j const additionalData = new WeakMap(); -export const pageViewId = generateUUID(); - export function setAdditionalData(obj, key, value) { const prevValue = additionalData.get(obj) || {}; additionalData.set(obj, { ...prevValue, [key]: value }); @@ -185,7 +182,7 @@ function buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest) { kobler: { tcf_purpose_2_given: purpose2Given, tcf_purpose_3_given: purpose3Given, - page_view_id: pageViewId + page_view_id: bidderRequest.pageViewId } } }; diff --git a/modules/limelightDigitalBidAdapter.js b/modules/limelightDigitalBidAdapter.js index 40728c54245..283a89c5a3f 100644 --- a/modules/limelightDigitalBidAdapter.js +++ b/modules/limelightDigitalBidAdapter.js @@ -48,7 +48,9 @@ export const spec = { { code: 'adnimation' }, { code: 'rtbdemand' }, { code: 'altstar' }, - { code: 'vaayaMedia' } + { code: 'vaayaMedia' }, + { code: 'performist' }, + { code: 'oveeo' } ], supportedMediaTypes: [BANNER, VIDEO], @@ -177,10 +179,21 @@ function buildPlacement(bidRequest) { bidId: bidRequest.bidId, transactionId: bidRequest.ortb2Imp?.ext?.tid, sizes: sizes.map(size => { + let floorInfo = null; + if (typeof bidRequest.getFloor === 'function') { + try { + floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: bidRequest.params.adUnitType, + size: [size[0], size[1]] + }); + } catch (e) {} + } return { width: size[0], - height: size[1] - } + height: size[1], + floorInfo: floorInfo + }; }), type: bidRequest.params.adUnitType.toUpperCase(), ortb2Imp: bidRequest.ortb2Imp, diff --git a/modules/liveIntentAnalyticsAdapter.js b/modules/liveIntentAnalyticsAdapter.js index 9a3bd53945e..1a548bfcd9c 100644 --- a/modules/liveIntentAnalyticsAdapter.js +++ b/modules/liveIntentAnalyticsAdapter.js @@ -1,5 +1,5 @@ import { ajax } from '../src/ajax.js'; -import { generateUUID, isNumber } from '../src/utils.js'; +import { generateUUID, isNumber, parseSizesInput } from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import { EVENTS } from '../src/constants.js'; import adapterManager from '../src/adapterManager.js'; @@ -41,6 +41,13 @@ function handleAuctionInitEvent(auctionInitEvent) { // dependeing on the result of rolling the dice outside of Prebid. const partnerIdFromAnalyticsLabels = auctionInitEvent.analyticsLabels?.partnerId; + const asz = auctionInitEvent?.adUnits.reduce((acc, adUnit) => + acc.concat( + parseSizesInput(adUnit?.mediaTypes?.banner?.sizes), + parseSizesInput(adUnit?.mediaTypes?.video?.playerSize) + ), [] + ) + const data = { id: generateUUID(), aid: auctionInitEvent.auctionId, @@ -51,7 +58,8 @@ function handleAuctionInitEvent(auctionInitEvent) { tr: window.liTreatmentRate, me: encodeBoolean(window.liModuleEnabled), liip: encodeBoolean(liveIntentIdsPresent), - aun: auctionInitEvent?.adUnits?.length || 0 + aun: auctionInitEvent?.adUnits?.length || 0, + asz: asz.join(',') }; const filteredData = ignoreUndefined(data); sendData('auction-init', filteredData); @@ -82,7 +90,8 @@ function handleBidWonEvent(bidWonEvent) { rts: bidWonEvent.responseTimestamp, tr: window.liTreatmentRate, me: encodeBoolean(window.liModuleEnabled), - liip: encodeBoolean(liveIntentIdsPresent) + liip: encodeBoolean(liveIntentIdsPresent), + asz: bidWonEvent.width + 'x' + bidWonEvent.height }; const filteredData = ignoreUndefined(data); diff --git a/modules/mediaforceBidAdapter.js b/modules/mediaforceBidAdapter.js index 3d7598e1ca9..a1d333ab0af 100644 --- a/modules/mediaforceBidAdapter.js +++ b/modules/mediaforceBidAdapter.js @@ -1,8 +1,9 @@ import {getDNT} from '../libraries/dnt/index.js'; -import { deepAccess, isStr, replaceAuctionPrice, triggerPixel, parseGPTSingleSizeArrayToRtbSize, isEmpty } from '../src/utils.js'; +import { deepAccess, isStr, replaceAuctionPrice, triggerPixel, parseGPTSingleSizeArrayToRtbSize } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { buildNativeRequest, parseNativeResponse } from '../libraries/nativeAssetsUtils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -27,88 +28,6 @@ const BIDDER_CODE = 'mediaforce'; const GVLID = 671; const ENDPOINT_URL = 'https://rtb.mfadsrvr.com/header_bid'; const TEST_ENDPOINT_URL = 'https://rtb.mfadsrvr.com/header_bid?debug_key=abcdefghijklmnop'; -const NATIVE_ID_MAP = {}; -const NATIVE_PARAMS = { - title: { - id: 1, - name: 'title' - }, - icon: { - id: 2, - type: 1, - name: 'img' - }, - image: { - id: 3, - type: 3, - name: 'img' - }, - body: { - id: 4, - name: 'data', - type: 2 - }, - sponsoredBy: { - id: 5, - name: 'data', - type: 1 - }, - cta: { - id: 6, - type: 12, - name: 'data' - }, - body2: { - id: 7, - name: 'data', - type: 10 - }, - rating: { - id: 8, - name: 'data', - type: 3 - }, - likes: { - id: 9, - name: 'data', - type: 4 - }, - downloads: { - id: 10, - name: 'data', - type: 5 - }, - displayUrl: { - id: 11, - name: 'data', - type: 11 - }, - price: { - id: 12, - name: 'data', - type: 6 - }, - salePrice: { - id: 13, - name: 'data', - type: 7 - }, - address: { - id: 14, - name: 'data', - type: 9 - }, - phone: { - id: 15, - name: 'data', - type: 8 - } -}; - -Object.keys(NATIVE_PARAMS).forEach((key) => { - NATIVE_ID_MAP[NATIVE_PARAMS[key].id] = key; -}); - const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; const DEFAULT_CURRENCY = 'USD' @@ -175,7 +94,7 @@ export const spec = { validImp = true; break; case NATIVE: - impObj.native = createNativeRequest(bid); + impObj.native = buildNativeRequest(bid.nativeParams); validImp = true; break; case VIDEO: @@ -275,7 +194,7 @@ export const spec = { adm = null; } if (ext?.native) { - bid.native = parseNative(ext.native); + bid.native = parseNativeResponse(ext.native); bid.mediaType = NATIVE; } else if (adm?.trim().startsWith(' { - const {id, img, data, title} = asset; - const key = NATIVE_ID_MAP[id]; - if (key) { - if (!isEmpty(title)) { - result.title = title.text - } else if (!isEmpty(img)) { - result[key] = { - url: img.url, - height: img.h, - width: img.w - } - } else if (!isEmpty(data)) { - result[key] = data.value; - } - } - }); - - return result; -} - -function createNativeRequest(bid) { - const assets = []; - if (bid.nativeParams) { - Object.keys(bid.nativeParams).forEach((key) => { - if (NATIVE_PARAMS[key]) { - const {name, type, id} = NATIVE_PARAMS[key]; - const assetObj = type ? {type} : {}; - let {len, sizes, required, aspect_ratios: aRatios} = bid.nativeParams[key]; - if (len) { - assetObj.len = len; - } - if (aRatios && aRatios[0]) { - aRatios = aRatios[0]; - const wmin = aRatios.min_width || 0; - const hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; - assetObj.wmin = wmin; - assetObj.hmin = hmin; - } - if (sizes && sizes.length) { - sizes = [].concat(...sizes); - assetObj.w = sizes[0]; - assetObj.h = sizes[1]; - } - const asset = {required: required ? 1 : 0, id}; - asset[name] = assetObj; - assets.push(asset); - } - }); - } - return { - ver: '1.2', - request: { - assets: assets, - context: 1, - plcmttype: 1, - ver: '1.2' - } - } -} - function createVideoRequest(bid) { const video = bid.mediaTypes.video; if (!video || !video.playerSize) return; diff --git a/modules/mediagoBidAdapter.js b/modules/mediagoBidAdapter.js index 7c1db69b869..c5fd2189cb9 100644 --- a/modules/mediagoBidAdapter.js +++ b/modules/mediagoBidAdapter.js @@ -33,7 +33,6 @@ const GVLID = 1020; // const ENDPOINT_URL = '/api/bid?tn='; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); const globals = {}; -const itemMaps = {}; /* ----- mguid:start ------ */ export const COOKIE_KEY_MGUID = '__mguid_'; @@ -139,7 +138,7 @@ function getItems(validBidRequests, bidderRequest) { const bidFloor = getBidFloor(req); const gpid = utils.deepAccess(req, 'ortb2Imp.ext.gpid') || - utils.deepAccess(req, 'params.placementId', 0); + utils.deepAccess(req, 'params.placementId', ''); const gdprConsent = {}; if (bidderRequest && bidderRequest.gdprConsent) { @@ -156,7 +155,8 @@ function getItems(validBidRequests, bidderRequest) { // if (mediaTypes.native) {} // banner广告类型 if (mediaTypes.banner) { - const id = '' + (i + 1); + // fix id is not unique where there are multiple requests in the same page + const id = getProperty(req, 'bidId') || ('' + (i + 1) + Math.random().toString(36).substring(2, 15)); ret = { id: id, bidfloor: bidFloor, @@ -177,10 +177,6 @@ function getItems(validBidRequests, bidderRequest) { }, tagid: req.params && req.params.tagid }; - itemMaps[id] = { - req, - ret - }; } return ret; @@ -217,7 +213,7 @@ function getParam(validBidRequests, bidderRequest) { const isMobile = getDevice() ? 1 : 0; // input test status by Publisher. more frequently for test true req const isTest = validBidRequests[0].params.test || 0; - const auctionId = getProperty(bidderRequest, 'auctionId'); + const bidderRequestId = getProperty(bidderRequest, 'bidderRequestId'); const items = getItems(validBidRequests, bidderRequest); const domain = utils.deepAccess(bidderRequest, 'refererInfo.domain') || document.domain; @@ -233,8 +229,7 @@ function getParam(validBidRequests, bidderRequest) { if (items && items.length) { const c = { - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - id: 'mgprebidjs_' + auctionId, + id: 'mgprebidjs_' + bidderRequestId, test: +isTest, at: 1, cur: ['USD'], @@ -295,7 +290,6 @@ function getParam(validBidRequests, bidderRequest) { export const spec = { code: BIDDER_CODE, gvlid: GVLID, - // aliases: ['ex'], // short code /** * Determines whether or not the given bid request is valid. * @@ -345,10 +339,9 @@ export const spec = { const bidResponses = []; for (const bid of bids) { const impid = getProperty(bid, 'impid'); - if (itemMaps[impid]) { - const bidId = getProperty(itemMaps[impid], 'req', 'bidId'); + if (impid) { const bidResponse = { - requestId: bidId, + requestId: getProperty(bid, 'impid'), cpm: getProperty(bid, 'price'), width: getProperty(bid, 'w'), height: getProperty(bid, 'h'), diff --git a/modules/mycodemediaBidAdapter.js b/modules/mycodemediaBidAdapter.js new file mode 100644 index 00000000000..592a659f3f1 --- /dev/null +++ b/modules/mycodemediaBidAdapter.js @@ -0,0 +1,19 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; + +const BIDDER_CODE = 'mycodemedia'; +const AD_URL = 'https://east-backend.mycodemedia.com/pbjs'; +const SYNC_URL = 'https://usersync.mycodemedia.com'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: getUserSyncs(SYNC_URL) +}; + +registerBidder(spec); diff --git a/modules/mycodemediaBidAdapter.md b/modules/mycodemediaBidAdapter.md new file mode 100644 index 00000000000..484233dd72d --- /dev/null +++ b/modules/mycodemediaBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: MyCodeMedia Bidder Adapter +Module Type: MyCodeMedia Bidder Adapter +Maintainer: support-platform@mycodemedia.com +``` + +# Description + +Connects to MyCodeMedia exchange for bids. +MyCodeMedia 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: 'mycodemedia', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'mycodemedia', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'mycodemedia', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/modules/neuwoRtdProvider.js b/modules/neuwoRtdProvider.js index 99715f0b484..3255547aa47 100644 --- a/modules/neuwoRtdProvider.js +++ b/modules/neuwoRtdProvider.js @@ -23,6 +23,17 @@ import { deepSetValue, logError, logInfo, mergeDeep } from "../src/utils.js"; const MODULE_NAME = "NeuwoRTDModule"; export const DATA_PROVIDER = "www.neuwo.ai"; +// Cached API response to avoid redundant requests. +let globalCachedResponse; + +/** + * Clears the cached API response. Primarily used for testing. + * @private + */ +export function clearCache() { + globalCachedResponse = undefined; +} + // Maps the IAB Content Taxonomy version string to the corresponding segtax ID. // Based on https://github.com/InteractiveAdvertisingBureau/AdCOM/blob/main/AdCOM%20v1.0%20FINAL.md#list--category-taxonomies- const IAB_CONTENT_TAXONOMY_MAP = { @@ -36,11 +47,13 @@ const IAB_CONTENT_TAXONOMY_MAP = { /** * Validates the configuration and initialises the module. + * * @param {Object} config The module configuration. * @param {Object} userConsent The user consent object. * @returns {boolean} `true` if the module is configured correctly, otherwise `false`. */ function init(config, userConsent) { + logInfo(MODULE_NAME, "init:", config, userConsent); const params = config?.params || {}; if (!params.neuwoApiUrl) { logError(MODULE_NAME, "init:", "Missing Neuwo Edge API Endpoint URL"); @@ -55,18 +68,46 @@ function init(config, userConsent) { /** * Fetches contextual data from the Neuwo API and enriches the bid request object with IAB categories. + * Uses cached response if available to avoid redundant API calls. + * * @param {Object} reqBidsConfigObj The bid request configuration object. * @param {function} callback The callback function to continue the auction. * @param {Object} config The module configuration. + * @param {Object} config.params Configuration parameters. + * @param {string} config.params.neuwoApiUrl The Neuwo API endpoint URL. + * @param {string} config.params.neuwoApiToken The Neuwo API authentication token. + * @param {string} [config.params.websiteToAnalyseUrl] Optional URL to analyze instead of current page. + * @param {string} [config.params.iabContentTaxonomyVersion] IAB content taxonomy version (default: "3.0"). + * @param {boolean} [config.params.enableCache=true] If true, caches API responses to avoid redundant requests (default: true). + * @param {boolean} [config.params.stripAllQueryParams] If true, strips all query parameters from the URL. + * @param {string[]} [config.params.stripQueryParamsForDomains] List of domains for which to strip all query params. + * @param {string[]} [config.params.stripQueryParams] List of specific query parameter names to strip. + * @param {boolean} [config.params.stripFragments] If true, strips URL fragments (hash). * @param {Object} userConsent The user consent object. */ export function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { logInfo(MODULE_NAME, "getBidRequestData:", "starting getBidRequestData", config); - const { websiteToAnalyseUrl, neuwoApiUrl, neuwoApiToken, iabContentTaxonomyVersion } = - config.params; + const { + websiteToAnalyseUrl, + neuwoApiUrl, + neuwoApiToken, + iabContentTaxonomyVersion, + enableCache = true, + stripAllQueryParams, + stripQueryParamsForDomains, + stripQueryParams, + stripFragments, + } = config.params; - const pageUrl = encodeURIComponent(websiteToAnalyseUrl || getRefererInfo().page); + const rawUrl = websiteToAnalyseUrl || getRefererInfo().page; + const processedUrl = cleanUrl(rawUrl, { + stripAllQueryParams, + stripQueryParamsForDomains, + stripQueryParams, + stripFragments + }); + const pageUrl = encodeURIComponent(processedUrl); // Adjusted for pages api.url?prefix=test (to add params with '&') as well as api.url (to add params with '?') const joiner = neuwoApiUrl.indexOf("?") < 0 ? "?" : "&"; const neuwoApiUrlFull = @@ -75,8 +116,13 @@ export function getBidRequestData(reqBidsConfigObj, callback, config, userConsen const success = (response) => { logInfo(MODULE_NAME, "getBidRequestData:", "Neuwo API raw response:", response); try { - const responseJson = JSON.parse(response); - injectIabCategories(responseJson, reqBidsConfigObj, iabContentTaxonomyVersion); + const responseParsed = JSON.parse(response); + + if (enableCache) { + globalCachedResponse = responseParsed; + } + + injectIabCategories(responseParsed, reqBidsConfigObj, iabContentTaxonomyVersion); } catch (ex) { logError(MODULE_NAME, "getBidRequestData:", "Error while processing Neuwo API response", ex); } @@ -88,15 +134,102 @@ export function getBidRequestData(reqBidsConfigObj, callback, config, userConsen callback(); }; - ajax(neuwoApiUrlFull, { success, error }, null); + if (enableCache && globalCachedResponse) { + logInfo(MODULE_NAME, "getBidRequestData:", "Using cached response:", globalCachedResponse); + injectIabCategories(globalCachedResponse, reqBidsConfigObj, iabContentTaxonomyVersion); + callback(); + } else { + logInfo(MODULE_NAME, "getBidRequestData:", "Calling Neuwo API Endpoint: ", neuwoApiUrlFull); + ajax(neuwoApiUrlFull, { success, error }, null); + } } // // HELPER FUNCTIONS // +/** + * Cleans a URL by stripping query parameters and/or fragments based on the provided configuration. + * + * @param {string} url The URL to clean. + * @param {Object} options Cleaning options. + * @param {boolean} [options.stripAllQueryParams] If true, strips all query parameters. + * @param {string[]} [options.stripQueryParamsForDomains] List of domains for which to strip all query params. + * @param {string[]} [options.stripQueryParams] List of specific query parameter names to strip. + * @param {boolean} [options.stripFragments] If true, strips URL fragments (hash). + * @returns {string} The cleaned URL. + */ +export function cleanUrl(url, options = {}) { + const { stripAllQueryParams, stripQueryParamsForDomains, stripQueryParams, stripFragments } = options; + + if (!url) { + logInfo(MODULE_NAME, "cleanUrl:", "Empty or null URL provided, returning as-is"); + return url; + } + + logInfo(MODULE_NAME, "cleanUrl:", "Input URL:", url, "Options:", options); + + try { + const urlObj = new URL(url); + + // Strip fragments if requested + if (stripFragments === true) { + urlObj.hash = ""; + logInfo(MODULE_NAME, "cleanUrl:", "Stripped fragment from URL"); + } + + // Option 1: Strip all query params unconditionally + if (stripAllQueryParams === true) { + urlObj.search = ""; + const cleanedUrl = urlObj.toString(); + logInfo(MODULE_NAME, "cleanUrl:", "Output URL:", cleanedUrl); + return cleanedUrl; + } + + // Option 2: Strip all query params for specific domains + if (Array.isArray(stripQueryParamsForDomains) && stripQueryParamsForDomains.length > 0) { + const hostname = urlObj.hostname; + const shouldStripForDomain = stripQueryParamsForDomains.some(domain => { + // Support exact match or subdomain match + return hostname === domain || hostname.endsWith("." + domain); + }); + + if (shouldStripForDomain) { + urlObj.search = ""; + const cleanedUrl = urlObj.toString(); + logInfo(MODULE_NAME, "cleanUrl:", "Output URL:", cleanedUrl); + return cleanedUrl; + } + } + + // Option 3: Strip specific query parameters + // Caveats: + // - "?=value" is treated as query parameter with key "" and value "value" + // - "??" is treated as query parameter with key "?" and value "" + if (Array.isArray(stripQueryParams) && stripQueryParams.length > 0) { + const queryParams = urlObj.searchParams; + logInfo(MODULE_NAME, "cleanUrl:", `Query parameters to strip: ${stripQueryParams}`); + stripQueryParams.forEach(param => { + queryParams.delete(param); + }); + urlObj.search = queryParams.toString(); + const cleanedUrl = urlObj.toString(); + logInfo(MODULE_NAME, "cleanUrl:", "Output URL:", cleanedUrl); + return cleanedUrl; + } + + const finalUrl = urlObj.toString(); + logInfo(MODULE_NAME, "cleanUrl:", "Output URL:", finalUrl); + return finalUrl; + } catch (e) { + logError(MODULE_NAME, "cleanUrl:", "Error cleaning URL:", e); + return url; + } +} + /** * Injects data into the OpenRTB 2.x global fragments of the bid request object. + * * @param {Object} reqBidsConfigObj The main bid request configuration object. * @param {string} path The dot-notation path where the data should be injected (e.g., 'site.content.data'). * @param {*} data The data to inject at the specified path. @@ -109,6 +242,7 @@ export function injectOrtbData(reqBidsConfigObj, path, data) { /** * Builds an IAB category data object for use in OpenRTB. + * * @param {Object} marketingCategories Marketing Categories returned by Neuwo API. * @param {string[]} tiers The tier keys to extract from marketingCategories. * @param {number} segtax The IAB taxonomy version Id. @@ -141,12 +275,13 @@ export function buildIabData(marketingCategories, tiers, segtax) { /** * Processes the Neuwo API response to build and inject IAB content and audience categories * into the bid request object. - * @param {Object} responseJson The parsed JSON response from the Neuwo API. + * + * @param {Object} responseParsed The parsed JSON response from the Neuwo API. * @param {Object} reqBidsConfigObj The bid request configuration object to be modified. * @param {string} iabContentTaxonomyVersion The version of the IAB content taxonomy to use for segtax mapping. */ -function injectIabCategories(responseJson, reqBidsConfigObj, iabContentTaxonomyVersion) { - const marketingCategories = responseJson.marketing_categories; +function injectIabCategories(responseParsed, reqBidsConfigObj, iabContentTaxonomyVersion) { + const marketingCategories = responseParsed.marketing_categories; if (!marketingCategories) { logError(MODULE_NAME, "injectIabCategories:", "No Marketing Categories in Neuwo API response."); diff --git a/modules/neuwoRtdProvider.md b/modules/neuwoRtdProvider.md index acd3f27d3ff..804130be1e6 100644 --- a/modules/neuwoRtdProvider.md +++ b/modules/neuwoRtdProvider.md @@ -63,41 +63,98 @@ ortb2: { } ``` -To get started, you can generate your API token at [https://neuwo.ai/generatetoken/](https://neuwo.ai/generatetoken/) or [contact us here](https://neuwo.ai/contact-us/). +To get started, you can generate your API token at [https://neuwo.ai/generatetoken/](https://neuwo.ai/generatetoken/), send us an email to [neuwo-helpdesk@neuwo.ai](mailto:neuwo-helpdesk@neuwo.ai) or [contact us here](https://neuwo.ai/contact-us/). ## Configuration -> **Important:** You must add the domain (origin) where Prebid.js is running to the list of allowed origins in Neuwo Edge API configuration. If you have problems, [contact us here](https://neuwo.ai/contact-us/). +> **Important:** You must add the domain (origin) where Prebid.js is running to the list of allowed origins in Neuwo Edge API configuration. If you have problems, send us an email to [neuwo-helpdesk@neuwo.ai](mailto:neuwo-helpdesk@neuwo.ai) or [contact us here](https://neuwo.ai/contact-us/). This module is configured as part of the `realTimeData.dataProviders` object. ```javascript pbjs.setConfig({ realTimeData: { - dataProviders: [{ - name: 'NeuwoRTDModule', - params: { - neuwoApiUrl: '', - neuwoApiToken: '', - iabContentTaxonomyVersion: '3.0', - } - }] - } + auctionDelay: 500, // Value can be adjusted based on the needs + dataProviders: [ + { + name: "NeuwoRTDModule", + waitForIt: true, + params: { + neuwoApiUrl: "", + neuwoApiToken: "", + iabContentTaxonomyVersion: "3.0", + enableCache: true, // Default: true. Caches API responses to avoid redundant requests + }, + }, + ], + }, }); ``` **Parameters** -| Name | Type | Required | Default | Description | -| :--------------------------------- | :----- | :------- | :------ | :------------------------------------------------------------------------------------------------ | -| `name` | String | Yes | | The name of the module, which is `NeuwoRTDModule`. | -| `params` | Object | Yes | | Container for module-specific parameters. | -| `params.neuwoApiUrl` | String | Yes | | The endpoint URL for the Neuwo Edge API. | -| `params.neuwoApiToken` | String | Yes | | Your unique API token provided by Neuwo. | -| `params.iabContentTaxonomyVersion` | String | No | `'3.0'` | Specifies the version of the IAB Content Taxonomy to be used. Supported values: `'2.2'`, `'3.0'`. | +| Name | Type | Required | Default | Description | +| :---------------------------------- | :------- | :------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | String | Yes | | The name of the module, which is `NeuwoRTDModule`. | +| `params` | Object | Yes | | Container for module-specific parameters. | +| `params.neuwoApiUrl` | String | Yes | | The endpoint URL for the Neuwo Edge API. | +| `params.neuwoApiToken` | String | Yes | | Your unique API token provided by Neuwo. | +| `params.iabContentTaxonomyVersion` | String | No | `'3.0'` | Specifies the version of the IAB Content Taxonomy to be used. Supported values: `'2.2'`, `'3.0'`. | +| `params.enableCache` | Boolean | No | `true` | If `true`, caches API responses to avoid redundant requests for the same page during the session. Set to `false` to disable caching and make a fresh API call on every bid request. | +| `params.stripAllQueryParams` | Boolean | No | `false` | If `true`, strips all query parameters from the URL before analysis. Takes precedence over other stripping options. | +| `params.stripQueryParamsForDomains` | String[] | No | `[]` | List of domains for which to strip **all** query parameters. When a domain matches, all query params are removed for that domain and all its subdomains (e.g., `'example.com'` strips params for both `'example.com'` and `'sub.example.com'`). This option takes precedence over `stripQueryParams` for matching domains. | +| `params.stripQueryParams` | String[] | No | `[]` | List of specific query parameter names to strip from the URL (e.g., `['utm_source', 'fbclid']`). Other parameters are preserved. Only applies when the domain does not match `stripQueryParamsForDomains`. | +| `params.stripFragments` | Boolean | No | `false` | If `true`, strips URL fragments (hash, e.g., `#section`) from the URL before analysis. | + +### API Response Caching + +By default, the module caches API responses during the page session to optimise performance and reduce redundant API calls. This behaviour can be disabled by setting `enableCache: false` if needed for dynamic content scenarios. + +### URL Cleaning Options + +The module provides optional URL cleaning capabilities to strip query parameters and/or fragments from the analysed URL before sending it to the Neuwo API. This can be useful for privacy, caching, or analytics purposes. + +**Example with URL cleaning:** + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 500, // Value can be adjusted based on the needs + dataProviders: [ + { + name: "NeuwoRTDModule", + waitForIt: true, + params: { + neuwoApiUrl: "", + neuwoApiToken: "", + iabContentTaxonomyVersion: "3.0", + + // Option 1: Strip all query parameters from the URL + stripAllQueryParams: true, + + // Option 2: Strip all query parameters only for specific domains + // stripQueryParamsForDomains: ['example.com', 'another-domain.com'], + + // Option 3: Strip specific query parameters by name + // stripQueryParams: ['utm_source', 'utm_campaign', 'fbclid'], + + // Optional: Strip URL fragments (hash) + stripFragments: true, + }, + }, + ], + }, +}); +``` ## Local Development +Install the exact versions of packages specified in the lockfile: + +```bash +npm ci +``` + > **Linux** Linux might require exporting the following environment variable before running the commands below: > `export CHROME_BIN=/usr/bin/chromium` @@ -110,20 +167,38 @@ npx gulp serve --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter For a faster build without tests: ```bash -npx gulp serve-fast --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter --notests +npx gulp serve-fast --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter ``` After starting the server, you can access the example page at: [http://localhost:9999/integrationExamples/gpt/neuwoRtdProvider_example.html](http://localhost:9999/integrationExamples/gpt/neuwoRtdProvider_example.html) ### Add development tools if necessary + If you don't have gulp-cli installed globally, run the following command in your Prebid.js source folder: + ```bash npm i -g gulp-cli ``` +## Linting + +To lint the module: + +```bash +npx eslint 'modules/neuwoRtdProvider.js' --cache --cache-strategy content +``` + ## Testing + To run the module-specific tests: + +```bash +npx gulp test-only --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter --file=test/spec/modules/euwoRtdProvider_spec.js +``` + +Skip building, if the project has already been built: + ```bash -npx gulp test-only --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter --file=test/spec/modules/neuwoRtdProvider_spec.js -``` \ No newline at end of file +npx gulp test-only-nobuild --file=test/spec/modules/neuwoRtdProvider_spec.js +``` diff --git a/modules/nextMillenniumBidAdapter.js b/modules/nextMillenniumBidAdapter.js index 09472759521..ec089e151aa 100644 --- a/modules/nextMillenniumBidAdapter.js +++ b/modules/nextMillenniumBidAdapter.js @@ -22,8 +22,9 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getRefererInfo} from '../src/refererDetection.js'; import { getViewportSize } from '../libraries/viewport/viewport.js'; +import { getConnectionInfo } from '../libraries/connectionInfo/connectionUtils.js'; -const NM_VERSION = '4.5.0'; +const NM_VERSION = '4.5.1'; const PBJS_VERSION = 'v$prebid.version$'; const GVLID = 1060; const BIDDER_CODE = 'nextMillennium'; @@ -148,6 +149,7 @@ export const spec = { }, buildRequests: function(validBidRequests, bidderRequest) { + const bidIds = new Map() const requests = []; window.nmmRefreshCounts = window.nmmRefreshCounts || {}; const site = getSiteObj(); @@ -180,10 +182,14 @@ export const spec = { const id = getPlacementId(bid); const {cur, mediaTypes} = getCurrency(bid); if (i === 0) postBody.cur = cur; - const imp = getImp(bid, id, mediaTypes); + + const impId = String(i + 1) + bidIds.set(impId, bid.bidId) + + const imp = getImp(impId, bid, id, mediaTypes); setOrtb2Parameters(ALLOWED_ORTB2_IMP_PARAMETERS, imp, bid?.ortb2Imp); postBody.imp.push(imp); - postBody.ext.next_mil_imps.push(getExtNextMilImp(bid)); + postBody.ext.next_mil_imps.push(getExtNextMilImp(impId, bid)); }); this.getUrlPixelMetric(EVENTS.BID_REQUESTED, validBidRequests); @@ -196,19 +202,21 @@ export const spec = { contentType: 'text/plain', withCredentials: true, }, + + bidIds, }); return requests; }, - interpretResponse: function(serverResponse) { + interpretResponse: function(serverResponse, bidRequest) { const response = serverResponse.body; const bidResponses = []; const bids = []; _each(response.seatbid, (resp) => { _each(resp.bid, (bid) => { - const requestId = bid.impid; + const requestId = bidRequest.bidIds.get(bid.impid); const {ad, adUrl, vastUrl, vastXml} = getAd(bid); @@ -327,11 +335,11 @@ export const spec = { }, }; -export function getExtNextMilImp(bid) { +export function getExtNextMilImp(impId, bid) { if (typeof window?.nmmRefreshCounts[bid.adUnitCode] === 'number') ++window.nmmRefreshCounts[bid.adUnitCode]; const {adSlots, allowedAds} = bid.params const nextMilImp = { - impId: bid.bidId, + impId, nextMillennium: { nm_version: NM_VERSION, pbjs_version: PBJS_VERSION, @@ -346,10 +354,10 @@ export function getExtNextMilImp(bid) { return nextMilImp; } -export function getImp(bid, id, mediaTypes) { +export function getImp(impId, bid, id, mediaTypes) { const {banner, video} = mediaTypes; const imp = { - id: bid.bidId, + id: impId, ext: { prebid: { storedrequest: { @@ -576,14 +584,16 @@ function getDeviceObj() { } function getDeviceConnectionType() { - const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; - if (connection?.type === 'ethernet') return 1; - if (connection?.type === 'wifi') return 2; - - if (connection?.effectiveType === 'slow-2g') return 3; - if (connection?.effectiveType === '2g') return 4; - if (connection?.effectiveType === '3g') return 5; - if (connection?.effectiveType === '4g') return 6; + const connection = getConnectionInfo(); + const connectionType = connection?.type; + const effectiveType = connection?.effectiveType; + if (connectionType === 'ethernet') return 1; + if (connectionType === 'wifi') return 2; + + if (effectiveType === 'slow-2g') return 3; + if (effectiveType === '2g') return 4; + if (effectiveType === '3g') return 5; + if (effectiveType === '4g') return 6; return undefined; } diff --git a/modules/nexx360BidAdapter.js b/modules/nexx360BidAdapter.js deleted file mode 100644 index c89238b9242..00000000000 --- a/modules/nexx360BidAdapter.js +++ /dev/null @@ -1,192 +0,0 @@ -import { deepSetValue, generateUUID, logError, logInfo } from '../src/utils.js'; -import {getStorageManager} from '../src/storageManager.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {getGlobal} from '../src/prebidGlobal.js'; -import {ortbConverter} from '../libraries/ortbConverter/converter.js' - -import { createResponse, enrichImp, enrichRequest, getAmxId, getUserSyncs } from '../libraries/nexx360Utils/index.js'; -import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; - -/** - * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest - * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid - * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse - * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions - * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync - * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests - */ - -const BIDDER_CODE = 'nexx360'; -const REQUEST_URL = 'https://fast.nexx360.io/booster'; -const PAGE_VIEW_ID = generateUUID(); -const BIDDER_VERSION = '6.3'; -const GVLID = 965; -const NEXXID_KEY = 'nexx360_storage'; - -const ALIASES = [ - { code: 'revenuemaker' }, - { code: 'first-id', gvlid: 1178 }, - { code: 'adwebone' }, - { code: 'league-m', gvlid: 965 }, - { code: 'prjads' }, - { code: 'pubtech' }, - { code: '1accord', gvlid: 965 }, - { code: 'easybid', gvlid: 1068 }, - { code: 'prismassp', gvlid: 965 }, - { code: 'spm', gvlid: 965 }, - { code: 'bidstailamedia', gvlid: 965 }, - { code: 'scoremedia', gvlid: 965 }, - { code: 'movingup', gvlid: 1416 }, - { code: 'glomexbidder', gvlid: 967 }, - { code: 'revnew', gvlid: 1468 }, - { code: 'pubxai', gvlid: 1485 }, -]; - -export const STORAGE = getStorageManager({ - bidderCode: BIDDER_CODE, -}); - -/** - * Get the NexxId - * @param - * @return {object | false } false if localstorageNotEnabled - */ - -export function getNexx360LocalStorage() { - if (!STORAGE.localStorageIsEnabled()) { - logInfo(`localstorage not enabled for Nexx360`); - return false; - } - const output = STORAGE.getDataFromLocalStorage(NEXXID_KEY); - if (output === null) { - const nexx360Storage = { nexx360Id: generateUUID() }; - STORAGE.setDataInLocalStorage(NEXXID_KEY, JSON.stringify(nexx360Storage)); - return nexx360Storage; - } - try { - return JSON.parse(output) - } catch (e) { - return false; - } -} - -const converter = ortbConverter({ - context: { - netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false - ttl: 90, // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) - }, - imp(buildImp, bidRequest, context) { - let imp = buildImp(bidRequest, context); - imp = enrichImp(imp, bidRequest); - const divId = bidRequest.params.divId || bidRequest.adUnitCode; - const slotEl = document.getElementById(divId); - if (slotEl) { - const { width, height } = getBoundingClientRect(slotEl); - deepSetValue(imp, 'ext.dimensions.slotW', width); - deepSetValue(imp, 'ext.dimensions.slotH', height); - deepSetValue(imp, 'ext.dimensions.cssMaxW', slotEl.style?.maxWidth); - deepSetValue(imp, 'ext.dimensions.cssMaxH', slotEl.style?.maxHeight); - } - if (bidRequest.params.tagId) deepSetValue(imp, 'ext.nexx360.tagId', bidRequest.params.tagId); - if (bidRequest.params.placement) deepSetValue(imp, 'ext.nexx360.placement', bidRequest.params.placement); - if (bidRequest.params.videoTagId) deepSetValue(imp, 'ext.nexx360.videoTagId', bidRequest.params.videoTagId); - if (bidRequest.params.adUnitPath) deepSetValue(imp, 'ext.adUnitPath', bidRequest.params.adUnitPath); - if (bidRequest.params.adUnitName) deepSetValue(imp, 'ext.adUnitName', bidRequest.params.adUnitName); - if (bidRequest.params.allBids) deepSetValue(imp, 'ext.nexx360.allBids', bidRequest.params.allBids); - return imp; - }, - request(buildRequest, imps, bidderRequest, context) { - let request = buildRequest(imps, bidderRequest, context); - const amxId = getAmxId(STORAGE, BIDDER_CODE); - request = enrichRequest(request, amxId, bidderRequest, PAGE_VIEW_ID, BIDDER_VERSION); - return request; - }, -}); - -/** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ -function isBidRequestValid(bid) { - if (bid.params.adUnitName && (typeof bid.params.adUnitName !== 'string' || bid.params.adUnitName === '')) { - logError('bid.params.adUnitName needs to be a string'); - return false; - } - if (bid.params.adUnitPath && (typeof bid.params.adUnitPath !== 'string' || bid.params.adUnitPath === '')) { - logError('bid.params.adUnitPath needs to be a string'); - return false; - } - if (bid.params.divId && (typeof bid.params.divId !== 'string' || bid.params.divId === '')) { - logError('bid.params.divId needs to be a string'); - return false; - } - if (bid.params.allBids && typeof bid.params.allBids !== 'boolean') { - logError('bid.params.allBids needs to be a boolean'); - return false; - } - if (!bid.params.tagId && !bid.params.videoTagId && !bid.params.nativeTagId && !bid.params.placement) { - logError('bid.params.tagId or bid.params.videoTagId or bid.params.nativeTagId or bid.params.placement must be defined'); - return false; - } - return true; -}; - -/** - * Make a server request from the list of BidRequests. - * - * @return ServerRequest Info describing the request to the server. - */ - -function buildRequests(bidRequests, bidderRequest) { - const data = converter.toORTB({bidRequests, bidderRequest}) - return { - method: 'POST', - url: REQUEST_URL, - data, - } -} - -/** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ - -function interpretResponse(serverResponse) { - const respBody = serverResponse.body; - if (!respBody || !Array.isArray(respBody.seatbid)) { - return []; - } - - const { bidderSettings } = getGlobal(); - const allowAlternateBidderCodes = bidderSettings && bidderSettings.standard ? bidderSettings.standard.allowAlternateBidderCodes : false; - - const responses = []; - for (let i = 0; i < respBody.seatbid.length; i++) { - const seatbid = respBody.seatbid[i]; - for (let j = 0; j < seatbid.bid.length; j++) { - const bid = seatbid.bid[j]; - const response = createResponse(bid, respBody); - if (allowAlternateBidderCodes) response.bidderCode = `n360_${bid.ext.ssp}`; - responses.push(response); - } - } - return responses; -} - -export const spec = { - code: BIDDER_CODE, - gvlid: GVLID, - aliases: ALIASES, - supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid, - buildRequests, - interpretResponse, - getUserSyncs, -}; - -registerBidder(spec); diff --git a/modules/nexx360BidAdapter.ts b/modules/nexx360BidAdapter.ts new file mode 100644 index 00000000000..a5aa9e92dd7 --- /dev/null +++ b/modules/nexx360BidAdapter.ts @@ -0,0 +1,168 @@ +import { deepSetValue, generateUUID, logError } from '../src/utils.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {AdapterRequest, BidderSpec, registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' + +import { interpretResponse, enrichImp, enrichRequest, getAmxId, getLocalStorageFunctionGenerator, getUserSyncs } from '../libraries/nexx360Utils/index.js'; +import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; +import { BidRequest, ClientBidderRequest } from '../src/adapterManager.js'; +import { ORTBImp, ORTBRequest } from '../src/prebid.public.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'nexx360'; +const REQUEST_URL = 'https://fast.nexx360.io/booster'; +const PAGE_VIEW_ID = generateUUID(); +const BIDDER_VERSION = '7.1'; +const GVLID = 965; +const NEXXID_KEY = 'nexx360_storage'; + +const DEFAULT_GZIP_ENABLED = false; + +type RequireAtLeastOne = + Omit & { + [K in Keys]-?: Required> & + Partial>> + }[Keys]; + +type Nexx360BidParams = RequireAtLeastOne<{ + tagId?: string; + placement?: string; + videoTagId?: string; + nativeTagId?: string; + adUnitPath?: string; + adUnitName?: string; + divId?: string; + allBids?: boolean; + customId?: string; + bidders?: Record; +}, "tagId" | "placement">; + +declare module '../src/adUnits' { + interface BidderParams { + [BIDDER_CODE]: Nexx360BidParams; + } +} + +const ALIASES = [ + { code: 'revenuemaker' }, + { code: 'first-id', gvlid: 1178 }, + { code: 'adwebone' }, + { code: 'league-m', gvlid: 965 }, + { code: 'prjads' }, + { code: 'pubtech' }, + { code: '1accord', gvlid: 965 }, + { code: 'easybid', gvlid: 1068 }, + { code: 'prismassp', gvlid: 965 }, + { code: 'spm', gvlid: 965 }, + { code: 'bidstailamedia', gvlid: 965 }, + { code: 'scoremedia', gvlid: 965 }, + { code: 'movingup', gvlid: 1416 }, + { code: 'glomexbidder', gvlid: 967 }, + { code: 'pubxai', gvlid: 1485 }, + { code: 'ybidder', gvlid: 1253 }, + { code: 'netads', gvlid: 965 }, +]; + +export const STORAGE = getStorageManager({ + bidderCode: BIDDER_CODE, +}); + +export const getNexx360LocalStorage = getLocalStorageFunctionGenerator<{ nexx360Id: string }>( + STORAGE, + BIDDER_CODE, + NEXXID_KEY, + 'nexx360Id' +); + +export const getGzipSetting = (): boolean => { + const getBidderConfig = config.getBidderConfig(); + if (getBidderConfig.nexx360?.gzipEnabled === 'true') { + return getBidderConfig.nexx360?.gzipEnabled === 'true'; + } + return DEFAULT_GZIP_ENABLED; +} + +const converter = ortbConverter({ + context: { + netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false + ttl: 90, // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + }, + imp(buildImp, bidRequest: BidRequest, context) { + let imp:ORTBImp = buildImp(bidRequest, context); + imp = enrichImp(imp, bidRequest); + const divId = bidRequest.params.divId || bidRequest.adUnitCode; + const slotEl:HTMLElement | null = typeof divId === 'string' ? document.getElementById(divId) : null; + if (slotEl) { + const { width, height } = getBoundingClientRect(slotEl); + deepSetValue(imp, 'ext.dimensions.slotW', width); + deepSetValue(imp, 'ext.dimensions.slotH', height); + deepSetValue(imp, 'ext.dimensions.cssMaxW', slotEl.style?.maxWidth); + deepSetValue(imp, 'ext.dimensions.cssMaxH', slotEl.style?.maxHeight); + } + deepSetValue(imp, 'ext.nexx360', bidRequest.params); + deepSetValue(imp, 'ext.nexx360.divId', divId); + if (bidRequest.params.adUnitPath) deepSetValue(imp, 'ext.adUnitPath', bidRequest.params.adUnitPath); + if (bidRequest.params.adUnitName) deepSetValue(imp, 'ext.adUnitName', bidRequest.params.adUnitName); + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + let request:ORTBRequest = buildRequest(imps, bidderRequest, context); + const amxId = getAmxId(STORAGE, BIDDER_CODE); + request = enrichRequest(request, amxId, PAGE_VIEW_ID, BIDDER_VERSION); + return request; + }, +}); + +const isBidRequestValid = (bid:BidRequest): boolean => { + if (bid.params.adUnitName && (typeof bid.params.adUnitName !== 'string' || bid.params.adUnitName === '')) { + logError('bid.params.adUnitName needs to be a string'); + return false; + } + if (bid.params.adUnitPath && (typeof bid.params.adUnitPath !== 'string' || bid.params.adUnitPath === '')) { + logError('bid.params.adUnitPath needs to be a string'); + return false; + } + if (bid.params.divId && (typeof bid.params.divId !== 'string' || bid.params.divId === '')) { + logError('bid.params.divId needs to be a string'); + return false; + } + if (bid.params.allBids && typeof bid.params.allBids !== 'boolean') { + logError('bid.params.allBids needs to be a boolean'); + return false; + } + if (!bid.params.tagId && !bid.params.videoTagId && !bid.params.nativeTagId && !bid.params.placement) { + logError('bid.params.tagId or bid.params.videoTagId or bid.params.nativeTagId or bid.params.placement must be defined'); + return false; + } + return true; +}; + +const buildRequests = ( + bidRequests: BidRequest[], + bidderRequest: ClientBidderRequest, +): AdapterRequest => { + const data:ORTBRequest = converter.toORTB({bidRequests, bidderRequest}) + const adapterRequest:AdapterRequest = { + method: 'POST', + url: REQUEST_URL, + data, + options: { + endpointCompression: getGzipSetting() + }, + } + return adapterRequest; +} + +export const spec:BidderSpec = { + code: BIDDER_CODE, + gvlid: GVLID, + aliases: ALIASES, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, +}; + +registerBidder(spec); diff --git a/modules/nodalsAiRtdProvider.js b/modules/nodalsAiRtdProvider.js index 8cf0df0bde2..104e91ab851 100644 --- a/modules/nodalsAiRtdProvider.js +++ b/modules/nodalsAiRtdProvider.js @@ -11,7 +11,7 @@ const GVLID = 1360; const ENGINE_VESION = '1.x.x'; const PUB_ENDPOINT_ORIGIN = 'https://nodals.io'; const LOCAL_STORAGE_KEY = 'signals.nodals.ai'; -const STORAGE_TTL = 3600; // 1 hour in seconds +const DEFAULT_STORAGE_TTL = 3600; // 1 hour in seconds const fillTemplate = (strings, ...keys) => { return function (values) { @@ -204,7 +204,11 @@ class NodalsAiRtdProvider { } #getEngine() { - return window?.$nodals?.adTargetingEngine[ENGINE_VESION]; + try { + return window?.$nodals?.adTargetingEngine?.[ENGINE_VESION]; + } catch (error) { + return undefined; + } } #setOverrides(params) { @@ -320,7 +324,7 @@ class NodalsAiRtdProvider { #dataIsStale(dataEnvelope) { const currentTime = Date.now(); const dataTime = dataEnvelope.createdAt || 0; - const staleThreshold = this.#overrides?.storageTTL ?? dataEnvelope?.data?.meta?.ttl ?? STORAGE_TTL; + const staleThreshold = this.#overrides?.storageTTL ?? dataEnvelope?.data?.meta?.ttl ?? DEFAULT_STORAGE_TTL; return currentTime - dataTime >= (staleThreshold * 1000); } diff --git a/modules/nubaBidAdapter.js b/modules/nubaBidAdapter.js index 0ebfe715508..6692a13bb1d 100644 --- a/modules/nubaBidAdapter.js +++ b/modules/nubaBidAdapter.js @@ -4,7 +4,7 @@ import { isBidRequestValid, buildRequests, interpretResponse } from '../librarie const BIDDER_CODE = 'nuba'; -const AD_URL = 'https://ads.nuba.io/openrtb2/auction'; +const AD_URL = 'https://ads.nuba.io/pbjs'; export const spec = { code: BIDDER_CODE, @@ -12,8 +12,7 @@ export const spec = { isBidRequestValid: isBidRequestValid(), buildRequests: buildRequests(AD_URL), - interpretResponse, - getUserSyncs: () => {}, + interpretResponse }; registerBidder(spec); diff --git a/modules/oguryBidAdapter.js b/modules/oguryBidAdapter.js index eed8fae088d..00c8bfb77ef 100644 --- a/modules/oguryBidAdapter.js +++ b/modules/oguryBidAdapter.js @@ -2,6 +2,7 @@ import { BANNER } from '../src/mediaTypes.js'; import { getWindowSelf, getWindowTop, isFn, deepAccess, isPlainObject, deepSetValue, mergeDeep } from '../src/utils.js'; +import { getDevicePixelRatio } from '../libraries/devicePixelRatio/devicePixelRatio.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { ajax } from '../src/ajax.js'; import { getAdUnitSizes } from '../libraries/sizeUtils/sizeUtils.js'; @@ -13,7 +14,7 @@ const DEFAULT_TIMEOUT = 1000; const BID_HOST = 'https://mweb-hb.presage.io/api/header-bidding-request'; const TIMEOUT_MONITORING_HOST = 'https://ms-ads-monitoring-events.presage.io'; const MS_COOKIE_SYNC_DOMAIN = 'https://ms-cookie-sync.presage.io'; -const ADAPTER_VERSION = '2.0.4'; +const ADAPTER_VERSION = '2.0.5'; export const ortbConverterProps = { context: { @@ -25,7 +26,7 @@ export const ortbConverterProps = { request(buildRequest, imps, bidderRequest, context) { const req = buildRequest(imps, bidderRequest, context); req.tmax = DEFAULT_TIMEOUT; - deepSetValue(req, 'device.pxratio', window.devicePixelRatio); + deepSetValue(req, 'device.pxratio', getDevicePixelRatio(getWindowContext())); deepSetValue(req, 'site.page', getWindowContext().location.href); req.ext = mergeDeep({}, req.ext, { @@ -99,15 +100,7 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, gpp return [ { type: 'image', - url: `${MS_COOKIE_SYNC_DOMAIN}/v1/init-sync/bid-switch?iab_string=${consent}&source=prebid&gpp=${gpp}&gpp_sid=${gppSid}` - }, - { - type: 'image', - url: `${MS_COOKIE_SYNC_DOMAIN}/ttd/init-sync?iab_string=${consent}&source=prebid&gpp=${gpp}&gpp_sid=${gppSid}` - }, - { - type: 'image', - url: `${MS_COOKIE_SYNC_DOMAIN}/xandr/init-sync?iab_string=${consent}&source=prebid&gpp=${gpp}&gpp_sid=${gppSid}` + url: `${MS_COOKIE_SYNC_DOMAIN}/user-sync?source=prebid&gdpr_consent=${consent}&gpp=${gpp}&gpp_sid=${gppSid}` } ]; } diff --git a/modules/omsBidAdapter.js b/modules/omsBidAdapter.js index 289a763a8ac..fd7be06409e 100644 --- a/modules/omsBidAdapter.js +++ b/modules/omsBidAdapter.js @@ -38,7 +38,7 @@ export const spec = { function buildRequests(bidReqs, bidderRequest) { try { const impressions = bidReqs.map(bid => { - let bidSizes = bid?.mediaTypes?.banner?.sizes || bid?.mediaTypes?.video?.playerSize || bid.sizes; + let bidSizes = bid?.mediaTypes?.banner?.sizes || bid.sizes || []; bidSizes = ((isArray(bidSizes) && isArray(bidSizes[0])) ? bidSizes : [bidSizes]); bidSizes = bidSizes.filter(size => isArray(size)); const processedSizes = bidSizes.map(size => ({w: parseInt(size[0], 10), h: parseInt(size[1], 10)})); @@ -51,18 +51,21 @@ function buildRequests(bidReqs, bidderRequest) { const imp = { id: bid.bidId, - banner: { - format: processedSizes, - ext: { - viewability: viewabilityAmountRounded, - } - }, ext: { ...gpidData }, tagid: String(bid.adUnitCode) }; + if (bid?.mediaTypes?.banner) { + imp.banner = { + format: processedSizes, + ext: { + viewability: viewabilityAmountRounded, + } + } + } + if (bid?.mediaTypes?.video) { imp.video = { ...bid.mediaTypes.video, @@ -172,7 +175,6 @@ function interpretResponse(serverResponse) { creativeId: bid.crid || bid.id, currency: 'USD', netRevenue: true, - ad: _getAdMarkup(bid), ttl: 300, meta: { advertiserDomains: bid?.adomain || [] @@ -181,8 +183,10 @@ function interpretResponse(serverResponse) { if (bid.mtype === 2) { bidResponse.mediaType = VIDEO; + bidResponse.vastXml = bid.adm; } else { bidResponse.mediaType = BANNER; + bidResponse.ad = _getAdMarkup(bid); } return bidResponse; diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index e2da98a67be..d919d1398b7 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -8,6 +8,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { deepClone, logError, deepAccess, getWinDimensions } from '../src/utils.js'; import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; import { toOrtbNativeRequest } from '../src/native.js'; +import { getConnectionInfo } from '../libraries/connectionInfo/connectionUtils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -81,14 +82,14 @@ export function isValid(type, bid) { return false; } -const isValidEventTracker = function(et) { +const isValidEventTracker = function (et) { if (!et.event || !et.methods || !Number.isInteger(et.event) || !Array.isArray(et.methods) || !et.methods.length > 0) { return false; } return true; } -const isValidAsset = function(asset) { +const isValidAsset = function (asset) { if (!asset.hasOwnProperty("id") || !Number.isInteger(asset.id)) return false; const hasValidContent = asset.title || asset.img || asset.data || asset.video; if (!hasValidContent) return false; @@ -141,9 +142,9 @@ function buildRequests(validBidRequests, bidderRequest) { payload.onetagSid = storage.getDataFromLocalStorage('onetag_sid'); } } catch (e) { } - const connection = navigator.connection || navigator.webkitConnection; - payload.networkConnectionType = (connection && connection.type) ? connection.type : null; - payload.networkEffectiveConnectionType = (connection && connection.effectiveType) ? connection.effectiveType : null; + const connection = getConnectionInfo(); + payload.networkConnectionType = connection?.type || null; + payload.networkEffectiveConnectionType = connection?.effectiveType || null; payload.fledgeEnabled = Boolean(bidderRequest?.paapi?.enabled) return { method: 'POST', @@ -209,7 +210,8 @@ function interpretResponse(serverResponse, bidderRequest) { const fledgeAuctionConfigs = body.fledgeAuctionConfigs return { bids, - paapi: fledgeAuctionConfigs} + paapi: fledgeAuctionConfigs + } } else { return bids; } @@ -288,8 +290,6 @@ function getPageInfo(bidderRequest) { wHeight: winDimensions.innerHeight, sWidth: winDimensions.screen.width, sHeight: winDimensions.screen.height, - sLeft: 'screenLeft' in topmostFrame ? topmostFrame.screenLeft : topmostFrame.screenX, - sTop: 'screenTop' in topmostFrame ? topmostFrame.screenTop : topmostFrame.screenY, xOffset: topmostFrame.pageXOffset, yOffset: topmostFrame.pageYOffset, docHidden: getDocumentVisibility(topmostFrame), @@ -298,7 +298,7 @@ function getPageInfo(bidderRequest) { timing: getTiming(), version: { prebid: '$prebid.version$', - adapter: '1.1.5' + adapter: '1.1.6' } }; } @@ -482,7 +482,7 @@ function getBidFloor(bidRequest, mediaType, sizes) { return { ...floorData, - size: size && size.length === 2 ? {width: size[0], height: size[1]} : null, + size: size && size.length === 2 ? { width: size[0], height: size[1] } : null, floor: floorData.floor != null ? floorData.floor : null }; }; diff --git a/modules/ooloAnalyticsAdapter.js b/modules/ooloAnalyticsAdapter.js index 22b8476ef54..2bcacd92dd9 100644 --- a/modules/ooloAnalyticsAdapter.js +++ b/modules/ooloAnalyticsAdapter.js @@ -22,6 +22,7 @@ const prebidVersion = '$prebid.version$' const analyticsType = 'endpoint' const ADAPTER_CODE = 'oolo' const AUCTION_END_SEND_TIMEOUT = 1500 +// TODO: consider using the Prebid-generated page view ID instead of generating a custom one export const PAGEVIEW_ID = +generatePageViewId() const { diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index f82e0337e7f..8645196d07d 100644 --- a/modules/operaadsBidAdapter.js +++ b/modules/operaadsBidAdapter.js @@ -28,8 +28,8 @@ import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; */ const BIDDER_CODE = 'operaads'; -const ENDPOINT = 'https://s.adx.opera.com/ortb/v2/'; -const USER_SYNC_ENDPOINT = 'https://s.adx.opera.com/usersync/page'; +const ENDPOINT = 'https://s.oa.opera.com/ortb/v2/'; +const USER_SYNC_ENDPOINT = 'https://s.oa.opera.com/usersync/page'; const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; diff --git a/modules/optableRtdProvider.js b/modules/optableRtdProvider.js index 2ef71ce9d44..29638ba3a94 100644 --- a/modules/optableRtdProvider.js +++ b/modules/optableRtdProvider.js @@ -17,6 +17,7 @@ export const parseConfig = (moduleConfig) => { let bundleUrl = deepAccess(moduleConfig, 'params.bundleUrl', null); const adserverTargeting = deepAccess(moduleConfig, 'params.adserverTargeting', true); const handleRtd = deepAccess(moduleConfig, 'params.handleRtd', null); + const instance = deepAccess(moduleConfig, 'params.instance', null); // If present, trim the bundle URL if (typeof bundleUrl === 'string') { @@ -25,18 +26,51 @@ export const parseConfig = (moduleConfig) => { // Verify that bundleUrl is a valid URL: only secure (HTTPS) URLs are allowed if (typeof bundleUrl === 'string' && bundleUrl.length && !bundleUrl.startsWith('https://')) { - throw new Error( - LOG_PREFIX + ' Invalid URL format for bundleUrl in moduleConfig. Only HTTPS URLs are allowed.' - ); + logError('Invalid URL format for bundleUrl in moduleConfig. Only HTTPS URLs are allowed.'); + return {bundleUrl: null, adserverTargeting, handleRtd: null}; } if (handleRtd && typeof handleRtd !== 'function') { - throw new Error(LOG_PREFIX + ' handleRtd must be a function'); + logError('handleRtd must be a function'); + return {bundleUrl, adserverTargeting, handleRtd: null}; } - return {bundleUrl, adserverTargeting, handleRtd}; + const result = {bundleUrl, adserverTargeting, handleRtd}; + if (instance !== null) { + result.instance = instance; + } + return result; } +/** + * Wait for Optable SDK event to fire with targeting data + * @param {string} eventName Name of the event to listen for + * @returns {Promise} Promise that resolves with targeting data or null + */ +const waitForOptableEvent = (eventName) => { + return new Promise((resolve) => { + const optableBundle = /** @type {Object} */ (window.optable); + const cachedData = optableBundle?.instance?.targetingFromCache(); + + if (cachedData && cachedData.ortb2) { + logMessage('Optable SDK already has cached data'); + resolve(cachedData); + return; + } + + const eventListener = (event) => { + logMessage(`Received ${eventName} event`); + // Extract targeting data from event detail + const targetingData = event.detail; + window.removeEventListener(eventName, eventListener); + resolve(targetingData); + }; + + window.addEventListener(eventName, eventListener); + logMessage(`Waiting for ${eventName} event`); + }); +}; + /** * Default function to handle/enrich RTD data * @param reqBidsConfigObj Bid request configuration object @@ -45,15 +79,8 @@ export const parseConfig = (moduleConfig) => { * @returns {Promise} */ export const defaultHandleRtd = async (reqBidsConfigObj, optableExtraData, mergeFn) => { - const optableBundle = /** @type {Object} */ (window.optable); - // Get targeting data from cache, if available - let targetingData = optableBundle?.instance?.targetingFromCache(); - // If no targeting data is found in the cache, call the targeting function - if (!targetingData) { - // Call Optable DCN for targeting data and return the ORTB2 object - targetingData = await optableBundle?.instance?.targeting(); - } - logMessage('Original targeting data from targeting(): ', targetingData); + // Wait for the Optable SDK to dispatch targeting data via event + let targetingData = await waitForOptableEvent('optable-targeting:change'); if (!targetingData || !targetingData.ortb2) { logWarn('No targeting data found'); @@ -92,7 +119,6 @@ export const getBidRequestData = (reqBidsConfigObj, callback, moduleConfig, user try { // Extract the bundle URL from the module configuration const {bundleUrl, handleRtd} = parseConfig(moduleConfig); - const handleRtdFn = handleRtd || defaultHandleRtd; const optableExtraData = config.getConfig('optableRtdConfig') || {}; @@ -135,8 +161,8 @@ export const getBidRequestData = (reqBidsConfigObj, callback, moduleConfig, user * @returns {Object} Targeting data */ export const getTargetingData = (adUnits, moduleConfig, userConsent, auction) => { - // Extract `adserverTargeting` from the module configuration - const {adserverTargeting} = parseConfig(moduleConfig); + // Extract `adserverTargeting` and `instance` from the module configuration + const {adserverTargeting, instance} = parseConfig(moduleConfig); logMessage('Ad Server targeting: ', adserverTargeting); if (!adserverTargeting) { @@ -145,9 +171,17 @@ export const getTargetingData = (adUnits, moduleConfig, userConsent, auction) => } const targetingData = {}; + // Resolve the SDK instance object based on the instance string + // Default to 'instance' if not provided + const instanceKey = instance || 'instance'; + const sdkInstance = window?.optable?.[instanceKey]; + if (!sdkInstance) { + logWarn(`No Optable SDK instance found for: ${instanceKey}`); + return targetingData; + } // Get the Optable targeting data from the cache - const optableTargetingData = window?.optable?.instance?.targetingKeyValuesFromCache() || {}; + const optableTargetingData = sdkInstance?.targetingKeyValuesFromCache?.() || targetingData; // If no Optable targeting data is found, return an empty object if (!Object.keys(optableTargetingData).length) { diff --git a/modules/optableRtdProvider.md b/modules/optableRtdProvider.md index 45fc7d589d7..4ac0d4541f4 100644 --- a/modules/optableRtdProvider.md +++ b/modules/optableRtdProvider.md @@ -46,7 +46,8 @@ pbjs.setConfig({ { name: 'optable', params: { - adserverTargeting: '', + adserverTargeting: true, // optional, true by default, set to true to also set GAM targeting keywords to ad slots + instance: window.optable.rtd.instance, // optional, defaults to window.optable.rtd.instance if not specified }, }, ], @@ -56,12 +57,13 @@ pbjs.setConfig({ ### Parameters -| Name | Type | Description | Default | Notes | -|--------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------|----------| -| name | String | Real time data module name | Always `optable` | | -| params | Object | | | | -| params.adserverTargeting | Boolean | If set to `true`, targeting keywords will be passed to the ad server upon auction completion | `true` | Optional | -| params.handleRtd | Function | An optional function that uses Optable data to enrich `reqBidsConfigObj` with the real-time data. If not provided, the module will do a default call to Optable bundle. The function signature is `[async] (reqBidsConfigObj, optableExtraData, mergeFn) => {}` | `null` | Optional | +| Name | Type | Description | Default | Notes | +|--------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------|----------| +| name | String | Real time data module name | Always `optable` | | +| params | Object | | | | +| params.adserverTargeting | Boolean | If set to `true`, targeting keywords will be passed to the ad server upon auction completion | `true` | Optional | +| params.instance | Object | Optable SDK instance to use for targeting data. | `window.optable.rtd.instance` | Optional | +| params.handleRtd | Function | An optional function that uses Optable data to enrich `reqBidsConfigObj` with the real-time data. If not provided, the module will do a default call to Optable bundle. The function signature is `[async] (reqBidsConfigObj, optableExtraData, mergeFn) => {}` | `null` | Optional | ## Publisher Customized RTD Handler Function diff --git a/modules/optimeraRtdProvider.js b/modules/optimeraRtdProvider.js index 3a46184e9c1..153fbea2980 100644 --- a/modules/optimeraRtdProvider.js +++ b/modules/optimeraRtdProvider.js @@ -56,9 +56,6 @@ export let transmitWithBidRequests = 'allow'; /** @type {Object} */ export let optimeraTargeting = {}; -/** @type {boolean} */ -export let fetchScoreFile = true; - /** @type {RtdSubmodule} */ export const optimeraSubmodule = { name: 'optimeraRTD', @@ -84,7 +81,6 @@ export function init(moduleConfig) { if (_moduleParams.transmitWithBidRequests) { transmitWithBidRequests = _moduleParams.transmitWithBidRequests; } - setScoresURL(); return true; } logError('Optimera clientID is missing in the Optimera RTD configuration.'); @@ -111,9 +107,9 @@ export function setScoresURL() { if (scoresURL !== newScoresURL) { scoresURL = newScoresURL; - fetchScoreFile = true; + return true; } else { - fetchScoreFile = false; + return false; } } @@ -125,6 +121,12 @@ export function setScoresURL() { * @param {object} userConsent */ export function fetchScores(reqBidsConfigObj, callback, config, userConsent) { + // If setScoresURL returns false, no need to re-fetch the score file + if (!setScoresURL()) { + callback(); + return; + } + // Else, fetch the score file const ajax = ajaxBuilder(); ajax(scoresURL, { success: (res, req) => { diff --git a/modules/permutiveIdentityManagerIdSystem.js b/modules/permutiveIdentityManagerIdSystem.js index 5dc12d44edb..cbd2a1b0d2b 100644 --- a/modules/permutiveIdentityManagerIdSystem.js +++ b/modules/permutiveIdentityManagerIdSystem.js @@ -1,7 +1,9 @@ import {MODULE_TYPE_UID} from '../src/activities/modules.js' import {submodule} from '../src/hook.js' import {getStorageManager} from '../src/storageManager.js' -import {prefixLog, safeJSONParse} from '../src/utils.js' +import {deepAccess, prefixLog, safeJSONParse} from '../src/utils.js' +import {hasPurposeConsent} from '../libraries/permutiveUtils/index.js' +import {VENDORLESS_GVLID} from "../src/consentHandler.js"; /** * @typedef {import('../modules/userId/index.js').Submodule} Submodule * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig @@ -15,8 +17,9 @@ const PERMUTIVE_ID_DATA_STORAGE_KEY = 'permutive-prebid-id' const ID5_DOMAIN = 'id5-sync.com' const LIVERAMP_DOMAIN = 'liveramp.com' const UID_DOMAIN = 'uidapi.com' +const GOOGLE_DOMAIN = 'google.com' -const PRIMARY_IDS = ['id5id', 'idl_env', 'uid2'] +const PRIMARY_IDS = ['id5id', 'idl_env', 'uid2', 'pairId'] export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}) @@ -80,6 +83,9 @@ export const permutiveIdentityManagerIdSubmodule = { * @type {string} */ name: MODULE_NAME, + gvlid: VENDORLESS_GVLID, + + disclosureURL: "https://assets.permutive.app/tcf/tcf.json", /** * decode the stored id value for passing to bid requests @@ -89,7 +95,19 @@ export const permutiveIdentityManagerIdSubmodule = { * @returns {(Object|undefined)} */ decode(value, config) { - return value + const storedPairId = value['pairId'] + let pairId + try { + if (storedPairId !== undefined) { + const decoded = safeJSONParse(atob(storedPairId)) + if (Array.isArray(decoded)) { + pairId = decoded + } + } + } catch (e) { + logger.logInfo('Error parsing pairId') + } + return pairId === undefined ? value : {...value, pairId} }, /** @@ -101,6 +119,12 @@ export const permutiveIdentityManagerIdSubmodule = { * @returns {IdResponse|undefined} */ getId(submoduleConfig, consentData, cacheIdObj) { + const enforceVendorConsent = deepAccess(submoduleConfig, 'params.enforceVendorConsent') + if (!hasPurposeConsent(consentData, [1], enforceVendorConsent)) { + logger.logInfo('GDPR purpose 1 consent not satisfied for Permutive Identity Manager') + return + } + const id = readFromSdkLocalStorage() if (Object.entries(id).length > 0) { logger.logInfo('found id in sdk storage') @@ -144,6 +168,10 @@ export const permutiveIdentityManagerIdSubmodule = { return data.ext } } + }, + 'pairId': { + source: GOOGLE_DOMAIN, + atype: 571187 } } } diff --git a/modules/permutiveIdentityManagerIdSystem.md b/modules/permutiveIdentityManagerIdSystem.md index ae249803d11..bb90e2c2dac 100644 --- a/modules/permutiveIdentityManagerIdSystem.md +++ b/modules/permutiveIdentityManagerIdSystem.md @@ -55,4 +55,10 @@ instead wait for up to the specified number of milliseconds for Permutive's SDK identities from the SDK directly if/when this happens. This value should be set to a value smaller than the `auctionDelay` set on the `userSync` configuration object, since -there is no point waiting longer than this as the auction will already have been triggered. \ No newline at end of file +there is no point waiting longer than this as the auction will already have been triggered. + +### enforceVendorConsent + +Publishers that require a vendor-based TCF check can set `enforceVendorConsent: true` in the module params. When enabled, +the module will only run when TCF vendor consent for Permutive (vendor 361) and purpose 1 is available. If the flag is +omitted or set to `false`, the module relies on the publisher-level purpose consent instead. \ No newline at end of file diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index bb06d2d138e..972154e3933 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -9,6 +9,8 @@ import {getGlobal} from '../src/prebidGlobal.js'; import {submodule} from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {deepAccess, deepSetValue, isFn, logError, mergeDeep, isPlainObject, safeJSONParse, prefixLog} from '../src/utils.js'; +import {VENDORLESS_GVLID} from '../src/consentHandler.js'; +import {hasPurposeConsent} from '../libraries/permutiveUtils/index.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; @@ -30,7 +32,9 @@ export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleNam function init(moduleConfig, userConsent) { readPermutiveModuleConfigFromCache() - return true + const enforceVendorConsent = deepAccess(moduleConfig, 'params.enforceVendorConsent') + + return hasPurposeConsent(userConsent, [1], enforceVendorConsent) } function liftIntoParams(params) { @@ -86,6 +90,7 @@ export function getModuleConfig(customModuleConfig) { maxSegs: 500, acBidders: [], overwrites: {}, + enforceVendorConsent: false, }, }, permutiveModuleConfig, @@ -466,6 +471,8 @@ let permutiveSDKInRealTime = false /** @type {RtdSubmodule} */ export const permutiveSubmodule = { name: MODULE_NAME, + disclosureURL: "https://assets.permutive.app/tcf/tcf.json", + gvlid: VENDORLESS_GVLID, getBidRequestData: function (reqBidsConfigObj, callback, customModuleConfig) { const completeBidRequestData = () => { logger.logInfo(`Request data updated`) diff --git a/modules/permutiveRtdProvider.md b/modules/permutiveRtdProvider.md index 3cf3ed2b367..8ef632c75f5 100644 --- a/modules/permutiveRtdProvider.md +++ b/modules/permutiveRtdProvider.md @@ -47,37 +47,11 @@ as well as enabling settings for specific use cases mentioned above (e.g. acbidd | params | Object | | - | | params.acBidders | String[] | An array of bidder codes to share cohorts with in certain versions of Prebid, see below | `[]` | | params.maxSegs | Integer | Maximum number of cohorts to be included in either the `permutive` or `p_standard` key-value. | `500` | +| params.enforceVendorConsent | Boolean | When `true`, require TCF vendor consent for Permutive (vendor 361). See note below. | `false` | -#### Context +#### Consent -While Permutive is listed as a TCF vendor (ID: 361), Permutive does not obtain consent directly from the TCF. As we act as a processor on behalf of our publishers consent is given to the Permutive SDK by the publisher, not by the [GDPR Consent Management Module](https://prebid-docs.atre.net/dev-docs/modules/consentManagement.html). - -This means that if GDPR enforcement is configured within the Permutive SDK _and_ the user consent isn’t given for Permutive to fire, no cohorts will populate. - -If you are also using the [TCF Control Module](https://docs.prebid.org/dev-docs/modules/tcfControl.html), in order to prevent Permutive from being blocked, it needs to be labeled within the Vendor Exceptions. - -#### Instructions - -1. Publisher enables rules within Prebid.js configuration. -2. Label Permutive as an exception, as shown below. -```javascript -[ - { - purpose: 'storage', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: ["permutive"] - }, - { - purpose: 'basicAds', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: [] - } -] -``` - -Before making any updates to this configuration, please ensure that this approach aligns with internal policies and current regulations regarding consent. +While Permutive is listed as a TCF vendor (ID: 361), Permutive does not typically obtain vendor consent from the TCF, but instead relies on the publisher purpose consents. Publishers wishing to use TCF vendor consent instead can add 361 to their CMP and set params.enforceVendorConsent to `true`. ## Cohort Activation with Permutive RTD Module diff --git a/modules/pixfutureBidAdapter.js b/modules/pixfutureBidAdapter.js index 145f85956d7..d8cd4877b1d 100644 --- a/modules/pixfutureBidAdapter.js +++ b/modules/pixfutureBidAdapter.js @@ -1,16 +1,16 @@ -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {getStorageManager} from '../src/storageManager.js'; -import {BANNER} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; -import {deepAccess, isArray, isNumber, isPlainObject} from '../src/utils.js'; -import {auctionManager} from '../src/auctionManager.js'; -import {getANKeywordParam} from '../libraries/appnexusUtils/anKeywords.js'; -import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; -import {transformSizes} from '../libraries/sizeUtils/tranformSize.js'; -import {addUserId, hasUserInfo, getBidFloor} from '../libraries/adrelevantisUtils/bidderUtils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; +import { deepAccess, isArray, isNumber, isPlainObject } from '../src/utils.js'; +import { auctionManager } from '../src/auctionManager.js'; +import { getANKeywordParam } from '../libraries/appnexusUtils/anKeywords.js'; +import { convertCamelToUnderscore } from '../libraries/appnexusUtils/anUtils.js'; +import { transformSizes } from '../libraries/sizeUtils/tranformSize.js'; +import { addUserId, hasUserInfo, getBidFloor } from '../libraries/adrelevantisUtils/bidderUtils.js'; const SOURCE = 'pbjs'; -const storageManager = getStorageManager({bidderCode: 'pixfuture'}); +const storageManager = getStorageManager({ bidderCode: 'pixfuture' }); const USER_PARAMS = ['age', 'externalUid', 'segments', 'gender', 'dnt', 'language']; let pixID = ''; const GVLID = 839; @@ -31,7 +31,7 @@ export const spec = { isBidRequestValid(bid) { return !!(bid.sizes && bid.bidId && bid.params && - (bid.params.pix_id && (typeof bid.params.pix_id === 'string'))); + (bid.params.pix_id && (typeof bid.params.pix_id === 'string'))); }, buildRequests(validBidRequests, bidderRequest) { @@ -50,7 +50,7 @@ export const spec = { const userObjBid = ((validBidRequests) || []).find(hasUserInfo); let userObj = {}; if (config.getConfig('coppa') === true) { - userObj = {'coppa': true}; + userObj = { 'coppa': true }; } if (userObjBid) { @@ -62,7 +62,7 @@ export const spec = { const segs = []; userObjBid.params.user[param].forEach(val => { if (isNumber(val)) { - segs.push({'id': val}); + segs.push({ 'id': val }); } else if (isPlainObject(val)) { segs.push(val); } @@ -101,16 +101,17 @@ export const spec = { payload.referrer_detection = refererinfo; } - if (validBidRequests[0].userId) { + if (validBidRequests[0]?.ortb2?.user?.ext?.eids?.length) { const eids = []; + const ortbEids = validBidRequests[0].ortb2.user.ext.eids; - addUserId(eids, deepAccess(validBidRequests[0], `userId.criteoId`), 'criteo.com', null); - addUserId(eids, deepAccess(validBidRequests[0], `userId.unifiedId`), 'thetradedesk.com', null); - addUserId(eids, deepAccess(validBidRequests[0], `userId.id5Id`), 'id5.io', null); - addUserId(eids, deepAccess(validBidRequests[0], `userId.sharedId`), 'thetradedesk.com', null); - addUserId(eids, deepAccess(validBidRequests[0], `userId.identityLink`), 'liveramp.com', null); - addUserId(eids, deepAccess(validBidRequests[0], `userId.liveIntentId`), 'liveintent.com', null); - addUserId(eids, deepAccess(validBidRequests[0], `userId.fabrickId`), 'home.neustar', null); + ortbEids.forEach(eid => { + const source = eid.source; + const uids = eid.uids || []; + uids.forEach(uidObj => { + addUserId(eids, uidObj.id, source, uidObj.atype || null); + }); + }); if (eids.length) { payload.eids = eids; @@ -124,7 +125,7 @@ export const spec = { const ret = { url: `${hostname}/pixservices`, method: 'POST', - options: {withCredentials: true}, + options: { withCredentials: true }, data: { v: 'v' + '$prebid.version$', pageUrl: referer, @@ -245,7 +246,7 @@ function bidToTag(bid) { tag.reserve = bidFloor; } if (bid.params.position) { - tag.position = {'above': 1, 'below': 2}[bid.params.position] || 0; + tag.position = { 'above': 1, 'below': 2 }[bid.params.position] || 0; } else { const mediaTypePos = deepAccess(bid, `mediaTypes.banner.pos`) || deepAccess(bid, `mediaTypes.video.pos`); // only support unknown, atf, and btf values for position at this time @@ -283,7 +284,7 @@ function bidToTag(bid) { } if (bid.renderer) { - tag.video = Object.assign({}, tag.video, {custom_renderer_present: true}); + tag.video = Object.assign({}, tag.video, { custom_renderer_present: true }); } if (bid.params.frameworks && isArray(bid.params.frameworks)) { diff --git a/modules/prebidServerBidAdapter/ortbConverter.js b/modules/prebidServerBidAdapter/ortbConverter.js index 7bf1602e5e7..ab9e18e6837 100644 --- a/modules/prebidServerBidAdapter/ortbConverter.js +++ b/modules/prebidServerBidAdapter/ortbConverter.js @@ -27,10 +27,10 @@ const BIDDER_SPECIFIC_REQUEST_PROPS = new Set(['bidderCode', 'bidderRequestId', const getMinimumFloor = (() => { const getMin = minimum(currencyCompare(floor => [floor.bidfloor, floor.bidfloorcur])); return function(candidates) { - let min; + let min = null; for (const candidate of candidates) { if (candidate?.bidfloorcur == null || candidate?.bidfloor == null) return null; - min = min == null ? candidate : getMin(min, candidate); + min = min === null ? candidate : getMin(min, candidate); } return min; } @@ -133,7 +133,7 @@ const PBS_CONVERTER = ortbConverter({ // also, take overrides from s2sConfig.adapterOptions const adapterOptions = context.s2sBidRequest.s2sConfig.adapterOptions; for (const req of context.actualBidRequests.values()) { - setImpBidParams(imp, req, context, context); + setImpBidParams(imp, req); if (adapterOptions && adapterOptions[req.bidder]) { Object.assign(imp.ext.prebid.bidder[req.bidder], adapterOptions[req.bidder]); } @@ -237,6 +237,10 @@ const PBS_CONVERTER = ortbConverter({ extPrebidAliases(orig, ortbRequest, proxyBidderRequest, context) { // override alias processing to do it for each bidder in the request context.actualBidderRequests.forEach(req => orig(ortbRequest, req, context)); + }, + extPrebidPageViewIds(orig, ortbRequest, proxyBidderRequest, context) { + // override page view ID processing to do it for each bidder in the request + context.actualBidderRequests.forEach(req => orig(ortbRequest, req, context)); } }, [RESPONSE]: { diff --git a/modules/publicGoodBidAdapter.js b/modules/publicGoodBidAdapter.js new file mode 100644 index 00000000000..b5fa56d7a53 --- /dev/null +++ b/modules/publicGoodBidAdapter.js @@ -0,0 +1,83 @@ +'use strict'; + +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'publicgood'; +const PUBLIC_GOOD_ENDPOINT = 'https://advice.pgs.io'; +var PGSAdServed = false; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, NATIVE], + + isBidRequestValid: function (bid) { + if (PGSAdServed || !bid.params.partnerId || !bid.params.slotId) { + return false; + } + + return true; + }, + + buildRequests: function (validBidRequests, bidderRequest) { + let partnerId = ""; + let slotId = ""; + + if (validBidRequests[0] && validBidRequests[0].params) { + partnerId = validBidRequests[0].params.partnerId; + slotId = validBidRequests[0].params.slotId; + } + + let payload = { + url: bidderRequest.refererInfo.page || bidderRequest.refererInfo.referer, + partner_id: partnerId, + isprebid: true, + slotid: slotId, + bidRequest: validBidRequests[0] + } + + return { + method: 'POST', + url: PUBLIC_GOOD_ENDPOINT, + data: payload, + options: { + withCredentials: false, + }, + bidId: validBidRequests[0].bidId + } + }, + + interpretResponse: function (serverResponse, bidRequest) { + const serverBody = serverResponse.body; + let bidResponses = []; + let bidResponse = {}; + let partnerId = serverBody && serverBody.targetData ? serverBody.targetData.partner_id : "error"; + + if (!serverBody || typeof serverBody !== 'object') { + return []; + } + + if (serverBody.action !== 'Hide' && !PGSAdServed) { + bidResponse.requestId = bidRequest.bidId; + bidResponse.creativeId = serverBody.targetData.target_id; + bidResponse.cpm = serverBody.targetData.cpm; + bidResponse.width = 320; + bidResponse.height = 470; + bidResponse.ad = `
`; + bidResponse.currency = 'USD'; + bidResponse.netRevenue = true; + bidResponse.ttl = 360; + bidResponse.meta = {advertiserDomains: []}; + bidResponses.push(bidResponse); + } + + return bidResponses; + }, + + onBidWon: function(bid) { + // Win once per page load + PGSAdServed = true; + } + +}; +registerBidder(spec); diff --git a/modules/publicGoodBigAdapter.md b/modules/publicGoodBigAdapter.md new file mode 100644 index 00000000000..09fb0879edf --- /dev/null +++ b/modules/publicGoodBigAdapter.md @@ -0,0 +1,33 @@ +# Overview + +**Module Name**: Public Good Bidder Adapter\ +**Module Type**: Bidder Adapter\ +**Maintainer**: publicgood@publicgood.com + +# Description + +Public Good's bid adapter is for use with approved publishers only. Any publisher who wishes to integrate with Pubic Good using the this adapter will need a partner ID. + +Please contact Public Good for additional information and a negotiated set of slots. + +# Test Parameters +``` +{ + bidder: 'publicgood', + params: { + partnerId: 'prebid-test', + slotId: 'test' + } +} +``` + +# Publisher Parameters +``` +{ + bidder: 'publicgood', + params: { + partnerId: '-- partner ID provided by public good --', + slotId: 'all | -- optional slot identifier --' + } +} +``` \ No newline at end of file diff --git a/modules/pubmaticAnalyticsAdapter.js b/modules/pubmaticAnalyticsAdapter.js index 38e27e8b7b9..19b1c11ffe9 100755 --- a/modules/pubmaticAnalyticsAdapter.js +++ b/modules/pubmaticAnalyticsAdapter.js @@ -1,4 +1,4 @@ -import { isArray, logError, logWarn, pick } from '../src/utils.js'; +import { isArray, logError, logWarn, pick, isFn } from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import { BID_STATUS, STATUS, REJECTION_REASON } from '../src/constants.js'; @@ -233,6 +233,7 @@ function getFeatureLevelDetails(auctionCache) { function getListOfIdentityPartners() { const namespace = getGlobal(); + if (!isFn(namespace.getUserIds)) return; const publisherProvidedEids = namespace.getConfig("ortb2.user.eids") || []; const availableUserIds = namespace.getUserIds() || {}; const identityModules = namespace.getConfig('userSync')?.userIds || []; diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 7f2abeb5b6a..f23665670e9 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -8,6 +8,7 @@ import { bidderSettings } from '../src/bidderSettings.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { NATIVE_ASSET_TYPES, NATIVE_IMAGE_TYPES, PREBID_NATIVE_DATA_KEYS_TO_ORTB, NATIVE_KEYS_THAT_ARE_NOT_ASSETS, NATIVE_KEYS } from '../src/constants.js'; import { addDealCustomTargetings, addPMPDeals } from '../libraries/dealUtils/dealUtils.js'; +import { getConnectionType } from '../libraries/connectionInfo/connectionUtils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -98,6 +99,9 @@ const converter = ortbConverter({ if (pmzoneid) imp.ext.pmZoneId = pmzoneid; setImpTagId(imp, adSlot.trim(), hashedKey); setImpFields(imp); + imp.ext?.ae != null && delete imp.ext.ae; + imp.ext?.igs != null && delete imp.ext.igs; + imp.ext?.paapi != null && delete imp.ext.paapi; // check for battr data types ['banner', 'video', 'native'].forEach(key => { if (imp[key]?.battr && !Array.isArray(imp[key].battr)) { @@ -563,12 +567,6 @@ const validateBlockedCategories = (bcats) => { return [...new Set(bcats.filter(item => typeof item === 'string' && item.length >= 3))]; } -const getConnectionType = () => { - const connection = window.navigator && (window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection); - const types = { ethernet: 1, wifi: 2, 'slow-2g': 4, '2g': 4, '3g': 5, '4g': 6 }; - return types[connection?.effectiveType] || 0; -} - /** * Optimizes the impressions array by consolidating impressions for the same ad unit and media type * @param {Array} imps - Array of impression objects @@ -856,16 +854,6 @@ export const spec = { */ interpretResponse: (response, request) => { const { bids } = converter.fromORTB({ response: response.body, request: request.data }); - const fledgeAuctionConfigs = deepAccess(response.body, 'ext.fledge_auction_configs'); - if (fledgeAuctionConfigs) { - return { - bids, - paapi: Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => ({ - bidId, - config: { auctionSignals: {}, ...cfg } - })) - }; - } return bids; }, diff --git a/modules/pubmaticRtdProvider.js b/modules/pubmaticRtdProvider.js index b373ac0a545..6dbb3a28aac 100644 --- a/modules/pubmaticRtdProvider.js +++ b/modules/pubmaticRtdProvider.js @@ -1,488 +1,103 @@ import { submodule } from '../src/hook.js'; -import { logError, logInfo, isPlainObject, isEmpty, isFn, mergeDeep } from '../src/utils.js'; -import { config as conf } from '../src/config.js'; -import { getDeviceType as fetchDeviceType, getOS } from '../libraries/userAgentUtils/index.js'; -import { getLowEntropySUA } from '../src/fpd/sua.js'; -import { getGlobal } from '../src/prebidGlobal.js'; -import { REJECTION_REASON } from '../src/constants.js'; +import { logError, mergeDeep, isPlainObject, isEmpty } from '../src/utils.js'; + +import { PluginManager } from '../libraries/pubmaticUtils/plugins/pluginManager.js'; +import { FloorProvider } from '../libraries/pubmaticUtils/plugins/floorProvider.js'; +import { UnifiedPricingRule } from '../libraries/pubmaticUtils/plugins/unifiedPricingRule.js'; +import { DynamicTimeout } from '../libraries/pubmaticUtils/plugins/dynamicTimeout.js'; /** - * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + * @typedef {import('./rtdModule/index.js').RtdSubmodule} RtdSubmodule */ - /** * This RTD module has a dependency on the priceFloors module. * We utilize the continueAuction function from the priceFloors module to incorporate price floors data into the current auction. */ -import { continueAuction } from './priceFloors.js'; // eslint-disable-line prebid/validate-imports export const CONSTANTS = Object.freeze({ SUBMODULE_NAME: 'pubmatic', REAL_TIME_MODULE: 'realTimeData', LOG_PRE_FIX: 'PubMatic-Rtd-Provider: ', - UTM: 'utm_', - UTM_VALUES: { - TRUE: '1', - FALSE: '0' - }, - TIME_OF_DAY_VALUES: { - MORNING: 'morning', - AFTERNOON: 'afternoon', - EVENING: 'evening', - NIGHT: 'night', - }, ENDPOINTS: { BASEURL: 'https://ads.pubmatic.com/AdServer/js/pwt', - FLOORS: 'floors.json', CONFIGS: 'config.json' - }, - BID_STATUS: { - NOBID: 0, - WON: 1, - FLOORED: 2 - }, - MULTIPLIERS: { - WIN: 1.0, - FLOORED: 0.8, - NOBID: 1.2 - }, - TARGETING_KEYS: { - PM_YM_FLRS: 'pm_ym_flrs', // Whether RTD floor was applied - PM_YM_FLRV: 'pm_ym_flrv', // Final floor value (after applying multiplier) - PM_YM_BID_S: 'pm_ym_bid_s' // Bid status (0: No bid, 1: Won, 2: Floored) } }); -const BROWSER_REGEX_MAP = [ - { regex: /\b(?:crios)\/([\w.]+)/i, id: 1 }, // Chrome for iOS - { regex: /(edg|edge)(?:e|ios|a)?(?:\/([\w.]+))?/i, id: 2 }, // Edge - { regex: /(opera|opr)(?:.+version\/|[/ ]+)([\w.]+)/i, id: 3 }, // Opera - { regex: /(?:ms|\()(ie) ([\w.]+)|(?:trident\/[\w.]+)/i, id: 4 }, // Internet Explorer - { regex: /fxios\/([-\w.]+)/i, id: 5 }, // Firefox for iOS - { regex: /((?:fban\/fbios|fb_iab\/fb4a)(?!.+fbav)|;fbav\/([\w.]+);)/i, id: 6 }, // Facebook In-App Browser - { regex: / wv\).+(chrome)\/([\w.]+)/i, id: 7 }, // Chrome WebView - { regex: /droid.+ version\/([\w.]+)\b.+(?:mobile safari|safari)/i, id: 8 }, // Android Browser - { regex: /(chrome|crios)(?:\/v?([\w.]+))?\b/i, id: 9 }, // Chrome - { regex: /version\/([\w.,]+) .*mobile\/\w+ (safari)/i, id: 10 }, // Safari Mobile - { regex: /version\/([\w(.|,]+) .*(mobile ?safari|safari)/i, id: 11 }, // Safari - { regex: /(firefox)\/([\w.]+)/i, id: 12 } // Firefox -]; - -export const defaultValueTemplate = { - currency: 'USD', - skipRate: 0, - schema: { - fields: ['mediaType', 'size'] - } -}; - -let initTime; -let _fetchFloorRulesPromise = null; let _fetchConfigPromise = null; -export let configMerged; -// configMerged is a reference to the function that can resolve configMergedPromise whenever we want -let configMergedPromise = new Promise((resolve) => { configMerged = resolve; }); -export let _country; -// Store multipliers from floors.json, will use default values from CONSTANTS if not available -export let _multipliers = null; - -// Use a private variable for profile configs -let _profileConfigs; -// Export getter and setter functions for _profileConfigs -export const getProfileConfigs = () => _profileConfigs; -export const setProfileConfigs = (configs) => { _profileConfigs = configs; }; - -// Waits for a given promise to resolve within a timeout -export function withTimeout(promise, ms) { - let timeout; - const timeoutPromise = new Promise((resolve) => { - timeout = setTimeout(() => resolve(undefined), ms); - }); - - return Promise.race([promise.finally(() => clearTimeout(timeout)), timeoutPromise]); -} - -// Utility Functions -export const getCurrentTimeOfDay = () => { - const currentHour = new Date().getHours(); - - return currentHour < 5 ? CONSTANTS.TIME_OF_DAY_VALUES.NIGHT - : currentHour < 12 ? CONSTANTS.TIME_OF_DAY_VALUES.MORNING - : currentHour < 17 ? CONSTANTS.TIME_OF_DAY_VALUES.AFTERNOON - : currentHour < 19 ? CONSTANTS.TIME_OF_DAY_VALUES.EVENING - : CONSTANTS.TIME_OF_DAY_VALUES.NIGHT; -} - -export const getBrowserType = () => { - const brandName = getLowEntropySUA()?.browsers - ?.map(b => b.brand.toLowerCase()) - .join(' ') || ''; - const browserMatch = brandName ? BROWSER_REGEX_MAP.find(({ regex }) => regex.test(brandName)) : -1; - - if (browserMatch?.id) return browserMatch.id.toString(); - - const userAgent = navigator?.userAgent; - let browserIndex = userAgent == null ? -1 : 0; - - if (userAgent) { - browserIndex = BROWSER_REGEX_MAP.find(({ regex }) => regex.test(userAgent))?.id || 0; - } - return browserIndex.toString(); -} - -// Find all bids for a specific ad unit -function findBidsForAdUnit(auction, code) { - return auction?.bidsReceived?.filter(bid => bid.adUnitCode === code) || []; -} +let _ymConfigPromise; +export const getYmConfigPromise = () => _ymConfigPromise; +export const setYmConfigPromise = (promise) => { _ymConfigPromise = promise; }; -// Find rejected bids for a specific ad unit -function findRejectedBidsForAdUnit(auction, code) { - if (!auction?.bidsRejected) return []; +export function ConfigJsonManager() { + let _ymConfig = {}; + const getYMConfig = () => _ymConfig; + const setYMConfig = (config) => { _ymConfig = config; } + let country; - // If bidsRejected is an array - if (Array.isArray(auction.bidsRejected)) { - return auction.bidsRejected.filter(bid => bid.adUnitCode === code); - } + /** + * Fetch configuration from the server + * @param {string} publisherId - Publisher ID + * @param {string} profileId - Profile ID + * @returns {Promise} - Promise resolving to the config object + */ + async function fetchConfig(publisherId, profileId) { + try { + const url = `${CONSTANTS.ENDPOINTS.BASEURL}/${publisherId}/${profileId}/${CONSTANTS.ENDPOINTS.CONFIGS}`; + const response = await fetch(url); + + if (!response.ok) { + logError(`${CONSTANTS.LOG_PRE_FIX} Error while fetching config: Not ok`); + return null; + } - // If bidsRejected is an object mapping bidders to their rejected bids - if (typeof auction.bidsRejected === 'object') { - return Object.values(auction.bidsRejected) - .filter(Array.isArray) - .flatMap(bidderBids => bidderBids.filter(bid => bid.adUnitCode === code)); - } + // Extract country code if available + const cc = response.headers?.get('country_code'); + country = cc ? cc.split(',')?.map(code => code.trim())[0] : undefined; - return []; -} + // Parse the JSON response + const ymConfigs = await response.json(); -// Find a rejected bid due to price floor -function findRejectedFloorBid(rejectedBids) { - return rejectedBids.find(bid => { - return bid.rejectionReason === REJECTION_REASON.FLOOR_NOT_MET && - (bid.floorData?.floorValue && bid.cpm < bid.floorData.floorValue); - }); -} + if (!isPlainObject(ymConfigs) || isEmpty(ymConfigs)) { + logError(`${CONSTANTS.LOG_PRE_FIX} profileConfigs is not an object or is empty`); + return null; + } -// Find the winning or highest bid for an ad unit -function findWinningBid(adUnitCode) { - try { - const pbjs = getGlobal(); - if (!pbjs?.getHighestCpmBids) return null; + // Store the configuration + setYMConfig(ymConfigs); - const highestCpmBids = pbjs.getHighestCpmBids(adUnitCode); - if (!highestCpmBids?.length) { - logInfo(CONSTANTS.LOG_PRE_FIX, `No highest CPM bids found for ad unit: ${adUnitCode}`); + return true; + } catch (error) { + logError(`${CONSTANTS.LOG_PRE_FIX} Error while fetching config: ${error}`); return null; } - - const highestCpmBid = highestCpmBids[0]; - logInfo(CONSTANTS.LOG_PRE_FIX, `Found highest CPM bid using pbjs.getHighestCpmBids() for ad unit: ${adUnitCode}, CPM: ${highestCpmBid.cpm}`); - return highestCpmBid; - } catch (error) { - logError(CONSTANTS.LOG_PRE_FIX, `Error finding highest CPM bid: ${error}`); - return null; - } -} - -// Find floor value from bidder requests -function findFloorValueFromBidderRequests(auction, code) { - if (!auction?.bidderRequests?.length) return 0; - - // Find all bids in bidder requests for this ad unit - const bidsFromRequests = auction.bidderRequests - .flatMap(request => request.bids || []) - .filter(bid => bid.adUnitCode === code); - - if (!bidsFromRequests.length) { - logInfo(CONSTANTS.LOG_PRE_FIX, `No bids found for ad unit: ${code}`); - return 0; } - const bidWithGetFloor = bidsFromRequests.find(bid => bid.getFloor); - if (!bidWithGetFloor) { - logInfo(CONSTANTS.LOG_PRE_FIX, `No bid with getFloor method found for ad unit: ${code}`); - return 0; - } - - // Helper function to extract sizes with their media types from a source object - const extractSizes = (source) => { - if (!source) return null; - - const result = []; - - // Extract banner sizes - if (source.mediaTypes?.banner?.sizes) { - source.mediaTypes.banner.sizes.forEach(size => { - result.push({ - size, - mediaType: 'banner' - }); - }); - } - - // Extract video sizes - if (source.mediaTypes?.video?.playerSize) { - const playerSize = source.mediaTypes.video.playerSize; - // Handle both formats: [[w, h]] and [w, h] - const videoSizes = Array.isArray(playerSize[0]) ? playerSize : [playerSize]; - - videoSizes.forEach(size => { - result.push({ - size, - mediaType: 'video' - }); - }); - } - - // Use general sizes as fallback if no specific media types found - if (result.length === 0 && source.sizes) { - source.sizes.forEach(size => { - result.push({ - size, - mediaType: 'banner' // Default to banner for general sizes - }); - }); - } - - return result.length > 0 ? result : null; - }; - - // Try to get sizes from different sources in order of preference - const adUnit = auction.adUnits?.find(unit => unit.code === code); - let sizes = extractSizes(adUnit) || extractSizes(bidWithGetFloor); - - // Handle fallback to wildcard size if no sizes found - if (!sizes) { - sizes = [{ size: ['*', '*'], mediaType: 'banner' }]; - logInfo(CONSTANTS.LOG_PRE_FIX, `No sizes found, using wildcard size for ad unit: ${code}`); - } - - // Try to get floor values for each size - let minFloor = -1; - - for (const sizeObj of sizes) { - // Extract size and mediaType from the object - const { size, mediaType } = sizeObj; - - // Call getFloor with the appropriate media type - const floorInfo = bidWithGetFloor.getFloor({ - currency: 'USD', // Default currency - mediaType: mediaType, // Use the media type we extracted - size: size - }); - - if (floorInfo?.floor && !isNaN(parseFloat(floorInfo.floor))) { - const floorValue = parseFloat(floorInfo.floor); - logInfo(CONSTANTS.LOG_PRE_FIX, `Floor value for ${mediaType} size ${size}: ${floorValue}`); - - // Update minimum floor value - minFloor = minFloor === -1 ? floorValue : Math.min(minFloor, floorValue); - } - } - - if (minFloor !== -1) { - logInfo(CONSTANTS.LOG_PRE_FIX, `Calculated minimum floor value ${minFloor} for ad unit: ${code}`); - return minFloor; - } - - logInfo(CONSTANTS.LOG_PRE_FIX, `No floor data found for ad unit: ${code}`); - return 0; -} - -// Select multiplier based on priority order: floors.json → config.json → default -function selectMultiplier(multiplierKey, profileConfigs) { - // Define sources in priority order - const multiplierSources = [ - { - name: 'config.json', - getValue: () => { - const configPath = profileConfigs?.plugins?.dynamicFloors?.pmTargetingKeys?.multiplier; - const lowerKey = multiplierKey.toLowerCase(); - return configPath && lowerKey in configPath ? configPath[lowerKey] : null; - } - }, - { - name: 'floor.json', - getValue: () => _multipliers && multiplierKey in _multipliers ? _multipliers[multiplierKey] : null - }, - { - name: 'default', - getValue: () => CONSTANTS.MULTIPLIERS[multiplierKey] - } - ]; - - // Find the first source with a non-null value - for (const source of multiplierSources) { - const value = source.getValue(); - if (value != null) { - return { value, source: source.name }; - } + /** + * Get configuration by name + * @param {string} name - Plugin name + * @returns {Object} - Plugin configuration + */ + const getConfigByName = (name) => { + return getYMConfig()?.plugins?.[name]; } - // Fallback (shouldn't happen due to default source) - return { value: CONSTANTS.MULTIPLIERS[multiplierKey], source: 'default' }; -} - -// Identify winning bid scenario and return scenario data -function handleWinningBidScenario(winningBid, code) { return { - scenario: 'winning', - bidStatus: CONSTANTS.BID_STATUS.WON, - baseValue: winningBid.cpm, - multiplierKey: 'WIN', - logMessage: `Bid won for ad unit: ${code}, CPM: ${winningBid.cpm}` + fetchConfig, + getYMConfig, + setYMConfig, + getConfigByName, + get country() { return country; } }; } -// Identify rejected floor bid scenario and return scenario data -function handleRejectedFloorBidScenario(rejectedFloorBid, code) { - const baseValue = rejectedFloorBid.floorData?.floorValue || 0; - return { - scenario: 'rejected', - bidStatus: CONSTANTS.BID_STATUS.FLOORED, - baseValue, - multiplierKey: 'FLOORED', - logMessage: `Bid rejected due to price floor for ad unit: ${code}, Floor value: ${baseValue}, Bid CPM: ${rejectedFloorBid.cpm}` - }; -} - -// Identify no bid scenario and return scenario data -function handleNoBidScenario(auction, code) { - const baseValue = findFloorValueFromBidderRequests(auction, code); - return { - scenario: 'nobid', - bidStatus: CONSTANTS.BID_STATUS.NOBID, - baseValue, - multiplierKey: 'NOBID', - logMessage: `No bids for ad unit: ${code}, Floor value: ${baseValue}` - }; -} - -// Determine which scenario applies based on bid conditions -function determineScenario(winningBid, rejectedFloorBid, bidsForAdUnit, auction, code) { - return winningBid ? handleWinningBidScenario(winningBid, code) - : rejectedFloorBid ? handleRejectedFloorBidScenario(rejectedFloorBid, code) - : handleNoBidScenario(auction, code); -} - -// Main function that determines bid status and calculates values -function determineBidStatusAndValues(winningBid, rejectedFloorBid, bidsForAdUnit, auction, code) { - const profileConfigs = getProfileConfigs(); - - // Determine the scenario based on bid conditions - const { bidStatus, baseValue, multiplierKey, logMessage } = - determineScenario(winningBid, rejectedFloorBid, bidsForAdUnit, auction, code); - - // Select the appropriate multiplier - const { value: multiplier, source } = selectMultiplier(multiplierKey, profileConfigs); - logInfo(CONSTANTS.LOG_PRE_FIX, logMessage + ` (Using ${source} multiplier: ${multiplier})`); - - return { bidStatus, baseValue, multiplier }; -} - -// Getter Functions -export const getOs = () => getOS().toString(); -export const getDeviceType = () => fetchDeviceType().toString(); -export const getCountry = () => _country; -export const getBidder = (request) => request?.bidder; -export const getUtm = () => { - const url = new URL(window.location?.href); - const urlParams = new URLSearchParams(url?.search); - return urlParams && urlParams.toString().includes(CONSTANTS.UTM) ? CONSTANTS.UTM_VALUES.TRUE : CONSTANTS.UTM_VALUES.FALSE; -} - -export const getFloorsConfig = (floorsData, profileConfigs) => { - if (!isPlainObject(profileConfigs) || isEmpty(profileConfigs)) { - logError(`${CONSTANTS.LOG_PRE_FIX} profileConfigs is not an object or is empty`); - return undefined; - } - - // Floor configs from adunit / setconfig - const defaultFloorConfig = conf.getConfig('floors') ?? {}; - if (defaultFloorConfig?.endpoint) { - delete defaultFloorConfig.endpoint; - } - // Plugin data from profile - const dynamicFloors = profileConfigs?.plugins?.dynamicFloors; - - // If plugin disabled or config not present, return undefined - if (!dynamicFloors?.enabled || !dynamicFloors?.config) { - return undefined; - } - - const config = { ...dynamicFloors.config }; - - // default values provided by publisher on profile - const defaultValues = config.defaultValues ?? {}; - // If floorsData is not present, use default values - const finalFloorsData = floorsData ?? { ...defaultValueTemplate, values: { ...defaultValues } }; - - delete config.defaultValues; - // If skiprate is provided in configs, overwrite the value in finalFloorsData - (config.skipRate !== undefined) && (finalFloorsData.skipRate = config.skipRate); - - // merge default configs from page, configs - return { - floors: { - ...defaultFloorConfig, - ...config, - data: finalFloorsData, - additionalSchemaFields: { - deviceType: getDeviceType, - timeOfDay: getCurrentTimeOfDay, - browser: getBrowserType, - os: getOs, - utm: getUtm, - country: getCountry, - bidder: getBidder, - }, - }, - }; -}; - -export const fetchData = async (publisherId, profileId, type) => { - try { - const endpoint = CONSTANTS.ENDPOINTS[type]; - const baseURL = (type === 'FLOORS') ? `${CONSTANTS.ENDPOINTS.BASEURL}/floors` : CONSTANTS.ENDPOINTS.BASEURL; - const url = `${baseURL}/${publisherId}/${profileId}/${endpoint}`; - const response = await fetch(url); - - if (!response.ok) { - logError(`${CONSTANTS.LOG_PRE_FIX} Error while fetching ${type}: Not ok`); - return; - } - - if (type === "FLOORS") { - const cc = response.headers?.get('country_code'); - _country = cc ? cc.split(',')?.map(code => code.trim())[0] : undefined; - } - - const data = await response.json(); - - // Extract multipliers from floors.json if available - if (type === "FLOORS" && data && data.multiplier) { - // Map of source keys to destination keys - const multiplierKeys = { - 'win': 'WIN', - 'floored': 'FLOORED', - 'nobid': 'NOBID' - }; +// Create core components +export const pluginManager = PluginManager(); +export const configJsonManager = ConfigJsonManager(); - // Initialize _multipliers and only add keys that exist in data.multiplier - _multipliers = Object.entries(multiplierKeys) - .reduce((acc, [srcKey, destKey]) => { - if (srcKey in data.multiplier) { - acc[destKey] = data.multiplier[srcKey]; - } - return acc; - }, {}); - - logInfo(CONSTANTS.LOG_PRE_FIX, `Using multipliers from floors.json: ${JSON.stringify(_multipliers)}`); - } - - return data; - } catch (error) { - logError(`${CONSTANTS.LOG_PRE_FIX} Error while fetching ${type}: ${error}`); - } -}; +// Register plugins +pluginManager.register('dynamicFloors', FloorProvider); +pluginManager.register('unifiedPricingRule', UnifiedPricingRule); +pluginManager.register('dynamicTimeout', DynamicTimeout); /** * Initialize the Pubmatic RTD Module. @@ -491,7 +106,6 @@ export const fetchData = async (publisherId, profileId, type) => { * @returns {boolean} */ const init = (config, _userConsent) => { - initTime = Date.now(); // Capture the initialization time let { publisherId, profileId } = config?.params || {}; if (!publisherId || !profileId) { @@ -502,30 +116,14 @@ const init = (config, _userConsent) => { publisherId = String(publisherId).trim(); profileId = String(profileId).trim(); - if (!isFn(continueAuction)) { - logError(`${CONSTANTS.LOG_PRE_FIX} continueAuction is not a function. Please ensure to add priceFloors module.`); - return false; - } - - _fetchFloorRulesPromise = fetchData(publisherId, profileId, "FLOORS"); - _fetchConfigPromise = fetchData(publisherId, profileId, "CONFIGS"); - - _fetchConfigPromise.then(async (profileConfigs) => { - const auctionDelay = conf?.getConfig('realTimeData')?.auctionDelay || 300; - const maxWaitTime = 0.8 * auctionDelay; - - const elapsedTime = Date.now() - initTime; - const remainingTime = Math.max(maxWaitTime - elapsedTime, 0); - const floorsData = await withTimeout(_fetchFloorRulesPromise, remainingTime); - - // Store the profile configs globally - setProfileConfigs(profileConfigs); - - const floorsConfig = getFloorsConfig(floorsData, profileConfigs); - floorsConfig && conf?.setConfig(floorsConfig); - configMerged(); - }); - + // Fetch configuration and initialize plugins + _ymConfigPromise = configJsonManager.fetchConfig(publisherId, profileId) + .then(success => { + if (!success) { + return Promise.reject(new Error('Failed to fetch configuration')); + } + return pluginManager.initialize(configJsonManager); + }); return true; }; @@ -534,34 +132,31 @@ const init = (config, _userConsent) => { * @param {function} callback */ const getBidRequestData = (reqBidsConfigObj, callback) => { - configMergedPromise.then(() => { - const hookConfig = { - reqBidsConfigObj, - context: this, - nextFn: () => true, - haveExited: false, - timer: null - }; - continueAuction(hookConfig); - if (_country) { + _ymConfigPromise.then(() => { + pluginManager.executeHook('processBidRequest', reqBidsConfigObj); + // Apply country information if available + const country = configJsonManager.country; + if (country) { const ortb2 = { user: { ext: { - ctr: _country, + ctr: country, } } - } + }; - mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, { - [CONSTANTS.SUBMODULE_NAME]: ortb2 - }); + reqBidsConfigObj.ortb2Fragments.bidder[CONSTANTS.SUBMODULE_NAME] = mergeDeep( + reqBidsConfigObj.ortb2Fragments.bidder[CONSTANTS.SUBMODULE_NAME] || {}, + ortb2 + ); } + callback(); - }).catch((error) => { - logError(CONSTANTS.LOG_PRE_FIX, 'Error in updating floors :', error); + }).catch(error => { + logError(CONSTANTS.LOG_PRE_FIX, error); callback(); }); -} +}; /** * Returns targeting data for ad units @@ -572,62 +167,7 @@ const getBidRequestData = (reqBidsConfigObj, callback) => { * @return {Object} - Targeting data for ad units */ export const getTargetingData = (adUnitCodes, config, userConsent, auction) => { - // Access the profile configs stored globally - const profileConfigs = getProfileConfigs(); - - // Return empty object if profileConfigs is undefined or pmTargetingKeys.enabled is explicitly set to false - if (!profileConfigs || profileConfigs?.plugins?.dynamicFloors?.pmTargetingKeys?.enabled === false) { - logInfo(`${CONSTANTS.LOG_PRE_FIX} pmTargetingKeys is disabled or profileConfigs is undefined`); - return {}; - } - - // Helper to check if RTD floor is applied to a bid - const isRtdFloorApplied = bid => bid.floorData?.floorProvider === "PM" && !bid.floorData.skipped; - - // Check if any bid has RTD floor applied - const hasRtdFloorAppliedBid = - auction?.adUnits?.some(adUnit => adUnit.bids?.some(isRtdFloorApplied)) || - auction?.bidsReceived?.some(isRtdFloorApplied); - - // Only log when RTD floor is applied - if (hasRtdFloorAppliedBid) { - logInfo(CONSTANTS.LOG_PRE_FIX, 'Setting targeting via getTargetingData:'); - } - - // Process each ad unit code - const targeting = {}; - - adUnitCodes.forEach(code => { - targeting[code] = {}; - - // For non-RTD floor applied cases, only set pm_ym_flrs to 0 - if (!hasRtdFloorAppliedBid) { - targeting[code][CONSTANTS.TARGETING_KEYS.PM_YM_FLRS] = 0; - return; - } - - // Find bids and determine status for RTD floor applied cases - const bidsForAdUnit = findBidsForAdUnit(auction, code); - const rejectedBidsForAdUnit = findRejectedBidsForAdUnit(auction, code); - const rejectedFloorBid = findRejectedFloorBid(rejectedBidsForAdUnit); - const winningBid = findWinningBid(code); - - // Determine bid status and values - const { bidStatus, baseValue, multiplier } = determineBidStatusAndValues( - winningBid, - rejectedFloorBid, - bidsForAdUnit, - auction, - code - ); - - // Set all targeting keys - targeting[code][CONSTANTS.TARGETING_KEYS.PM_YM_FLRS] = 1; - targeting[code][CONSTANTS.TARGETING_KEYS.PM_YM_FLRV] = (baseValue * multiplier).toFixed(2); - targeting[code][CONSTANTS.TARGETING_KEYS.PM_YM_BID_S] = bidStatus; - }); - - return targeting; + return pluginManager.executeHook('getTargeting', adUnitCodes, config, userConsent, auction); }; export const pubmaticSubmodule = { diff --git a/modules/revnewBidAdapter.md b/modules/revnewBidAdapter.md new file mode 100644 index 00000000000..fa5e8ab327b --- /dev/null +++ b/modules/revnewBidAdapter.md @@ -0,0 +1,57 @@ +# Overview + +``` +Module Name: Revnew Bid Adapter +Module Type: Bidder Adapter +Maintainer: gabriel@nexx360.io +``` + +# Description + +Connects to Revnew network for bids. + +To use us as a bidder you must have an account and an active "tagId" or "placement" on our platform. + +# Test Parameters + +## Web + +### Display +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'revnew', + params: { + tagId: 'testnexx' + } + }] + }, +]; +``` + +### Video Instream +``` + var videoAdUnit = { + code: 'video1', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids: [{ + bidder: 'revnew', + params: { + placement: 'TEST_PLACEMENT' + } + }] + }; +``` diff --git a/modules/revnewBidAdapter.ts b/modules/revnewBidAdapter.ts new file mode 100644 index 00000000000..ecdcdb5e845 --- /dev/null +++ b/modules/revnewBidAdapter.ts @@ -0,0 +1,97 @@ +import { deepSetValue, generateUUID, logError } from '../src/utils.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {AdapterRequest, BidderSpec, registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' + +import { interpretResponse, enrichImp, enrichRequest, getAmxId, getLocalStorageFunctionGenerator, getUserSyncs } from '../libraries/nexx360Utils/index.js'; +import { BidRequest, ClientBidderRequest } from '../src/adapterManager.js'; +import { ORTBImp, ORTBRequest } from '../src/prebid.public.js'; + +const BIDDER_CODE = 'revnew'; +const REQUEST_URL = 'https://fast.nexx360.io/revnew'; +const PAGE_VIEW_ID = generateUUID(); +const BIDDER_VERSION = '1.0'; +const GVLID = 1468; +const REVNEW_KEY = 'revnew_storage'; + +type RequireAtLeastOne = + Omit & { + [K in Keys]-?: Required> & + Partial>> + }[Keys]; + +type RevnewBidParams = RequireAtLeastOne<{ + tagId?: string; + placement?: string; + customId?: string; +}, "tagId" | "placement">; + +declare module '../src/adUnits' { + interface BidderParams { + [BIDDER_CODE]: RevnewBidParams; + } +} + +export const STORAGE = getStorageManager({ + bidderCode: BIDDER_CODE, +}); + +export const getRevnewLocalStorage = getLocalStorageFunctionGenerator<{ revnewId: string }>( + STORAGE, + BIDDER_CODE, + REVNEW_KEY, + 'revnewId' +); + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 120, + }, + imp(buildImp, bidRequest: BidRequest, context) { + let imp:ORTBImp = buildImp(bidRequest, context); + imp = enrichImp(imp, bidRequest); + deepSetValue(imp, 'ext.revnew', bidRequest.params); + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + let request:ORTBRequest = buildRequest(imps, bidderRequest, context); + const amxId = getAmxId(STORAGE, BIDDER_CODE); + request = enrichRequest(request, amxId, PAGE_VIEW_ID, BIDDER_VERSION); + return request; + }, +}); + +const isBidRequestValid = (bid:BidRequest): boolean => { + if (!bid.params.tagId && !bid.params.placement) { + logError('bid.params.tagId or bid.params.placement must be defined'); + return false; + } + return true; +}; + +const buildRequests = ( + bidRequests: BidRequest[], + bidderRequest: ClientBidderRequest, +): AdapterRequest => { + const data:ORTBRequest = converter.toORTB({bidRequests, bidderRequest}) + const adapterRequest:AdapterRequest = { + method: 'POST', + url: REQUEST_URL, + data, + } + return adapterRequest; +}; + +export const spec:BidderSpec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, +}; + +registerBidder(spec); diff --git a/modules/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index 4d61d827650..cb551bb0b62 100644 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -52,7 +52,7 @@ export const spec = { demand: raiGetDemandType(bid), videoData: raiGetVideoInfo(bid), scr_rsl: raiGetResolution(), - cpuc: (typeof window.navigator !== 'undefined' ? window.navigator.hardwareConcurrency : null), + cpuc: null, kws: bid.params.keywords, schain: bid?.ortb2?.source?.ext?.schain, gpid: raiSetPbAdSlot(bid), diff --git a/modules/rtbhouseBidAdapter.js b/modules/rtbhouseBidAdapter.js index f909f2302f9..9aec645b715 100644 --- a/modules/rtbhouseBidAdapter.js +++ b/modules/rtbhouseBidAdapter.js @@ -143,18 +143,32 @@ registerBidder(spec); /** * @param {object} slot Ad Unit Params by Prebid - * @returns {number} floor by imp type + * @returns {number|null} floor value, or null if not available */ function applyFloor(slot) { - const floors = []; + // If Price Floors module is available, use it if (typeof slot.getFloor === 'function') { - Object.keys(slot.mediaTypes).forEach(type => { - if (SUPPORTED_MEDIA_TYPES.includes(type)) { - floors.push(slot.getFloor({ currency: DEFAULT_CURRENCY_ARR[0], mediaType: type, size: slot.sizes || '*' })?.floor); + try { + const floor = slot.getFloor({ + currency: DEFAULT_CURRENCY_ARR[0], + mediaType: '*', + size: '*' + }); + + if (floor && floor.currency === DEFAULT_CURRENCY_ARR[0] && !isNaN(parseFloat(floor.floor))) { + return floor.floor; } - }); + } catch (e) { + logError('RTB House: Error calling getFloor:', e); + } } - return floors.length > 0 ? Math.max(...floors) : parseFloat(slot.params.bidfloor); + + // Fallback to bidfloor param if available + if (slot.params.bidfloor && !isNaN(parseFloat(slot.params.bidfloor))) { + return parseFloat(slot.params.bidfloor); + } + + return null; } /** diff --git a/modules/rtdModule/index.ts b/modules/rtdModule/index.ts index 73473b6d163..1b3bff0baf3 100644 --- a/modules/rtdModule/index.ts +++ b/modules/rtdModule/index.ts @@ -2,8 +2,8 @@ import {config} from '../../src/config.js'; import {getHook, module} from '../../src/hook.js'; import {logError, logInfo, logWarn, mergeDeep} from '../../src/utils.js'; import * as events from '../../src/events.js'; -import { EVENTS, JSON_MAPPING } from '../../src/constants.js'; -import adapterManager, {gdprDataHandler, uspDataHandler, gppDataHandler} from '../../src/adapterManager.js'; +import {EVENTS, JSON_MAPPING} from '../../src/constants.js'; +import adapterManager, {gdprDataHandler, gppDataHandler, uspDataHandler} from '../../src/adapterManager.js'; import {timedAuctionHook} from '../../src/utils/perfMetrics.js'; import {GDPR_GVLIDS} from '../../src/consentHandler.js'; import {MODULE_TYPE_RTD} from '../../src/activities/modules.js'; @@ -163,10 +163,31 @@ export const setBidRequestsData = timedAuctionHook('rtd', function setBidRequest const timeout = shouldDelayAuction ? _moduleConfig.auctionDelay : 0; waitTimeout = setTimeout(exitHook, timeout); + const fpdKey = 'ortb2Fragments'; relevantSubModules.forEach(sm => { - const fpdGuard = guardOrtb2Fragments(reqBidsConfigObj.ortb2Fragments || {}, activityParams(MODULE_TYPE_RTD, sm.name)); - sm.getBidRequestData({...reqBidsConfigObj, ortb2Fragments: fpdGuard}, onGetBidRequestDataCallback.bind(sm), sm.config, _userConsent, timeout); + const fpdGuard = guardOrtb2Fragments(reqBidsConfigObj[fpdKey] ?? {}, activityParams(MODULE_TYPE_RTD, sm.name)); + // submodules need to be able to modify the request object, but we need + // to protect the FPD portion of it. Use a proxy that passes through everything + // except 'ortb2Fragments'. + const request = new Proxy(reqBidsConfigObj, { + get(target, prop, receiver) { + if (prop === fpdKey) return fpdGuard; + return Reflect.get(target, prop, receiver); + }, + set(target, prop, value, receiver) { + if (prop === fpdKey) { + mergeDeep(fpdGuard, value); + return true; + } + return Reflect.set(target, prop, value, receiver); + }, + deleteProperty(target, prop) { + if (prop === fpdKey) return true; + return Reflect.deleteProperty(target, prop) + } + }) + sm.getBidRequestData(request, onGetBidRequestDataCallback.bind(sm), sm.config, _userConsent, timeout); }); function onGetBidRequestDataCallback() { diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 6543a6a88e1..47c311ceb9a 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -427,7 +427,6 @@ export const spec = { 'x_source.tid', 'l_pb_bid_id', 'p_screen_res', - 'o_ae', 'o_cdep', 'rp_floor', 'rp_secure', @@ -545,15 +544,11 @@ export const spec = { data['ppuid'] = configUserId; } - if (bidRequest?.ortb2Imp?.ext?.ae) { - data['o_ae'] = 1; - } // If the bid request contains a 'mobile' property under 'ortb2.site', add it to 'data' as 'p_site.mobile'. if (typeof bidRequest?.ortb2?.site?.mobile === 'number') { data['p_site.mobile'] = bidRequest.ortb2.site.mobile } - addDesiredSegtaxes(bidderRequest, data); // loop through userIds and add to request if (bidRequest?.ortb2?.user?.ext?.eids) { bidRequest.ortb2.user.ext.eids.forEach(({ source, uids = [], inserter, matcher, mm, ext = {} }) => { @@ -655,7 +650,7 @@ export const spec = { * @param {*} responseObj * @param {BidRequest|Object.} request - if request was SRA the bidRequest argument will be a keyed BidRequest array object, * non-SRA responses return a plain BidRequest object - * @return {{fledgeAuctionConfigs: *, bids: *}} An array of bids which + * @return {*} An array of bids */ interpretResponse: function (responseObj, request) { responseObj = responseObj.body; @@ -760,15 +755,7 @@ export const spec = { return (adB.cpm || 0.0) - (adA.cpm || 0.0); }); - const fledgeAuctionConfigs = responseObj.component_auction_config?.map(config => { - return { config, bidId: config.bidId } - }); - - if (fledgeAuctionConfigs) { - return { bids, paapi: fledgeAuctionConfigs }; - } else { - return bids; - } + return bids; }, getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { if (syncOptions.iframeEnabled) { @@ -1064,27 +1051,6 @@ function applyFPD(bidRequest, mediaType, data) { } } -function addDesiredSegtaxes(bidderRequest, target) { - if (rubiConf.readTopics === false) { - return; - } - const iSegments = [1, 2, 5, 6, 7, 507].concat(rubiConf.sendSiteSegtax?.map(seg => Number(seg)) || []); - const vSegments = [4, 508].concat(rubiConf.sendUserSegtax?.map(seg => Number(seg)) || []); - const userData = bidderRequest.ortb2?.user?.data || []; - const siteData = bidderRequest.ortb2?.site?.content?.data || []; - userData.forEach(iterateOverSegmentData(target, 'v', vSegments)); - siteData.forEach(iterateOverSegmentData(target, 'i', iSegments)); -} - -function iterateOverSegmentData(target, char, segments) { - return (topic) => { - const taxonomy = Number(topic.ext?.segtax); - if (segments.includes(taxonomy)) { - target[`tg_${char}.tax${taxonomy}`] = topic.segment?.map(seg => seg.id).join(','); - } - } -} - /** * @param sizes * @returns {*} diff --git a/modules/screencoreBidAdapter.js b/modules/screencoreBidAdapter.js index ac6f5895751..ccf59b28ce7 100644 --- a/modules/screencoreBidAdapter.js +++ b/modules/screencoreBidAdapter.js @@ -1,16 +1,18 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { getStorageManager } from '../src/storageManager.js'; import { - createBuildRequestsFn, - createInterpretResponseFn, - createUserSyncGetter, isBidRequestValid, -} from '../libraries/vidazooUtils/bidderUtils.js'; + buildRequestsBase, + interpretResponse, + getUserSyncs, + buildPlacementProcessingFunction +} from '../libraries/teqblazeUtils/bidderUtils.js'; +import { getTimeZone } from '../libraries/timezone/timezone.js'; const BIDDER_CODE = 'screencore'; const GVLID = 1473; const BIDDER_VERSION = '1.0.0'; +const SYNC_URL = 'https://cs.screencore.io'; const REGION_SUBDOMAIN_SUFFIX = { EU: 'taqeu', US: 'taqus', @@ -23,8 +25,7 @@ const REGION_SUBDOMAIN_SUFFIX = { */ function getRegionSubdomainSuffix() { try { - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const region = timezone.split('/')[0]; + const region = getTimeZone().split('/')[0]; switch (region) { case 'Asia': @@ -48,32 +49,29 @@ function getRegionSubdomainSuffix() { } } -export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); - export function createDomain() { const subDomain = getRegionSubdomainSuffix(); return `https://${subDomain}.screencore.io`; } -const buildRequests = createBuildRequestsFn(createDomain, null, storage, BIDDER_CODE, BIDDER_VERSION, false); +const AD_URL = `${createDomain()}/prebid`; -const interpretResponse = createInterpretResponseFn(BIDDER_CODE, false); +const placementProcessingFunction = buildPlacementProcessingFunction(); -const getUserSyncs = createUserSyncGetter({ - iframeSyncUrl: 'https://cs.screencore.io/api/sync/iframe', - imageSyncUrl: 'https://cs.screencore.io/api/sync/image', -}); +const buildRequests = (validBidRequests = [], bidderRequest = {}) => { + return buildRequestsBase({ adUrl: AD_URL, validBidRequests, bidderRequest, placementProcessingFunction }); +}; export const spec = { code: BIDDER_CODE, version: BIDDER_VERSION, gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid, + isBidRequestValid: isBidRequestValid(), buildRequests, interpretResponse, - getUserSyncs, + getUserSyncs: getUserSyncs(SYNC_URL), }; registerBidder(spec); diff --git a/modules/screencoreBidAdapter.md b/modules/screencoreBidAdapter.md index 60dc9b9ab21..8e5d9e3d3da 100644 --- a/modules/screencoreBidAdapter.md +++ b/modules/screencoreBidAdapter.md @@ -27,7 +27,8 @@ var adUnits = [ param1: 'loremipsum', param2: 'dolorsitamet' }, - placementId: 'testBanner' + placementId: 'testBanner', + endpointId: 'testEndpoint' } } ] diff --git a/modules/seedtagBidAdapter.js b/modules/seedtagBidAdapter.js index a6dfa0076d7..f93ed814a0a 100644 --- a/modules/seedtagBidAdapter.js +++ b/modules/seedtagBidAdapter.js @@ -4,6 +4,7 @@ import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { _map, getWinDimensions, isArray, triggerPixel } from '../src/utils.js'; import { getViewportCoordinates } from '../libraries/viewport/viewport.js'; +import { getConnectionInfo } from '../libraries/connectionInfo/connectionUtils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -53,12 +54,9 @@ function getBidFloor(bidRequest) { } const getConnectionType = () => { - const connection = - navigator.connection || - navigator.mozConnection || - navigator.webkitConnection || - {}; - switch (connection.type || connection.effectiveType) { + const connection = getConnectionInfo(); + const connectionType = connection?.type || connection?.effectiveType; + switch (connectionType) { case 'wifi': case 'ethernet': return deviceConnection.FIXED; @@ -127,7 +125,7 @@ function buildBidRequest(validBidRequest) { adUnitCode: validBidRequest.adUnitCode, geom: geom(validBidRequest.adUnitCode), placement: params.placement, - requestCount: validBidRequest.bidRequestsCount || 1, + requestCount: validBidRequest.bidderRequestsCount || 1, }; if (hasVideoMediaType(validBidRequest) && hasMandatoryVideoParams(validBidRequest)) { diff --git a/modules/seenthisBrandStories.md b/modules/seenthisBrandStories.md new file mode 100644 index 00000000000..8b438d8ca6c --- /dev/null +++ b/modules/seenthisBrandStories.md @@ -0,0 +1,10 @@ +# Overview + +Module Name: SeenThis Brand Stories +Maintainer: tech@seenthis.se + +# Description + +Module to allow publishers to handle SeenThis Brand Stories ads. The module will handle communication with the ad iframe and resize the ad iframe accurately and handle fullscreen mode according to product specification. + +This will allow publishers to safely run the ad format without the need to disable Safeframe when using Prebid.js. diff --git a/modules/seenthisBrandStories.ts b/modules/seenthisBrandStories.ts new file mode 100644 index 00000000000..8c6ee589272 --- /dev/null +++ b/modules/seenthisBrandStories.ts @@ -0,0 +1,161 @@ +import { EVENTS } from "../src/constants.js"; +import { getBoundingClientRect } from "../libraries/boundingClientRect/boundingClientRect.js"; +import { getWinDimensions } from "../src/utils.js"; +import * as events from "../src/events.js"; + +export const DEFAULT_MARGINS = "16px"; +export const SEENTHIS_EVENTS = [ + "@seenthis_storylines/ready", + "@seenthis_enabled", + "@seenthis_disabled", + "@seenthis_metric", + "@seenthis_detach", + "@seenthis_modal/opened", + "@seenthis_modal/closed", + "@seenthis_modal/beforeopen", + "@seenthis_modal/beforeclose", +]; + +const classNames: Record = { + container: "storylines-container", + expandedBody: "seenthis-storylines-fullscreen", +}; +const containerElements: Record = {}; +const frameElements: Record = {}; +const isInitialized: Record = {}; + +export function calculateMargins(element: HTMLElement) { + const boundingClientRect = getBoundingClientRect(element); + const wrapperLeftMargin = window.getComputedStyle(element).marginLeft; + const marginLeft = boundingClientRect.left - parseInt(wrapperLeftMargin, 10); + + if (boundingClientRect.width === 0 || marginLeft === 0) { + element.style.setProperty("--storylines-margins", DEFAULT_MARGINS); + element.style.setProperty("--storylines-margin-left", DEFAULT_MARGINS); + return; + } + element.style.setProperty("--storylines-margin-left", `-${marginLeft}px`); + element.style.setProperty("--storylines-margins", `${marginLeft * 2}px`); +} + +export function getFrameByEvent(event: MessageEvent) { + const isAncestor = (childWindow: Window, frameWindow: Window) => { + if (frameWindow === childWindow) { + return true; + } else if (childWindow === window.top) { + return false; + } + if (!childWindow?.parent) return false; + return isAncestor(childWindow.parent, frameWindow); + }; + const iframeThatMatchesSource = Array.from( + document.getElementsByTagName("iframe") + ).find((frame) => + isAncestor(event.source as Window, frame.contentWindow as Window) + ); + return iframeThatMatchesSource || null; +} + +export function addStyleToSingleChildAncestors( + element: HTMLElement, + { key, value }: { key: string; value: string } +) { + if (!element || !key) return; + if ( + key in element.style && + "offsetWidth" in element && + element.offsetWidth < getWinDimensions().innerWidth + ) { + element.style.setProperty(key, value); + } + if (!element.parentElement || element.parentElement?.children.length > 1) { + return; + } + addStyleToSingleChildAncestors(element.parentElement, { key, value }); +} + +export function findAdWrapper(target: HTMLDivElement) { + return target?.parentElement?.parentElement; +} + +export function applyFullWidth(target: HTMLDivElement) { + const adWrapper = findAdWrapper(target); + if (adWrapper) { + addStyleToSingleChildAncestors(adWrapper, { key: "width", value: "100%" }); + } +} + +export function applyAutoHeight(target: HTMLDivElement) { + const adWrapper = findAdWrapper(target); + if (adWrapper) { + addStyleToSingleChildAncestors(adWrapper, { key: "height", value: "auto" }); + addStyleToSingleChildAncestors(adWrapper, { + key: "min-height", + value: "auto", + }); + } +} + +// listen to messages from iframes +window.addEventListener("message", (event) => { + if (!["https://video.seenthis.se"].includes(event?.origin)) return; + + const data = event?.data; + if (!data) return; + + switch (data.type) { + case "storylines:init": { + const storyKey = data.storyKey; + if (!storyKey || isInitialized[storyKey]) return; + isInitialized[storyKey] = true; + + frameElements[storyKey] = getFrameByEvent(event); + containerElements[storyKey] = frameElements[storyKey] + ?.parentElement as HTMLDivElement; + event.source?.postMessage( + "storylines:init-ok", + "*" as WindowPostMessageOptions + ); + + const styleEl = document.createElement("style"); + styleEl.textContent = data.css; + document.head.appendChild(styleEl); + if (data.fixes.includes("full-width")) { + applyFullWidth(containerElements[storyKey]); + } + if (data.fixes.includes("auto-height")) { + applyAutoHeight(containerElements[storyKey]); + } + + containerElements[storyKey]?.classList.add(classNames.container); + calculateMargins(containerElements[storyKey]); + + events.emit(EVENTS.BILLABLE_EVENT, { + vendor: "seenthis", + type: "storylines_init", + }); + break; + } + case "@seenthis_modal/beforeopen": { + const storyKey = data.detail.storyKey; + document.body.classList.add(classNames.expandedBody); + containerElements[storyKey]?.classList.add("expanded"); + break; + } + case "@seenthis_modal/beforeclose": { + const storyKey = data.detail.storyKey; + document.body.classList.remove(classNames.expandedBody); + containerElements[storyKey]?.classList.remove("expanded"); + break; + } + } + + // dispatch SEENTHIS_EVENTS to parent window + if (SEENTHIS_EVENTS.includes(data.type)) { + window.dispatchEvent(new CustomEvent(data.type, { detail: data })); + } +}); + +Array.from(window.frames).forEach((frame) => { + frame.postMessage("storylines:bridge-ready", "*"); +}); diff --git a/modules/sevioBidAdapter.js b/modules/sevioBidAdapter.js index a5923ecc6a3..66bfca681d9 100644 --- a/modules/sevioBidAdapter.js +++ b/modules/sevioBidAdapter.js @@ -3,7 +3,11 @@ import { detectWalletsPresence} from "../libraries/cryptoUtils/wallets.js"; import { registerBidder } from "../src/adapters/bidderFactory.js"; import { BANNER, NATIVE } from "../src/mediaTypes.js"; import { config } from "../src/config.js"; +import {getDomComplexity, getPageDescription, getPageTitle} from "../libraries/fpdUtils/pageInfo.js"; import * as converter from '../libraries/ortbConverter/converter.js'; + +const PREBID_VERSION = '$prebid.version$'; +const ADAPTER_VERSION = '1.0.1'; const ORTB = converter.ortbConverter({ context: { ttl: 300 } }); @@ -17,6 +21,28 @@ const detectAdType = (bid) => ["native", "banner"].find((t) => bid.mediaTypes?.[t]) || "unknown" ).toUpperCase(); +const getReferrerInfo = (bidderRequest) => { + return bidderRequest?.refererInfo?.page ?? ''; +} + +const normalizeKeywords = (input) => { + if (!input) return []; + + if (Array.isArray(input)) { + return input.map(k => k.trim()).filter(Boolean); + } + + if (typeof input === 'string') { + return input + .split(',') + .map(k => k.trim()) + .filter(Boolean); + } + + // Any other type → ignore + return []; +}; + const parseNativeAd = function (bid) { try { const nativeAd = JSON.parse(bid.ad); @@ -123,22 +149,67 @@ export const spec = { buildRequests: function (bidRequests, bidderRequest) { const userSyncEnabled = config.getConfig("userSync.syncEnabled"); + const currencyConfig = config.getConfig('currency'); + const currency = + currencyConfig?.adServerCurrency || + currencyConfig?.defaultCurrency || + null; + // (!) that avoids top-level side effects (the thing that can stop registerBidder from running) + const computeTTFB = (w = (typeof window !== 'undefined' ? window : undefined)) => { + try { + const wt = (() => { try { return w?.top ?? w; } catch { return w; } })(); + const p = wt?.performance || wt?.webkitPerformance || wt?.msPerformance || wt?.mozPerformance; + if (!p) return ''; + + if (typeof p.getEntriesByType === 'function') { + const nav = p.getEntriesByType('navigation')?.[0]; + if (nav?.responseStart > 0 && nav?.requestStart > 0) { + return String(Math.round(nav.responseStart - nav.requestStart)); + } + } + + const t = p.timing; + if (t?.responseStart > 0 && t?.requestStart > 0) { + return String(t.responseStart - t.requestStart); + } + + return ''; + } catch { + return ''; + } + }; + + // simple caching + const getTTFBOnce = (() => { + let cached = false; + let done = false; + return () => { + if (done) return cached; + done = true; + cached = computeTTFB(); + return cached; + }; + })(); const ortbRequest = ORTB.toORTB({ bidderRequest, bidRequests }); if (bidRequests.length === 0) { return []; } - const gdpr = bidderRequest.gdprConsent; - const usp = bidderRequest.uspConsent; - const gpp = bidderRequest.gppConsent; + const gdpr = bidderRequest?.gdprConsent; + const usp = bidderRequest?.uspConsent; + const gpp = bidderRequest?.gppConsent; const hasWallet = detectWalletsPresence(); return bidRequests.map((bidRequest) => { const isNative = detectAdType(bidRequest)?.toLowerCase() === 'native'; - const size = bidRequest.mediaTypes?.banner?.sizes[0] || bidRequest.mediaTypes?.native?.sizes[0] || []; - const width = size[0]; - const height = size[1]; + const adSizes = bidRequest.mediaTypes?.banner?.sizes || bidRequest.mediaTypes?.native?.sizes || []; + const formattedSizes = Array.isArray(adSizes) + ? adSizes + .filter(size => Array.isArray(size) && size.length === 2) + .map(([width, height]) => ({ width, height })) + : []; const originalAssets = bidRequest.mediaTypes?.native?.ortb?.assets || []; + // convert icon to img type 1 const processedAssets = originalAssets.map(asset => { if (asset.icon) { @@ -164,19 +235,22 @@ export const spec = { source: eid.source, id: eid.uids?.[0]?.id })).filter(eid => eid.source && eid.id), + ...(currency ? { currency } : {}), ads: [ { - maxSize: { - width: width, - height: height, - }, + sizes: formattedSizes, referenceId: bidRequest.params.referenceId, tagId: bidRequest.params.zone, type: detectAdType(bidRequest), ...(isNative && { nativeRequest: { ver: "1.2", assets: processedAssets || {}} }) }, ], - keywords: { tokens: ortbRequest?.site?.keywords || bidRequest.params?.keywords || [] }, + keywords: { + tokens: normalizeKeywords( + ortbRequest?.site?.keywords || + bidRequest.params?.keywords + ) + }, privacy: { gpp: gpp?.consentString || "", tcfeu: gdpr?.consentString || "", @@ -186,6 +260,28 @@ export const spec = { wdb: hasWallet, externalRef: bidRequest.bidId, userSyncOption: userSyncEnabled === false ? "OFF" : "BIDDERS", + referer: getReferrerInfo(bidderRequest), + pageReferer: document.referrer, + context: [{ + source: "title", + text: getPageTitle().slice(0, 300) + }, { + source: "meta:description", + text: getPageDescription().slice(0, 300) + }], + domComplexity: getDomComplexity(document), + device: bidderRequest?.ortb2?.device || {}, + deviceWidth: screen.width, + deviceHeight: screen.height, + timeout: bidderRequest?.timeout, + viewportHeight: utils.getWinDimensions().visualViewport.height, + viewportWidth: utils.getWinDimensions().visualViewport.width, + timeToFirstByte: getTTFBOnce(), + ext: { + ...(bidderRequest?.ortb2?.ext || {}), + adapter_version: ADAPTER_VERSION, + prebid_version: PREBID_VERSION + } }; const wrapperOn = diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index e9f653595b7..a11820b5897 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -1,11 +1,11 @@ -import {getDNT} from '../libraries/dnt/index.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; -import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { getDNT } from '../libraries/dnt/index.js'; import { handleCookieSync, PID_STORAGE_NAME, prepareSplitImps } from '../libraries/equativUtils/equativUtils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { deepAccess, generateUUID, inIframe, isPlainObject, logWarn, mergeDeep } from '../src/utils.js'; +import { config } from '../src/config.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; +import { deepAccess, generateUUID, inIframe, isPlainObject, logWarn, mergeDeep } from '../src/utils.js'; const VERSION = '4.3.0'; const BIDDER_CODE = 'sharethrough'; @@ -45,8 +45,8 @@ export const sharethroughInternal = { export const converter = ortbConverter({ context: { netRevenue: true, - ttl: 360 - } + ttl: 360, + }, }); export const sharethroughAdapterSpec = { @@ -99,7 +99,7 @@ export const sharethroughAdapterSpec = { test: 0, }; - req.user = firstPartyData.user ?? {} + req.user = firstPartyData.user ?? {}; if (!req.user.ext) req.user.ext = {}; req.user.ext.eids = bidRequests[0].userIdAsEids || []; @@ -108,7 +108,7 @@ export const sharethroughAdapterSpec = { eqtvNetworkId = bidRequests[0].params.equativNetworkId; req.site.publisher = { id: bidRequests[0].params.equativNetworkId, - ...req.site.publisher + ...req.site.publisher, }; const pid = storage.getDataFromLocalStorage(PID_STORAGE_NAME); if (pid) { @@ -116,10 +116,6 @@ export const sharethroughAdapterSpec = { } } - if (bidderRequest.ortb2?.device?.ext?.cdep) { - req.device.ext['cdep'] = bidderRequest.ortb2.device.ext.cdep; - } - // if present, merge device object from ortb2 into `req.device` if (bidderRequest?.ortb2?.device) { mergeDeep(req.device, bidderRequest.ortb2.device); @@ -190,7 +186,9 @@ export const sharethroughAdapterSpec = { if (propIsTypeArray) { const notAssignable = (!Array.isArray(vidReq[prop]) || vidReq[prop].length === 0) && vidReq[prop]; if (notAssignable) { - logWarn(`${IDENTIFIER_PREFIX} Invalid video request property: "${prop}" must be an array with at least 1 entry. Value supplied: "${vidReq[prop]}". This will not be added to the bid request.`); + logWarn( + `${IDENTIFIER_PREFIX} Invalid video request property: "${prop}" must be an array with at least 1 entry. Value supplied: "${vidReq[prop]}". This will not be added to the bid request.` + ); return; } } @@ -207,24 +205,39 @@ export const sharethroughAdapterSpec = { }; const propertiesToConsider = [ - 'api', 'battr', 'companiontype', 'delivery', 'linearity', 'maxduration', 'mimes', 'minduration', 'placement', 'playbackmethod', 'plcmt', 'protocols', 'skip', 'skipafter', 'skipmin', 'startdelay' + 'api', + 'battr', + 'companiontype', + 'delivery', + 'linearity', + 'maxduration', + 'mimes', + 'minduration', + 'placement', + 'playbackmethod', + 'plcmt', + 'protocols', + 'skip', + 'skipafter', + 'skipmin', + 'startdelay', ]; if (!isEqtvTest) { propertiesToConsider.push('companionad'); } - propertiesToConsider.forEach(propertyToConsider => { + propertiesToConsider.forEach((propertyToConsider) => { applyVideoProperty(propertyToConsider, videoRequest, impression); }); } else if (isEqtvTest && nativeRequest) { const nativeImp = converter.toORTB({ bidRequests: [bidReq], - bidderRequest + bidderRequest, }); impression.native = { - ...nativeImp.imp[0].native + ...nativeImp.imp[0].native, }; } else { impression.banner = { @@ -232,7 +245,8 @@ export const sharethroughAdapterSpec = { topframe: inIframe() ? 0 : 1, format: bidReq.sizes.map((size) => ({ w: +size[0], h: +size[1] })), }; - const battr = deepAccess(bidReq, 'mediaTypes.banner.battr', null) || deepAccess(bidReq, 'ortb2Imp.banner.battr'); + const battr = + deepAccess(bidReq, 'mediaTypes.banner.battr', null) || deepAccess(bidReq, 'ortb2Imp.banner.battr'); if (battr) impression.banner.battr = battr; } @@ -248,7 +262,7 @@ export const sharethroughAdapterSpec = { }) .filter((imp) => !!imp); - let splitImps = [] + let splitImps = []; if (isEqtvTest) { const bid = bidRequests[0]; const currency = config.getConfig('currency.adServerCurrency') || 'USD'; @@ -309,8 +323,8 @@ export const sharethroughAdapterSpec = { brandName: bid.ext?.brandName || null, demandSource: bid.ext?.demandSource || null, dchain: bid.ext?.dchain || null, - primaryCatId: bid.ext?.primaryCatId || null, - secondaryCatIds: bid.ext?.secondaryCatIds || null, + primaryCatId: bid.ext?.primaryCatId || '', + secondaryCatIds: bid.ext?.secondaryCatIds || [], mediaType: bid.ext?.mediaType || null, }, }; @@ -320,7 +334,7 @@ export const sharethroughAdapterSpec = { response.vastXml = bid.adm; } else if (response.mediaType === NATIVE) { response.native = { - ortb: JSON.parse(bid.adm) + ortb: JSON.parse(bid.adm), }; } @@ -339,7 +353,7 @@ export const sharethroughAdapterSpec = { getUserSyncs: (syncOptions, serverResponses, gdprConsent) => { if (isEqtvTest) { - return handleCookieSync(syncOptions, serverResponses, gdprConsent, eqtvNetworkId, storage) + return handleCookieSync(syncOptions, serverResponses, gdprConsent, eqtvNetworkId, storage); } else { const shouldCookieSync = syncOptions.pixelEnabled && deepAccess(serverResponses, '0.body.cookieSyncUrls') !== undefined; @@ -349,13 +363,13 @@ export const sharethroughAdapterSpec = { }, // Empty implementation for prebid core to be able to find it - onTimeout: (data) => { }, + onTimeout: (data) => {}, // Empty implementation for prebid core to be able to find it - onBidWon: (bid) => { }, + onBidWon: (bid) => {}, // Empty implementation for prebid core to be able to find it - onSetTargeting: (bid) => { }, + onSetTargeting: (bid) => {}, }; function getBidRequestFloor(bid) { diff --git a/modules/smarthubBidAdapter.js b/modules/smarthubBidAdapter.js index 853c0d1a29a..2a896a92499 100644 --- a/modules/smarthubBidAdapter.js +++ b/modules/smarthubBidAdapter.js @@ -25,12 +25,16 @@ const ALIASES = { 'addigi': {area: '1', pid: '425'}, 'jambojar': {area: '1', pid: '426'}, 'anzu': {area: '1', pid: '445'}, + 'amcom': {area: '1', pid: '397'}, + 'adastra': {area: '1', pid: '33'}, + 'radiantfusion': {area: '1', pid: '455'}, }; const BASE_URLS = { 'attekmi': 'https://prebid.attekmi.co/pbjs', 'smarthub': 'https://prebid.attekmi.co/pbjs', 'markapp': 'https://markapp-prebid.attekmi.co/pbjs', + 'markapp-apac': 'https://markapp-apac-prebid.attekmi.co/pbjs', 'jdpmedia': 'https://jdpmedia-prebid.attekmi.co/pbjs', 'tredio': 'https://tredio-prebid.attekmi.co/pbjs', 'felixads': 'https://felixads-prebid.attekmi.co/pbjs', @@ -40,20 +44,23 @@ const BASE_URLS = { 'jambojar': 'https://jambojar-prebid.attekmi.co/pbjs', 'jambojar-apac': 'https://jambojar-apac-prebid.attekmi.co/pbjs', 'anzu': 'https://anzu-prebid.attekmi.co/pbjs', + 'amcom': 'https://amcom-prebid.attekmi.co/pbjs', + 'adastra': 'https://adastra-prebid.attekmi.co/pbjs', + 'radiantfusion': 'https://radiantfusion-prebid.attekmi.co/pbjs', }; const adapterState = {}; const _getPartnerUrl = (partner) => { const region = ALIASES[partner]?.region; - const partnerRegion = region ? `${partner}-${String(region).toLocaleLowerCase()}` : partner; + const partnerName = region ? `${partner}-${String(region).toLocaleLowerCase()}` : partner; const urls = Object.keys(BASE_URLS); - if (urls.includes(partnerRegion)) { - return BASE_URLS[partnerRegion]; + if (urls.includes(partnerName)) { + return BASE_URLS[partnerName]; } - return `${BASE_URLS[BIDDER_CODE]}?partnerName=${partnerRegion}`; + return `${BASE_URLS[BIDDER_CODE]}?partnerName=${partnerName}`; } const _getPartnerName = (bid) => String(bid.params?.partnerName || bid.bidder).toLowerCase(); diff --git a/modules/snigelBidAdapter.js b/modules/snigelBidAdapter.js index 937c597c46c..29534ae7318 100644 --- a/modules/snigelBidAdapter.js +++ b/modules/snigelBidAdapter.js @@ -18,7 +18,6 @@ const getConfig = config.getConfig; const storageManager = getStorageManager({bidderCode: BIDDER_CODE}); const refreshes = {}; const placementCounters = {}; -const pageViewId = generateUUID(); const pageViewStart = new Date().getTime(); let auctionCounter = 0; @@ -43,7 +42,7 @@ export const spec = { site: deepAccess(bidRequests, '0.params.site'), sessionId: getSessionId(), counter: auctionCounter++, - pageViewId: pageViewId, + pageViewId: bidderRequest.pageViewId, pageViewStart: pageViewStart, gdprConsent: gdprApplies === true ? hasFullGdprConsent(deepAccess(bidderRequest, 'gdprConsent')) : false, cur: getCurrencies(), diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index 7e4dede2ce6..2e38a88c2f4 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -8,7 +8,6 @@ import { isInteger, logWarn, getBidIdParameter, - isEmptyStr, mergeDeep } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js' @@ -122,17 +121,6 @@ export const spec = { imp.ext = imp.ext || {} imp.ext.deals = segmentsString.split(',').map(deal => deal.trim()) } - - const auctionEnvironment = bid?.ortb2Imp?.ext?.ae - if (bidderRequest.paapi?.enabled && isInteger(auctionEnvironment)) { - imp.ext = imp.ext || {} - imp.ext.ae = auctionEnvironment - } else { - if (imp.ext?.ae) { - delete imp.ext.ae - } - } - sovrnImps.push(imp) }) @@ -217,13 +205,13 @@ export const spec = { /** * Format Sovrn responses as Prebid bid responses * @param {*} param0 A successful response from Sovrn. - * @return {Array} An array of formatted bids (+ fledgeAuctionConfigs if available) + * @return {Bid[]} An array of formatted bids. */ - interpretResponse: function({ body: {id, seatbid, ext} }) { + interpretResponse: function({ body: {id, seatbid} }) { if (!id || !seatbid || !Array.isArray(seatbid)) return [] try { - const bids = seatbid + return seatbid .filter(seat => seat) .map(seat => seat.bid.map(sovrnBid => { const bid = { @@ -249,45 +237,6 @@ export const spec = { return bid })) .flat() - - let fledgeAuctionConfigs = null; - if (isArray(ext?.igbid)) { - const seller = ext.seller - const decisionLogicUrl = ext.decisionLogicUrl - const sellerTimeout = ext.sellerTimeout - ext.igbid.filter(item => isValidIgBid(item)).forEach((igbid) => { - const perBuyerSignals = {} - igbid.igbuyer.filter(item => isValidIgBuyer(item)).forEach(buyerItem => { - perBuyerSignals[buyerItem.igdomain] = buyerItem.buyerdata - }) - const interestGroupBuyers = [...Object.keys(perBuyerSignals)] - if (interestGroupBuyers.length) { - fledgeAuctionConfigs = fledgeAuctionConfigs || {} - fledgeAuctionConfigs[igbid.impid] = { - seller, - decisionLogicUrl, - sellerTimeout, - interestGroupBuyers: interestGroupBuyers, - perBuyerSignals, - } - } - }) - } - if (fledgeAuctionConfigs) { - fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => { - return { - bidId, - config: Object.assign({ - auctionSignals: {} - }, cfg) - } - }) - return { - bids, - paapi: fledgeAuctionConfigs, - } - } - return bids } catch (e) { logError('Could not interpret bidresponse, error details:', e) return e @@ -385,12 +334,4 @@ function _getBidFloors(bid) { return !isNaN(paramValue) ? paramValue : undefined } -function isValidIgBid(igBid) { - return !isEmptyStr(igBid.impid) && isArray(igBid.igbuyer) && igBid.igbuyer.length -} - -function isValidIgBuyer(igBuyer) { - return !isEmptyStr(igBuyer.igdomain) -} - registerBidder(spec) diff --git a/modules/sparteoBidAdapter.js b/modules/sparteoBidAdapter.js index b84b662990c..13cb0198594 100644 --- a/modules/sparteoBidAdapter.js +++ b/modules/sparteoBidAdapter.js @@ -12,7 +12,7 @@ const BIDDER_CODE = 'sparteo'; const GVLID = 1028; const TTL = 60; const HTTP_METHOD = 'POST'; -const REQUEST_URL = 'https://bid.sparteo.com/auction'; +const REQUEST_URL = `https://bid.sparteo.com/auction?network_id=\${NETWORK_ID}\${SITE_DOMAIN_QUERY}\${APP_DOMAIN_QUERY}\${BUNDLE_QUERY}`; const USER_SYNC_URL_IFRAME = 'https://sync.sparteo.com/sync/iframe.html?from=prebidjs'; let isSynced = window.sparteoCrossfire?.started || false; @@ -25,14 +25,25 @@ const converter = ortbConverter({ request(buildRequest, imps, bidderRequest, context) { const request = buildRequest(imps, bidderRequest, context); - deepSetValue(request, 'site.publisher.ext.params.pbjsVersion', '$prebid.version$'); - - if (bidderRequest.bids[0].params.networkId) { - request.site.publisher.ext.params.networkId = bidderRequest.bids[0].params.networkId; + if (!!(bidderRequest?.ortb2?.site) && !!(bidderRequest?.ortb2?.app)) { + request.site = bidderRequest.ortb2.site; + delete request.app; } - if (bidderRequest.bids[0].params.publisherId) { - request.site.publisher.ext.params.publisherId = bidderRequest.bids[0].params.publisherId; + const hasSite = !!request.site; + const hasApp = !!request.app; + const root = hasSite ? 'site' : (hasApp ? 'app' : null); + + if (root) { + deepSetValue(request, `${root}.publisher.ext.params.pbjsVersion`, '$prebid.version$'); + const networkId = bidderRequest?.bids?.[0]?.params?.networkId; + if (networkId) { + deepSetValue(request, `${root}.publisher.ext.params.networkId`, networkId); + } + const pubId = bidderRequest?.bids?.[0]?.params?.publisherId; + if (pubId) { + deepSetValue(request, `${root}.publisher.ext.params.publisherId`, pubId); + } } return request; @@ -105,6 +116,57 @@ function outstreamRender(bid) { }); } +function replaceMacros(payload, endpoint) { + const networkId = + payload?.site?.publisher?.ext?.params?.networkId ?? + payload?.app?.publisher?.ext?.params?.networkId; + + let siteDomain; + let appDomain; + let bundle; + + if (payload?.site) { + siteDomain = payload.site?.domain; + if (!siteDomain && payload.site?.page) { + try { siteDomain = new URL(payload.site.page).hostname; } catch (e) { } + } + if (siteDomain) { + siteDomain = siteDomain.trim().split('/')[0].split(':')[0].replace(/^www\./, ''); + } else { + logWarn('Domain not found. Missing the site.domain or the site.page field'); + siteDomain = 'unknown'; + } + } else if (payload?.app) { + appDomain = payload.app?.domain || ''; + if (appDomain) { + appDomain = appDomain.trim().split('/')[0].split(':')[0].replace(/^www\./, ''); + } else { + appDomain = 'unknown'; + } + + const raw = payload.app?.bundle ?? ''; + const trimmed = String(raw).trim(); + if (!trimmed || trimmed.toLowerCase() === 'null') { + logWarn('Bundle not found. Missing the app.bundle field.'); + bundle = 'unknown'; + } else { + bundle = trimmed; + } + } + + const macroMap = { + NETWORK_ID: networkId ?? '', + BUNDLE_QUERY: payload?.app ? (bundle ? `&bundle=${encodeURIComponent(bundle)}` : '') : '', + SITE_DOMAIN_QUERY: siteDomain ? `&site_domain=${encodeURIComponent(siteDomain)}` : '', + APP_DOMAIN_QUERY: appDomain ? `&app_domain=${encodeURIComponent(appDomain)}` : '' + }; + + return endpoint.replace( + /\$\{(NETWORK_ID|SITE_DOMAIN_QUERY|APP_DOMAIN_QUERY|BUNDLE_QUERY)\}/g, + (_, key) => String(macroMap[key] ?? '') + ); +} + export const spec = { code: BIDDER_CODE, gvlid: GVLID, @@ -166,9 +228,12 @@ export const spec = { buildRequests: function (bidRequests, bidderRequest) { const payload = converter.toORTB({bidRequests, bidderRequest}) + const endpoint = bidRequests[0].params.endpoint ? bidRequests[0].params.endpoint : REQUEST_URL; + const url = replaceMacros(payload, endpoint); + return { method: HTTP_METHOD, - url: bidRequests[0].params.endpoint ? bidRequests[0].params.endpoint : REQUEST_URL, + url: url, data: payload }; }, diff --git a/modules/sspBCBidAdapter.js b/modules/sspBCBidAdapter.js index 3625b912579..5ce8fb34492 100644 --- a/modules/sspBCBidAdapter.js +++ b/modules/sspBCBidAdapter.js @@ -1,4 +1,5 @@ import { deepAccess, getWinDimensions, getWindowTop, isArray, logInfo, logWarn } from '../src/utils.js'; +import { getDevicePixelRatio } from '../libraries/devicePixelRatio/devicePixelRatio.js'; import { ajax } from '../src/ajax.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; @@ -165,7 +166,7 @@ const getNotificationPayload = bidData => { const applyClientHints = ortbRequest => { const { location } = document; - const { connection = {}, deviceMemory, userAgentData = {} } = navigator; + const { connection = {}, userAgentData = {} } = navigator; const viewport = getWinDimensions().visualViewport || false; const segments = []; const hints = { @@ -173,8 +174,8 @@ const applyClientHints = ortbRequest => { 'CH-Rtt': connection.rtt, 'CH-SaveData': connection.saveData, 'CH-Downlink': connection.downlink, - 'CH-DeviceMemory': deviceMemory, - 'CH-Dpr': W.devicePixelRatio, + 'CH-DeviceMemory': null, + 'CH-Dpr': getDevicePixelRatio(W), 'CH-ViewportWidth': viewport.width, 'CH-BrowserBrands': JSON.stringify(userAgentData.brands), 'CH-isMobile': userAgentData.mobile, diff --git a/modules/ssp_genieeBidAdapter.js b/modules/ssp_genieeBidAdapter.js index c768c511e9c..c4464f0f59a 100644 --- a/modules/ssp_genieeBidAdapter.js +++ b/modules/ssp_genieeBidAdapter.js @@ -342,11 +342,22 @@ export const spec = { */ isBidRequestValid: function (bidRequest) { if (!bidRequest.params.zoneId) return false; - const currencyType = config.getConfig('currency.adServerCurrency'); - if (typeof currencyType === 'string' && ALLOWED_CURRENCIES.indexOf(currencyType) === -1) { - utils.logError('Invalid currency type, we support only JPY and USD!'); - return false; + + if (bidRequest.params.hasOwnProperty('currency')) { + const bidCurrency = bidRequest.params.currency; + + if (!ALLOWED_CURRENCIES.includes(bidCurrency)) { + utils.logError(`[${BIDDER_CODE}] Currency "${bidCurrency}" in bid params is not supported. Supported are: ${ALLOWED_CURRENCIES.join(', ')}.`); + return false; + } + } else { + const adServerCurrency = config.getConfig('currency.adServerCurrency'); + if (typeof adServerCurrency === 'string' && !ALLOWED_CURRENCIES.includes(adServerCurrency)) { + utils.logError(`[${BIDDER_CODE}] adServerCurrency "${adServerCurrency}" is not supported. Supported are: ${ALLOWED_CURRENCIES.join(', ')}.`); + return false; + } } + return true; }, /** diff --git a/modules/startioBidAdapter.js b/modules/startioBidAdapter.js index 76a68f8ce95..74629f2cc9c 100644 --- a/modules/startioBidAdapter.js +++ b/modules/startioBidAdapter.js @@ -7,7 +7,7 @@ import { ortb25Translator } from '../libraries/ortb2.5Translator/translator.js'; const BIDDER_CODE = 'startio'; const METHOD = 'POST'; const GVLID = 1216; -const ENDPOINT_URL = `http://pbc-rtb.startappnetwork.com/1.3/2.5/getbid?account=pbc`; +const ENDPOINT_URL = `https://pbc-rtb.startappnetwork.com/1.3/2.5/getbid?account=pbc`; const converter = ortbConverter({ imp(buildImp, bidRequest, context) { diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index dda4694a52c..3e7127f1ab7 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -114,6 +114,9 @@ const converter = ortbConverter({ bidResponse(buildBidResponse, bid, context) { const bidResponse = buildBidResponse(bid, context); bidResponse.nurl = bid.nurl; + if (bid.burl) { + bidResponse.burl = bid.burl; + } bidResponse.ad = replaceAuctionPrice(bid.adm, bid.price); if (bid.ext && bid.ext.dchain) { deepSetValue(bidResponse, 'meta.dchain', bid.ext.dchain); @@ -212,9 +215,20 @@ export const spec = { return bids; }, onBidWon: (bid) => { - if (bid.nurl) { + if (bid.nurl && !bid.deferBilling) { const resolvedNurl = replaceAuctionPrice(bid.nurl, bid.originalCpm); ajax(resolvedNurl); + bid.taboolaBillingFired = true; + } + }, + onBidBillable: (bid) => { + if (bid.taboolaBillingFired) { + return; + } + const billingUrl = bid.burl || bid.nurl; + if (billingUrl) { + const resolvedBillingUrl = replaceAuctionPrice(billingUrl, bid.originalCpm); + ajax(resolvedBillingUrl); } }, getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { diff --git a/modules/tappxBidAdapter.js b/modules/tappxBidAdapter.js index 10b28c3c7ea..6ca3adb4d54 100644 --- a/modules/tappxBidAdapter.js +++ b/modules/tappxBidAdapter.js @@ -16,7 +16,7 @@ const BIDDER_CODE = 'tappx'; const GVLID_CODE = 628; const TTL = 360; const CUR = 'USD'; -const TAPPX_BIDDER_VERSION = '0.1.4'; +const TAPPX_BIDDER_VERSION = '0.1.5'; const TYPE_CNN = 'prebidjs'; const LOG_PREFIX = '[TAPPX]: '; const VIDEO_SUPPORT = ['instream', 'outstream']; @@ -209,6 +209,7 @@ function interpretBid(serverBid, request) { if (typeof serverBid.lurl !== 'undefined') { bidReturned.lurl = serverBid.lurl } if (typeof serverBid.nurl !== 'undefined') { bidReturned.nurl = serverBid.nurl } if (typeof serverBid.burl !== 'undefined') { bidReturned.burl = serverBid.burl } + if (typeof serverBid.adomain !== 'undefined') { bidReturned.adomain = serverBid.adomain } if (typeof request.bids?.mediaTypes !== 'undefined' && typeof request.bids?.mediaTypes.video !== 'undefined') { bidReturned.vastXml = serverBid.adm; @@ -231,7 +232,7 @@ function interpretBid(serverBid, request) { } if (typeof bidReturned.adomain !== 'undefined' || bidReturned.adomain !== null) { - bidReturned.meta = { advertiserDomains: request.bids?.adomain }; + bidReturned.meta = { advertiserDomains: bidReturned.adomain }; } return bidReturned; diff --git a/modules/tcfControl.ts b/modules/tcfControl.ts index 6884c5a96cc..81d5df3d802 100644 --- a/modules/tcfControl.ts +++ b/modules/tcfControl.ts @@ -65,7 +65,8 @@ const CONFIGURABLE_RULES = { purpose: 'basicAds', enforcePurpose: true, enforceVendor: true, - vendorExceptions: [] + vendorExceptions: [], + deferS2Sbidders: false } }, personalizedAds: { @@ -228,7 +229,7 @@ function getConsent(consentData, type, purposeNo, gvlId) { * @param {number=} gvlId - GVL ID for the module * @returns {boolean} */ -export function validateRules(rule, consentData, currentModule, gvlId) { +export function validateRules(rule, consentData, currentModule, gvlId, params = {}) { const ruleOptions = CONFIGURABLE_RULES[rule.purpose]; // return 'true' if vendor present in 'vendorExceptions' @@ -236,8 +237,9 @@ export function validateRules(rule, consentData, currentModule, gvlId) { return true; } const vendorConsentRequred = rule.enforceVendor && !((gvlId === VENDORLESS_GVLID || (rule.softVendorExceptions || []).includes(currentModule))); + const deferS2Sbidders = params['isS2S'] && rule.purpose === 'basicAds' && rule.deferS2Sbidders && !gvlId; const {purpose, vendor} = getConsent(consentData, ruleOptions.type, ruleOptions.id, gvlId); - return (!rule.enforcePurpose || purpose) && (!vendorConsentRequred || vendor); + return (!rule.enforcePurpose || purpose) && (!vendorConsentRequred || deferS2Sbidders || vendor); } function gdprRule(purposeNo, checkConsent, blocked = null, gvlidFallback: any = () => null) { @@ -247,7 +249,7 @@ function gdprRule(purposeNo, checkConsent, blocked = null, gvlidFallback: any = if (shouldEnforce(consentData, purposeNo, modName)) { const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], modName, gvlidFallback(params)); - const allow = !!checkConsent(consentData, modName, gvlid); + const allow = !!checkConsent(consentData, modName, gvlid, params); if (!allow) { blocked && blocked.add(modName); return {allow}; @@ -257,7 +259,7 @@ function gdprRule(purposeNo, checkConsent, blocked = null, gvlidFallback: any = } function singlePurposeGdprRule(purposeNo, blocked = null, gvlidFallback: any = () => null) { - return gdprRule(purposeNo, (cd, modName, gvlid) => !!validateRules(ACTIVE_RULES.purpose[purposeNo], cd, modName, gvlid), blocked, gvlidFallback); + return gdprRule(purposeNo, (cd, modName, gvlid, params) => !!validateRules(ACTIVE_RULES.purpose[purposeNo], cd, modName, gvlid, params), blocked, gvlidFallback); } function exceptPrebidModules(ruleFn) { diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index 8d12bf81a3e..dbdda501658 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -1,9 +1,11 @@ import {logError, parseSizesInput, isArray, getBidIdParameter, getWinDimensions, getScreenOrientation} from '../src/utils.js'; +import {getDevicePixelRatio} from '../libraries/devicePixelRatio/devicePixelRatio.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; import {isAutoplayEnabled} from '../libraries/autoplayDetection/autoplay.js'; -import {getDM, getHC, getHLen} from '../libraries/navigatorData/navigatorData.js'; +import {getHLen} from '../libraries/navigatorData/navigatorData.js'; import {getTimeToFirstByte} from '../libraries/timeToFirstBytesUtils/timeToFirstBytesUtils.js'; +import { getConnectionInfo } from '../libraries/connectionInfo/connectionUtils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -64,21 +66,21 @@ export const spec = { pageReferrer: document.referrer, pageTitle: getPageTitle().slice(0, 300), pageDescription: getPageDescription().slice(0, 300), - networkBandwidth: getConnectionDownLink(window.navigator), - networkQuality: getNetworkQuality(window.navigator), + networkBandwidth: getConnectionDownLink(), + networkQuality: getNetworkQuality(), timeToFirstByte: getTimeToFirstByte(window), data: bids, domComplexity: getDomComplexity(document), device: bidderRequest?.ortb2?.device || {}, deviceWidth: screen.width, deviceHeight: screen.height, - devicePixelRatio: topWindow.devicePixelRatio, + devicePixelRatio: getDevicePixelRatio(topWindow), screenOrientation: getScreenOrientation(), historyLength: getHLen(), viewportHeight: getWinDimensions().visualViewport.height, viewportWidth: getWinDimensions().visualViewport.width, - hardwareConcurrency: getHC(), - deviceMemory: getDM(), + hardwareConcurrency: null, + deviceMemory: null, hb_version: '$prebid.version$', timeout: bidderRequest?.timeout, eids: getUserIdAsEids(validBidRequests), @@ -256,12 +258,13 @@ function getPageDescription() { return (element && element.content) || ''; } -function getConnectionDownLink(nav) { - return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : ''; +function getConnectionDownLink() { + const connection = getConnectionInfo(); + return connection?.downlink != null ? connection.downlink.toString() : ''; } -function getNetworkQuality(navigator) { - const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; +function getNetworkQuality() { + const connection = getConnectionInfo(); return connection?.effectiveType ?? ''; } diff --git a/modules/terceptAnalyticsAdapter.js b/modules/terceptAnalyticsAdapter.js index 594600b30e8..6bc86ba8c98 100644 --- a/modules/terceptAnalyticsAdapter.js +++ b/modules/terceptAnalyticsAdapter.js @@ -16,6 +16,8 @@ const events = { bids: [] }; +let adUnitMap = new Map(); + var terceptAnalyticsAdapter = Object.assign(adapter( { emptyUrl, @@ -29,21 +31,34 @@ var terceptAnalyticsAdapter = Object.assign(adapter( Object.assign(events, {bids: []}); events.auctionInit = args; auctionTimestamp = args.timestamp; + adUnitMap.set(args.auctionId, args.adUnits); } else if (eventType === EVENTS.BID_REQUESTED) { mapBidRequests(args).forEach(item => { events.bids.push(item) }); } else if (eventType === EVENTS.BID_RESPONSE) { mapBidResponse(args, 'response'); } else if (eventType === EVENTS.NO_BID) { mapBidResponse(args, 'no_bid'); + } else if (eventType === EVENTS.BIDDER_ERROR) { + send({ + bidderError: mapBidResponse(args, 'bidder_error') + }); } else if (eventType === EVENTS.BID_WON) { send({ bidWon: mapBidResponse(args, 'win') - }, 'won'); + }); + } else if (eventType === EVENTS.AD_RENDER_SUCCEEDED) { + send({ + adRenderSucceeded: mapBidResponse(args, 'render_succeeded') + }); + } else if (eventType === EVENTS.AD_RENDER_FAILED) { + send({ + adRenderFailed: mapBidResponse(args, 'render_failed') + }); } } if (eventType === EVENTS.AUCTION_END) { - send(events, 'auctionEnd'); + send(events); } } }); @@ -68,28 +83,93 @@ function mapBidRequests(params) { return arr; } +function getAdSlotData(auctionId, adUnitCode) { + const auctionAdUnits = adUnitMap?.get(auctionId); + + if (!Array.isArray(auctionAdUnits)) { + return {}; + } + + const matchingAdUnit = auctionAdUnits.find(au => au.code === adUnitCode); + + return { + adserverAdSlot: matchingAdUnit?.ortb2Imp?.ext?.data?.adserver?.adslot, + pbAdSlot: matchingAdUnit?.ortb2Imp?.ext?.data?.pbadslot, + }; +} + function mapBidResponse(bidResponse, status) { - if (status !== 'win') { - const bid = events.bids.filter(o => o.bidId === bidResponse.bidId || o.bidId === bidResponse.requestId)[0]; + const isRenderEvent = (status === 'render_succeeded' || status === 'render_failed'); + const bid = isRenderEvent ? bidResponse.bid : bidResponse; + const { adserverAdSlot, pbAdSlot } = getAdSlotData(bid?.auctionId, bid?.adUnitCode); + + if (status === 'bidder_error') { + return { + ...bidResponse, + adserverAdSlot: adserverAdSlot, + pbAdSlot: pbAdSlot, + status: 6, + host: window.location.hostname, + path: window.location.pathname, + search: window.location.search + } + } else if (status !== 'win') { + const existingBid = isRenderEvent ? null : events.bids.filter(o => o.bidId === bid.bidId || o.bidId === bid.requestId)[0]; const responseTimestamp = Date.now(); - Object.assign(bid, { - bidderCode: bidResponse.bidder, - bidId: status === 'timeout' ? bidResponse.bidId : bidResponse.requestId, - adUnitCode: bidResponse.adUnitCode, - auctionId: bidResponse.auctionId, - creativeId: bidResponse.creativeId, - transactionId: bidResponse.transactionId, - currency: bidResponse.currency, - cpm: bidResponse.cpm, - netRevenue: bidResponse.netRevenue, - mediaType: bidResponse.mediaType, - statusMessage: bidResponse.statusMessage, - status: bidResponse.status, - renderStatus: status === 'timeout' ? 3 : (status === 'no_bid' ? 5 : 2), - timeToRespond: bidResponse.timeToRespond, - requestTimestamp: bidResponse.requestTimestamp, - responseTimestamp: bidResponse.responseTimestamp ? bidResponse.responseTimestamp : responseTimestamp - }); + + const getRenderStatus = () => { + if (status === 'timeout') return 3; + if (status === 'no_bid') return 5; + if (status === 'render_succeeded') return 7; + if (status === 'render_failed') return 8; + return 2; + }; + + const mappedData = { + bidderCode: bid.bidder, + bidId: (status === 'timeout' || status === 'no_bid') ? bid.bidId : bid.requestId, + adUnitCode: bid.adUnitCode, + auctionId: bid.auctionId, + creativeId: bid.creativeId, + transactionId: bid.transactionId, + currency: bid.currency, + cpm: bid.cpm, + netRevenue: bid.netRevenue, + renderedSize: isRenderEvent ? bid.size : null, + width: bid.width, + height: bid.height, + mediaType: bid.mediaType, + statusMessage: bid.statusMessage, + status: bid.status, + renderStatus: getRenderStatus(), + timeToRespond: bid.timeToRespond, + requestTimestamp: bid.requestTimestamp, + responseTimestamp: bid.responseTimestamp ? bid.responseTimestamp : responseTimestamp, + renderTimestamp: isRenderEvent ? Date.now() : null, + reason: status === 'render_failed' ? bidResponse.reason : null, + message: status === 'render_failed' ? bidResponse.message : null, + host: isRenderEvent ? window.location.hostname : null, + path: isRenderEvent ? window.location.pathname : null, + search: isRenderEvent ? window.location.search : null, + adserverAdSlot: adserverAdSlot, + pbAdSlot: pbAdSlot, + ttl: bid.ttl, + dealId: bid.dealId, + ad: isRenderEvent ? null : bid.ad, + adUrl: isRenderEvent ? null : bid.adUrl, + adId: bid.adId, + size: isRenderEvent ? null : bid.size, + adserverTargeting: isRenderEvent ? null : bid.adserverTargeting, + videoCacheKey: isRenderEvent ? null : bid.videoCacheKey, + native: isRenderEvent ? null : bid.native, + meta: bid.meta || {} + }; + + if (isRenderEvent) { + return mappedData; + } else { + Object.assign(existingBid, mappedData); + } } else { return { bidderCode: bidResponse.bidder, @@ -102,6 +182,8 @@ function mapBidResponse(bidResponse, status) { cpm: bidResponse.cpm, netRevenue: bidResponse.netRevenue, renderedSize: bidResponse.size, + width: bidResponse.width, + height: bidResponse.height, mediaType: bidResponse.mediaType, statusMessage: bidResponse.statusMessage, status: bidResponse.status, @@ -109,14 +191,28 @@ function mapBidResponse(bidResponse, status) { timeToRespond: bidResponse.timeToRespond, requestTimestamp: bidResponse.requestTimestamp, responseTimestamp: bidResponse.responseTimestamp, + renderTimestamp: null, + reason: null, + message: null, host: window.location.hostname, path: window.location.pathname, - search: window.location.search + search: window.location.search, + adserverAdSlot: adserverAdSlot, + pbAdSlot: pbAdSlot, + ttl: bidResponse.ttl, + dealId: bidResponse.dealId, + ad: bidResponse.ad, + adUrl: bidResponse.adUrl, + adId: bidResponse.adId, + adserverTargeting: bidResponse.adserverTargeting, + videoCacheKey: bidResponse.videoCacheKey, + native: bidResponse.native, + meta: bidResponse.meta || {} } } } -function send(data, status) { +function send(data) { const location = getWindowLocation(); if (typeof data !== 'undefined' && typeof data.auctionInit !== 'undefined') { Object.assign(data.auctionInit, { host: location.host, path: location.pathname, search: location.search }); diff --git a/modules/theAdxBidAdapter.js b/modules/theAdxBidAdapter.js index 15ac4376548..d57e307c7e1 100644 --- a/modules/theAdxBidAdapter.js +++ b/modules/theAdxBidAdapter.js @@ -9,6 +9,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { getConnectionInfo } from '../libraries/connectionInfo/connectionUtils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -374,11 +375,11 @@ const buildDeviceComponent = (bidRequest, bidderRequest) => { dnt: getDNT() ? 1 : 0, }; // Include connection info if available - const CONNECTION = navigator.connection || navigator.webkitConnection; - if (CONNECTION && CONNECTION.type) { - device['connectiontype'] = CONNECTION.type; - if (CONNECTION.downlinkMax) { - device['connectionDownlinkMax'] = CONNECTION.downlinkMax; + const connection = getConnectionInfo(); + if (connection?.type) { + device['connectiontype'] = connection.type; + if (connection.downlinkMax != null) { + device['connectionDownlinkMax'] = connection.downlinkMax; } } diff --git a/modules/timeoutRtdProvider.js b/modules/timeoutRtdProvider.js index fd7f639a1db..72ed6b7b9f8 100644 --- a/modules/timeoutRtdProvider.js +++ b/modules/timeoutRtdProvider.js @@ -2,6 +2,7 @@ import { submodule } from '../src/hook.js'; import * as ajax from '../src/ajax.js'; import { logInfo, deepAccess, logError } from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; +import { bidderTimeoutFunctions } from '../libraries/bidderTimeoutUtils/bidderTimeoutUtils.js'; /** * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule @@ -11,113 +12,9 @@ const SUBMODULE_NAME = 'timeout'; // this allows the stubbing of functions during testing export const timeoutRtdFunctions = { - getDeviceType, - getConnectionSpeed, - checkVideo, - calculateTimeoutModifier, handleTimeoutIncrement }; -const entries = Object.entries || function(obj) { - const ownProps = Object.keys(obj); - let i = ownProps.length; - const resArray = new Array(i); - while (i--) { resArray[i] = [ownProps[i], obj[ownProps[i]]]; } - return resArray; -}; - -function getDeviceType() { - const userAgent = window.navigator.userAgent.toLowerCase(); - if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(userAgent))) { - return 5; // tablet - } - if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(userAgent))) { - return 4; // mobile - } - return 2; // personal computer -} - -function checkVideo(adUnits) { - return adUnits.some((adUnit) => { - return adUnit.mediaTypes && adUnit.mediaTypes.video; - }); -} - -function getConnectionSpeed() { - const connection = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection || {} - const connectionType = connection.type || connection.effectiveType; - - switch (connectionType) { - case 'slow-2g': - case '2g': - return 'slow'; - - case '3g': - return 'medium'; - - case 'bluetooth': - case 'cellular': - case 'ethernet': - case 'wifi': - case 'wimax': - case '4g': - return 'fast'; - } - - return 'unknown'; -} -/** - * Calculate the time to be added to the timeout - * @param {Array} adUnits - * @param {Object} rules - * @return {number} - */ -function calculateTimeoutModifier(adUnits, rules) { - logInfo('Timeout rules', rules); - let timeoutModifier = 0; - let toAdd = 0; - - if (rules.includesVideo) { - const hasVideo = timeoutRtdFunctions.checkVideo(adUnits); - toAdd = rules.includesVideo[hasVideo] || 0; - logInfo(`Adding ${toAdd} to timeout for includesVideo ${hasVideo}`) - timeoutModifier += toAdd; - } - - if (rules.numAdUnits) { - const numAdUnits = adUnits.length; - if (rules.numAdUnits[numAdUnits]) { - timeoutModifier += rules.numAdUnits[numAdUnits]; - } else { - for (const [rangeStr, timeoutVal] of entries(rules.numAdUnits)) { - const [lowerBound, upperBound] = rangeStr.split('-'); - if (parseInt(lowerBound) <= numAdUnits && numAdUnits <= parseInt(upperBound)) { - logInfo(`Adding ${timeoutVal} to timeout for numAdUnits ${numAdUnits}`) - timeoutModifier += timeoutVal; - break; - } - } - } - } - - if (rules.deviceType) { - const deviceType = timeoutRtdFunctions.getDeviceType(); - toAdd = rules.deviceType[deviceType] || 0; - logInfo(`Adding ${toAdd} to timeout for deviceType ${deviceType}`) - timeoutModifier += toAdd; - } - - if (rules.connectionSpeed) { - const connectionSpeed = timeoutRtdFunctions.getConnectionSpeed(); - toAdd = rules.connectionSpeed[connectionSpeed] || 0; - logInfo(`Adding ${toAdd} to timeout for connectionSpeed ${connectionSpeed}`) - timeoutModifier += toAdd; - } - - logInfo('timeout Modifier calculated', timeoutModifier); - return timeoutModifier; -} - /** * * @param {Object} reqBidsConfigObj @@ -161,7 +58,7 @@ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { */ function handleTimeoutIncrement(reqBidsConfigObj, rules) { const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; - const timeoutModifier = timeoutRtdFunctions.calculateTimeoutModifier(adUnits, rules); + const timeoutModifier = bidderTimeoutFunctions.calculateTimeoutModifier(adUnits, rules); const bidderTimeout = reqBidsConfigObj.timeout || getGlobal().getConfig('bidderTimeout'); reqBidsConfigObj.timeout = bidderTimeout + timeoutModifier; } diff --git a/modules/toponBidAdapter.js b/modules/toponBidAdapter.js new file mode 100644 index 00000000000..5df8f3ff943 --- /dev/null +++ b/modules/toponBidAdapter.js @@ -0,0 +1,226 @@ +import { logWarn, generateUUID } from "../src/utils.js"; +import { registerBidder } from "../src/adapters/bidderFactory.js"; +import { BANNER } from "../src/mediaTypes.js"; +import { ortbConverter } from "../libraries/ortbConverter/converter.js"; + +const PREBID_VERSION = "$prebid.version$"; +const BIDDER_CODE = "topon"; +const LOG_PREFIX = "TopOn"; +const GVLID = 1305; +const ENDPOINT = "https://web-rtb.anyrtb.com/ortb/prebid"; +const DEFAULT_TTL = 360; +const USER_SYNC_URL = "https://pb.anyrtb.com/pb/page/prebidUserSyncs.html"; +const USER_SYNC_IMG_URL = "https://cm.anyrtb.com/cm/sdk_sync"; + +let lastPubid; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: DEFAULT_TTL, + currency: "USD", + }, + imp(buildImp, bidRequest, context) { + const mediaType = + bidRequest.mediaType || Object.keys(bidRequest.mediaTypes || {})[0]; + + if (mediaType === "banner") { + const sizes = bidRequest.mediaTypes.banner.sizes; + return { + id: bidRequest.bidId, + banner: { + format: sizes.map(([w, h]) => ({ w, h })), + }, + tagid: bidRequest.adUnitCode, + }; + } + + return null; + }, + request(buildRequest, imps, bidderRequest, context) { + const requestId = + bidderRequest.bidderRequestId || + bidderRequest.auctionId || + generateUUID(); + const ortb2 = bidderRequest.ortb2 || {}; + + return { + id: requestId, + imp: imps, + site: { + page: ortb2.site?.page || bidderRequest.refererInfo?.page, + domain: ortb2.site?.domain || location.hostname, + }, + device: ortb2.device || {}, + ext: { + prebid: { + channel: { + version: PREBID_VERSION, + source: "pbjs", + }, + }, + }, + source: { + ext: { + prebid: 1, + }, + }, + }; + }, + bidResponse(buildBidResponse, bid, context) { + return buildBidResponse(bid, context); + }, + response(buildResponse, bidResponses, ortbResponse, context) { + return buildResponse(bidResponses, ortbResponse, context); + }, + overrides: { + imp: { + bidfloor: false, + extBidfloor: false, + }, + bidResponse: { + native: false, + }, + }, +}); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER], + isBidRequestValid: (bid) => { + if (!(bid && bid.params)) { + return false; + } + const { pubid } = bid.params || {}; + if (!pubid) { + return false; + } else if (typeof pubid !== "string") { + return false; + } + return true; + }, + buildRequests: (validBidRequests, bidderRequest) => { + const { pubid } = bidderRequest?.bids?.[0]?.params || {}; + lastPubid = pubid; + const ortbRequest = converter.toORTB({ validBidRequests, bidderRequest }); + + const url = ENDPOINT + "?pubid=" + pubid; + return { + method: "POST", + url, + data: ortbRequest, + }; + }, + interpretResponse: (response, request) => { + if (!response.body || typeof response.body !== "object") { + return; + } + + const { id, seatbid: seatbids } = response.body; + if (id && seatbids) { + seatbids.forEach((seatbid) => { + seatbid.bid.forEach((bid) => { + let height = bid.h; + let width = bid.w; + const isBanner = bid.mtype === 1; + if ( + (!height || !width) && + request.data && + request.data.imp && + request.data.imp.length > 0 + ) { + request.data.imp.forEach((req) => { + if (bid.impid === req.id) { + if (isBanner) { + let bannerHeight = 1; + let bannerWidth = 1; + if (req.banner.format && req.banner.format.length > 0) { + bannerHeight = req.banner.format[0].h; + bannerWidth = req.banner.format[0].w; + } + height = bannerHeight; + width = bannerWidth; + } else { + height = 1; + width = 1; + } + } + }); + bid.w = width; + bid.h = height; + } + }); + }); + } + + const { bids } = converter.fromORTB({ + response: response.body, + request: request.data, + }); + + return bids; + }, + getUserSyncs: function ( + syncOptions, + responses, + gdprConsent, + uspConsent, + gppConsent + ) { + const pubid = lastPubid; + const syncs = []; + const params = []; + + if (typeof pubid === "string" && pubid.length > 0) { + params.push(`pubid=tpn${encodeURIComponent(pubid)}`); + } + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === "boolean") { + params.push(`gdpr=${Number(gdprConsent.gdprApplies)}`); + } + if (typeof gdprConsent.consentString === "string") { + params.push(`consent=${encodeURIComponent(gdprConsent.consentString)}`); + } + } + if (uspConsent) { + params.push(`us_privacy=${encodeURIComponent(uspConsent)}`); + } + if (gppConsent) { + if (typeof gppConsent.gppString === "string") { + params.push(`gpp=${encodeURIComponent(gppConsent.gppString)}`); + } + if (Array.isArray(gppConsent.applicableSections)) { + params.push( + `gpp_sid=${encodeURIComponent( + gppConsent.applicableSections.join(",") + )}` + ); + } + } + if (syncOptions?.iframeEnabled) { + const syncUrl = `${USER_SYNC_URL}${ + params.length > 0 ? "?" + params.join("&") : "" + }`; + + syncs.push({ + type: "iframe", + url: syncUrl, + }); + } else if (syncOptions?.pixelEnabled) { + const syncUrl = `${USER_SYNC_IMG_URL}${ + params.length > 0 ? "?" + params.join("&") : "" + }`; + + syncs.push({ + type: "image", + url: syncUrl, + }); + } + return syncs; + }, + onBidWon: (bid) => { + logWarn(`[${LOG_PREFIX}] Bid won: ${JSON.stringify(bid)}`); + }, +}; +registerBidder(spec); diff --git a/modules/toponBidAdapter.md b/modules/toponBidAdapter.md new file mode 100644 index 00000000000..e42ad438fb8 --- /dev/null +++ b/modules/toponBidAdapter.md @@ -0,0 +1,29 @@ +# Overview + +``` +Module Name: TopOn Bid Adapter +Module Type: Bidder Adapter +Maintainer: support@toponad.net +``` + +# Description + +TopOn Bid Adapter for Prebid.js + +# Sample Banner Ad Unit: For Publishers + +``` +var adUnits = [{ + code: 'test-div', + sizes: [ + [300, 250], + [728, 90] + ], + bids: [{ + bidder: 'topon', + params: { + pubid: 'pub-uuid', // required, must be a string, not an integer or other js type. + } + }] +}]; +``` diff --git a/modules/trustxBidAdapter.js b/modules/trustxBidAdapter.js new file mode 100644 index 00000000000..2b0f2c80331 --- /dev/null +++ b/modules/trustxBidAdapter.js @@ -0,0 +1,456 @@ +import { + logInfo, + logError, + logMessage, + deepAccess, + deepSetValue, + mergeDeep +} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {Renderer} from '../src/Renderer.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {config} from '../src/config.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + */ + +const BIDDER_CODE = 'trustx'; +const BID_TTL = 360; +const NET_REVENUE = false; +const SUPPORTED_CURRENCY = 'USD'; +const OUTSTREAM_PLAYER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; +const ADAPTER_VERSION = '1.0'; + +const ortbAdapterConverter = ortbConverter({ + context: { + netRevenue: NET_REVENUE, + ttl: BID_TTL + }, + imp(buildImp, bidRequest, context) { + const impression = buildImp(bidRequest, context); + const params = bidRequest.params || {}; + + if (!impression.bidfloor) { + let floor = parseFloat(params.bidfloor || params.bidFloor || params.floorcpm || 0) || 0; + + if (typeof bidRequest.getFloor === 'function') { + const mediaTypes = bidRequest.mediaTypes || {}; + const curMediaType = mediaTypes.video ? 'video' : 'banner'; + const floorInfo = bidRequest.getFloor({ + currency: SUPPORTED_CURRENCY, + mediaType: curMediaType, + size: bidRequest.sizes ? bidRequest.sizes.map(([w, h]) => ({w, h})) : '*' + }); + + if (floorInfo && typeof floorInfo === 'object' && + floorInfo.currency === SUPPORTED_CURRENCY && + !isNaN(parseFloat(floorInfo.floor))) { + floor = Math.max(floor, parseFloat(floorInfo.floor)); + } + } + + impression.bidfloor = floor; + impression.bidfloorcur = params.currency || SUPPORTED_CURRENCY; + } + + const tagId = params.uid || params.secid; + if (tagId) { + impression.tagid = tagId.toString(); + } + + if (bidRequest.adUnitCode) { + if (!impression.ext) { + impression.ext = {}; + } + impression.ext.divid = bidRequest.adUnitCode.toString(); + } + + if (impression.banner && impression.banner.format && impression.banner.format.length > 0) { + const firstFormat = impression.banner.format[0]; + if (firstFormat.w && firstFormat.h && (impression.banner.w == null || impression.banner.h == null)) { + impression.banner.w = firstFormat.w; + impression.banner.h = firstFormat.h; + } + } + + return impression; + }, + request(buildRequest, imps, bidderRequest, context) { + const requestObj = buildRequest(imps, bidderRequest, context); + mergeDeep(requestObj, { + ext: { + hb: 1, + prebidver: '$prebid.version$', + adapterver: ADAPTER_VERSION, + } + }); + + if (!requestObj.source) { + requestObj.source = {}; + } + + if (!requestObj.source.tid && bidderRequest.ortb2?.source?.tid) { + requestObj.source.tid = bidderRequest.ortb2.source.tid.toString(); + } + + if (!requestObj.source.ext) { + requestObj.source.ext = {}; + } + requestObj.source.ext.wrapper = 'Prebid_js'; + requestObj.source.ext.wrapper_version = '$prebid.version$'; + + // CCPA + if (bidderRequest.uspConsent) { + deepSetValue(requestObj, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + // GPP + if (bidderRequest.gppConsent?.gppString) { + deepSetValue(requestObj, 'regs.gpp', bidderRequest.gppConsent.gppString); + deepSetValue(requestObj, 'regs.gpp_sid', bidderRequest.gppConsent.applicableSections); + } else if (bidderRequest.ortb2?.regs?.gpp) { + deepSetValue(requestObj, 'regs.gpp', bidderRequest.ortb2.regs.gpp); + deepSetValue(requestObj, 'regs.gpp_sid', bidderRequest.ortb2.regs.gpp_sid); + } + + // COPPA + if (config.getConfig('coppa') === true) { + deepSetValue(requestObj, 'regs.coppa', 1); + } + + // User IDs (eIDs) + if (bidderRequest.bidRequests && bidderRequest.bidRequests.length > 0) { + const bidRequest = bidderRequest.bidRequests[0]; + if (bidRequest.userIdAsEids && bidRequest.userIdAsEids.length > 0) { + deepSetValue(requestObj, 'user.ext.eids', bidRequest.userIdAsEids); + } + + // Supply Chain (schain) + if (bidRequest.schain) { + deepSetValue(requestObj, 'source.ext.schain', bidRequest.schain); + } + } + + if (requestObj.tmax == null && bidderRequest.timeout) { + const timeout = parseInt(bidderRequest.timeout, 10); + if (!isNaN(timeout)) { + requestObj.tmax = timeout; + } + } + + return requestObj; + }, + bidResponse(buildBidResponse, bid, context) { + const {bidRequest} = context; + let responseMediaType; + + if (bid.mtype === 2) { + responseMediaType = VIDEO; + } else if (bid.mtype === 1) { + responseMediaType = BANNER; + } else { + responseMediaType = BANNER; + } + + context.mediaType = responseMediaType; + context.currency = SUPPORTED_CURRENCY; + + if (responseMediaType === VIDEO) { + context.vastXml = bid.adm; + } + + const bidResponseObj = buildBidResponse(bid, context); + + if (bid.ext?.bidder?.trustx?.networkName) { + bidResponseObj.adserverTargeting = { 'hb_ds': bid.ext.bidder.trustx.networkName }; + if (!bidResponseObj.meta) { + bidResponseObj.meta = {}; + } + bidResponseObj.meta.demandSource = bid.ext.bidder.trustx.networkName; + } + + if (responseMediaType === VIDEO && bidRequest.mediaTypes.video.context === 'outstream') { + bidResponseObj.renderer = setupOutstreamRenderer(bidResponseObj); + bidResponseObj.adResponse = { + content: bidResponseObj.vastXml, + width: bidResponseObj.width, + height: bidResponseObj.height + }; + } + + return bidResponseObj; + } +}); + +export const spec = { + code: BIDDER_CODE, + VERSION: ADAPTER_VERSION, + supportedMediaTypes: [BANNER, VIDEO], + ENDPOINT: 'https://ads.trustx.org/pbhb', + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + return ( + isParamsValid(bid) && + isBannerValid(bid) && + isVideoValid(bid) + ); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests + * @param {object} bidderRequest bidder request object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const adType = containsVideoRequest(validBidRequests) ? VIDEO : BANNER; + const requestData = ortbAdapterConverter.toORTB({ + bidRequests: validBidRequests, + bidderRequest, + context: {contextMediaType: adType} + }); + + if (validBidRequests[0].params.test) { + logMessage('trustx test mode enabled'); + } + + return { + method: 'POST', + url: spec.ENDPOINT, + data: requestData + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {object} bidRequest The bid request object. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + return ortbAdapterConverter.fromORTB({ + response: serverResponse.body, + request: bidRequest.data + }).bids; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {object[]} The user syncs which should be dropped. + */ + getUserSyncs: function(syncOptions, serverResponses) { + logInfo('trustx.getUserSyncs', 'syncOptions', syncOptions, 'serverResponses', serverResponses); + let syncElements = []; + + // Early return if sync is completely disabled + if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { + return syncElements; + } + + // Server always returns ext.usersync, so we extract sync URLs from server response + if (serverResponses && Array.isArray(serverResponses) && serverResponses.length > 0) { + serverResponses.forEach(resp => { + const userSync = deepAccess(resp, 'body.ext.usersync'); + if (userSync) { + Object.keys(userSync).forEach(key => { + const value = userSync[key]; + if (value.syncs && value.syncs.length) { + value.syncs.forEach(syncItem => { + syncElements.push({ + type: syncItem.type === 'iframe' ? 'iframe' : 'image', + url: syncItem.url + }); + }); + } + }); + } + }); + + // Per requirement: If iframeEnabled, return only iframes; + // if not iframeEnabled but pixelEnabled, return only pixels + if (syncOptions.iframeEnabled) { + syncElements = syncElements.filter(s => s.type === 'iframe'); + } else if (syncOptions.pixelEnabled) { + syncElements = syncElements.filter(s => s.type === 'image'); + } + } + + logInfo('trustx.getUserSyncs result=%o', syncElements); + return syncElements; + } +}; + +/** + * Creates an outstream renderer for video ads + * @param {Bid} bid The bid response + * @return {Renderer} A renderer for outstream video + */ +function setupOutstreamRenderer(bid) { + const renderer = Renderer.install({ + id: bid.adId, + url: OUTSTREAM_PLAYER_URL, + loaded: false + }); + + renderer.setRender(outstreamRender); + + return renderer; +} + +/** + * Outstream renderer function called by the renderer + * @param {Object} bid The bid object + */ +function outstreamRender(bid) { + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + sizes: [bid.width, bid.height], + targetId: bid.adUnitCode, + adResponse: bid.adResponse + }); + }); +} + +/* ======== + * Helpers + *========= + */ + +/** + * Checks if the bid request has banner media type + * @param {BidRequest} bidRequest + * @return {boolean} True if has banner media type + */ +function hasBannerFormat(bidRequest) { + return !!deepAccess(bidRequest, 'mediaTypes.banner'); +} + +/** + * Checks if the bid request has video media type + * @param {BidRequest|BidRequest[]} bidRequest bid request or array of bid requests + * @return {boolean} True if has video media type + */ +function containsVideoRequest(bidRequest) { + if (Array.isArray(bidRequest)) { + return bidRequest.some(bid => !!deepAccess(bid, 'mediaTypes.video')); + } + return !!deepAccess(bidRequest, 'mediaTypes.video'); +} + +/** + * Validates basic bid parameters + * @param {BidRequest} bidRequest + * @return {boolean} True if parameters are valid + */ +function isParamsValid(bidRequest) { + if (!bidRequest.params) { + return false; + } + + if (bidRequest.params.test) { + return true; + } + + const hasTagId = bidRequest.params.uid || bidRequest.params.secid; + + if (!hasTagId) { + logError('trustx validation failed: Placement ID (uid or secid) not declared'); + return false; + } + + const hasMediaType = containsVideoRequest(bidRequest) || hasBannerFormat(bidRequest); + if (!hasMediaType) { + return false; + } + + return true; +} + +/** + * Validates banner bid request + * @param {BidRequest} bidRequest + * @return {boolean} True if valid banner bid request + */ +function isBannerValid(bidRequest) { + // If there's no banner no need to validate + if (!hasBannerFormat(bidRequest)) { + return true; + } + + const banner = deepAccess(bidRequest, 'mediaTypes.banner'); + if (!Array.isArray(banner.sizes)) { + return false; + } + + return true; +} + +/** + * Validates video bid request + * @param {BidRequest} bidRequest + * @return {boolean} True if valid video bid request + */ +function isVideoValid(bidRequest) { + // If there's no video no need to validate + if (!containsVideoRequest(bidRequest)) { + return true; + } + + const videoConfig = deepAccess(bidRequest, 'mediaTypes.video', {}); + const videoBidParams = deepAccess(bidRequest, 'params.video', {}); + const params = deepAccess(bidRequest, 'params', {}); + + if (params && params.test) { + return true; + } + + const consolidatedVideoParams = { + ...videoConfig, + ...videoBidParams // Bidder Specific overrides + }; + + if (!Array.isArray(consolidatedVideoParams.mimes) || consolidatedVideoParams.mimes.length === 0) { + logError('trustx validation failed: mimes are invalid'); + return false; + } + + if (!Array.isArray(consolidatedVideoParams.protocols) || consolidatedVideoParams.protocols.length === 0) { + logError('trustx validation failed: protocols are invalid'); + return false; + } + + if (!consolidatedVideoParams.context) { + logError('trustx validation failed: context id not declared'); + return false; + } + + if (consolidatedVideoParams.context !== 'instream' && consolidatedVideoParams.context !== 'outstream') { + logError('trustx validation failed: only context instream or outstream is supported'); + return false; + } + + if (typeof consolidatedVideoParams.playerSize === 'undefined' || !Array.isArray(consolidatedVideoParams.playerSize) || !Array.isArray(consolidatedVideoParams.playerSize[0])) { + logError('trustx validation failed: player size not declared or is not in format [[w,h]]'); + return false; + } + + return true; +} + +registerBidder(spec); diff --git a/modules/trustxBidAdapter.md b/modules/trustxBidAdapter.md new file mode 100644 index 00000000000..c9634201cb1 --- /dev/null +++ b/modules/trustxBidAdapter.md @@ -0,0 +1,298 @@ +# Overview + +``` +Module Name: TRUSTX Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@trustx.org +``` + +# Description + +Module that connects to TRUSTX's premium demand sources. +TRUSTX bid adapter supports Banner and Video ad formats with advanced targeting capabilities. + +# Required Parameters + +## Banner + +- `uid` or `secid` (required) - Placement ID / Tag ID +- `mediaTypes.banner.sizes` (required) - Array of banner sizes + +## Video + +- `uid` or `secid` (required) - Placement ID / Tag ID +- `mediaTypes.video.context` (required) - Must be 'instream' or 'outstream' +- `mediaTypes.video.playerSize` (required) - Array format [[w, h]] +- `mediaTypes.video.mimes` (required) - Array of MIME types +- `mediaTypes.video.protocols` (required) - Array of protocol numbers + +# Test Parameters + +## Banner + +``` +var adUnits = [ + { + code: 'trustx-banner-container', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600], [728, 90]] + } + }, + bids: [{ + bidder: 'trustx', + params: { + uid: 123456, + bidFloor: 0.5 + } + }] + } +]; +``` + +## Video + +We support the following OpenRTB params that can be specified in `mediaTypes.video` or in `bids[].params.video`: +- 'mimes' +- 'minduration' +- 'maxduration' +- 'plcmt' +- 'protocols' +- 'startdelay' +- 'skip' +- 'skipafter' +- 'minbitrate' +- 'maxbitrate' +- 'delivery' +- 'playbackmethod' +- 'api' +- 'linearity' + +## Instream Video adUnit using mediaTypes.video + +*Note:* By default, the adapter will read the mandatory parameters from mediaTypes.video. +*Note:* The TRUSTX ad server will respond with a VAST XML to load into your defined player. + +``` +var adUnits = [ + { + code: 'trustx-video-container', + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + mimes: ['video/mp4', 'video/webm'], + protocols: [2, 3, 5, 6], + api: [2, 7], + position: 1, + delivery: [2], + minduration: 5, + maxduration: 60, + plcmt: 1, + playbackmethod: [1, 3, 5], + } + }, + bids: [ + { + bidder: 'trustx', + params: { + uid: 123456, + bidFloor: 5.0 + } + } + ] + } +] +``` + +## Outstream Video + +TRUSTX also supports outstream video format that can be displayed in non-video placements. + +``` +var adUnits = [ + { + code: 'trustx-outstream-container', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[640, 360]], + mimes: ['video/mp4', 'video/webm'], + protocols: [2, 3, 5, 6], + api: [2, 7], + placement: 3, + minduration: 5, + maxduration: 30, + } + }, + bids: [ + { + bidder: 'trustx', + params: { + uid: 123456, + bidFloor: 6.0 + } + } + ] + } +] +``` + +# Test Mode + +By passing `bid.params.test = true` you will be able to receive a test creative without needing to set up real placements. + +## Banner + +``` +var adUnits = [ + { + code: 'trustx-test-banner', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600], [728, 90]] + } + }, + bids: [{ + bidder: 'trustx', + params: { + test: true + } + }] + } +]; +``` + +## Video + +``` +var adUnits = [ + { + code: 'trustx-test-video', + mediaTypes: { + video: { + context: "instream", + playerSize: [[640, 480]], + mimes: ['video/mp4', 'video/webm'], + protocols: [2, 3, 5, 6], + } + }, + bids: [ + { + bidder: 'trustx', + params: { + test: true + } + } + ] + } +] +``` + +# Optional Parameters + +## Bid Floor + +You can specify bid floor using any of these parameter names: +- `bidFloor` (camelCase) +- `bidfloor` (lowercase) +- `floorcpm` (alternative name) + +The adapter also supports Prebid's Floor Module via `getFloor()` function. The highest value between params and Floor Module will be used. + +``` +params: { + uid: 455069, + bidFloor: 0.5, // or bidfloor or floorcpm + currency: 'USD' // optional, defaults to USD +} +``` + +## Backward Compatibility with Grid Adapter + +The TRUSTX adapter is fully compatible with Grid adapter parameters for seamless migration: + +- `uid` - Same as Grid adapter (required) +- `secid` - Alternative to uid (required if uid not provided) +- `bidFloor` / `bidfloor` / `floorcpm` - Bid floor (optional) + +# First Party Data (FPD) Support + +The adapter automatically includes First Party Data from `ortb2` configuration: + +## Site FPD + +``` +pbjs.setConfig({ + ortb2: { + site: { + name: 'Example Site', + domain: 'example.com', + page: 'https://example.com/page', + cat: ['IAB12-1'], + content: { + data: [{ + name: 'reuters.com', + segment: [{id: '391'}, {id: '52'}] + }] + } + } + } +}); +``` + +## User FPD + +User IDs are passed through Prebid's User ID modules (e.g., SharedId) via `user.ext.eids`. + +## Device FPD + +Device data from `ortb2.device` is automatically included in requests. + +# User Sync + +The adapter supports server-side user sync. Sync URLs are extracted from server response (`ext.usersync`) and automatically registered with Prebid.js. + +``` +pbjs.setConfig({ + userSync: { + syncEnabled: true, + iframeEnabled: true, + pixelEnabled: true + } +}); +``` + +# Additional Configuration Options + +## GPP Support + +TRUSTX fully supports Global Privacy Platform (GPP) standards. GPP data is automatically passed from `bidderRequest.gppConsent` or `ortb2.regs.gpp`. + +## CCPA Support + +CCPA (US Privacy) data is automatically passed from `bidderRequest.uspConsent` or `ortb2.regs.ext.us_privacy`. + +## COPPA Support + +COPPA compliance is automatically handled when `config.getConfig('coppa') === true`. + +## DSA Support + +Digital Services Act (DSA) data is automatically passed from `ortb2.regs.ext.dsa`. + +## Supply Chain (schain) + +Supply chain data is automatically passed from `ortb2.source.ext.schain` or `bidRequest.schain`. + +## Source Transaction ID (tid) + +Transaction ID is automatically passed from `ortb2.source.tid`. + +# Response Format + +The adapter returns bids in standard Prebid.js format with the following additional fields: + +- `adserverTargeting.hb_ds` - Network name from server response (`ext.bidder.trustx.networkName`) +- `meta.demandSource` - Network name metadata (same as `networkName` from server) +- `netRevenue: false` - Revenue model \ No newline at end of file diff --git a/modules/uniquest_widgetBidAdapter.js b/modules/uniquest_widgetBidAdapter.js new file mode 100644 index 00000000000..f40c47b238c --- /dev/null +++ b/modules/uniquest_widgetBidAdapter.js @@ -0,0 +1,67 @@ +import {getBidIdParameter} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; +import {interpretResponse} from '../libraries/uniquestUtils/uniquestUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory').BidRequest} BidRequest + * @typedef {import('../src/auction').BidderRequest} BidderRequest + */ + +const BIDDER_CODE = 'uniquest_widget'; +const ENDPOINT = 'https://adpb.ust-ad.com/hb/prebid/widgets'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return !!(bid.params && bid.params.wid); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests} validBidRequests an array of bids + * @param {BidderRequest} bidderRequest + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + const bidRequests = []; + + for (let i = 0; i < validBidRequests.length; i++) { + let queryString = ''; + const request = validBidRequests[i]; + + const bid = request.bidId; + const wid = getBidIdParameter('wid', request.params); + const widths = request.sizes.map(size => size[0]).join(','); + const heights = request.sizes.map(size => size[1]).join(','); + const timeout = bidderRequest.timeout + + queryString = tryAppendQueryString(queryString, 'bid', bid); + queryString = tryAppendQueryString(queryString, 'wid', wid); + queryString = tryAppendQueryString(queryString, 'widths', widths); + queryString = tryAppendQueryString(queryString, 'heights', heights); + queryString = tryAppendQueryString(queryString, 'timeout', timeout); + + bidRequests.push({ + method: 'GET', + url: ENDPOINT, + data: queryString, + }); + } + return bidRequests; + }, + interpretResponse: interpretResponse, +}; + +registerBidder(spec); diff --git a/modules/uniquest_widgetBidAdapter.md b/modules/uniquest_widgetBidAdapter.md new file mode 100644 index 00000000000..7d8196d3b7b --- /dev/null +++ b/modules/uniquest_widgetBidAdapter.md @@ -0,0 +1,33 @@ +# Overview + +``` +Module Name: UNIQUEST Widget Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid_info@muneee.co.jp +``` + +# Description +Connects to UNIQUEST exchange for bids. + +# Test Parameters +```js +var adUnits = [ + // Banner adUnit + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [ + [1, 1], + ] + } + }, + bids: [{ + bidder: 'uniquest_widget', + params: { + wid: 'skDT3WYk', + } + }] + } +]; +``` diff --git a/modules/widespaceBidAdapter.js b/modules/widespaceBidAdapter.js index 9eb796571ca..7a8dec47a28 100644 --- a/modules/widespaceBidAdapter.js +++ b/modules/widespaceBidAdapter.js @@ -3,6 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {deepClone, parseQueryStringParameters, parseSizesInput} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; +import { getConnectionInfo } from '../libraries/connectionInfo/connectionUtils.js'; const BIDDER_CODE = 'widespace'; const WS_ADAPTER_VERSION = '2.0.1'; @@ -82,10 +83,10 @@ export const spec = { } // Include connection info if available - const CONNECTION = navigator.connection || navigator.webkitConnection; - if (CONNECTION && CONNECTION.type && CONNECTION.downlinkMax) { - data['netinfo.type'] = CONNECTION.type; - data['netinfo.downlinkMax'] = CONNECTION.downlinkMax; + const connection = getConnectionInfo(); + if (connection?.type && connection.downlinkMax != null) { + data['netinfo.type'] = connection.type; + data['netinfo.downlinkMax'] = connection.downlinkMax; } // Include debug data when available diff --git a/modules/wurflRtdProvider.js b/modules/wurflRtdProvider.js index d2e85f40ec7..dd88ea567eb 100644 --- a/modules/wurflRtdProvider.js +++ b/modules/wurflRtdProvider.js @@ -4,259 +4,1272 @@ import { loadExternalScript } from '../src/adloader.js'; import { mergeDeep, prefixLog, + debugTurnedOn, } from '../src/utils.js'; import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { getGlobal } from '../src/prebidGlobal.js'; // Constants const REAL_TIME_MODULE = 'realTimeData'; const MODULE_NAME = 'wurfl'; +const MODULE_VERSION = '2.4.0'; // WURFL_JS_HOST is the host for the WURFL service endpoints const WURFL_JS_HOST = 'https://prebid.wurflcloud.com'; // WURFL_JS_ENDPOINT_PATH is the path for the WURFL.js endpoint used to load WURFL data const WURFL_JS_ENDPOINT_PATH = '/wurfl.js'; +// STATS_HOST is the host for the WURFL stats endpoint +const STATS_HOST = 'https://stats.prebid.wurflcloud.com' // STATS_ENDPOINT_PATH is the path for the stats endpoint used to send analytics data -const STATS_ENDPOINT_PATH = '/v1/prebid/stats'; +const STATS_ENDPOINT_PATH = '/v2/prebid/stats'; + +// Storage keys for localStorage caching +const WURFL_RTD_STORAGE_KEY = 'wurflrtd'; + +// OpenRTB 2.0 device type constants +// Based on OpenRTB 2.6 specification +const ORTB2_DEVICE_TYPE = { + MOBILE_OR_TABLET: 1, + PERSONAL_COMPUTER: 2, + CONNECTED_TV: 3, + PHONE: 4, + TABLET: 5, + CONNECTED_DEVICE: 6, + SET_TOP_BOX: 7, + OOH_DEVICE: 8 +}; + +// OpenRTB 2.0 device fields that can be enriched from WURFL data +const ORTB2_DEVICE_FIELDS = [ + 'make', 'model', 'devicetype', 'os', 'osv', 'hwv', + 'h', 'w', 'ppi', 'pxratio', 'js' +]; + +// Enrichment type constants +const ENRICHMENT_TYPE = { + UNKNOWN: 'unknown', + NONE: 'none', + LCE: 'lce', + LCE_ERROR: 'lcefailed', + WURFL_PUB: 'wurfl_pub', + WURFL_SSP: 'wurfl_ssp' +}; + +// Consent class constants +const CONSENT_CLASS = { + NO: 0, // No consent/opt-out/COPPA + PARTIAL: 1, // Partial or ambiguous + FULL: 2, // Full consent or non-GDPR region + ERROR: -1 // Error computing consent +}; + +// Default sampling rate constant +const DEFAULT_SAMPLING_RATE = 100; + +// Default over quota constant +const DEFAULT_OVER_QUOTA = 0; + +// A/B test constants +const AB_TEST = { + CONTROL_GROUP: 'control', + TREATMENT_GROUP: 'treatment', + DEFAULT_SPLIT: 0.5, + DEFAULT_NAME: 'unknown', + ENRICHMENT_TYPE_LCE: 'lce', + ENRICHMENT_TYPE_WURFL: 'wurfl' +}; const logger = prefixLog('[WURFL RTD Submodule]'); -// enrichedBidders holds a list of prebid bidder names, of bidders which have been -// injected with WURFL data -const enrichedBidders = new Set(); +// Storage manager for WURFL RTD provider +export const storage = getStorageManager({ + moduleType: MODULE_TYPE_RTD, + moduleName: MODULE_NAME, +}); -/** - * init initializes the WURFL RTD submodule - * @param {Object} config Configuration for WURFL RTD submodule - * @param {Object} userConsent User consent data - */ -const init = (config, userConsent) => { - logger.logMessage('initialized'); - return true; -} +// bidderEnrichment maps bidder codes to their enrichment type for beacon reporting +let bidderEnrichment; -/** - * getBidRequestData enriches the OpenRTB 2.0 device data with WURFL data - * @param {Object} reqBidsConfigObj Bid request configuration object - * @param {Function} callback Called on completion - * @param {Object} config Configuration for WURFL RTD submodule - * @param {Object} userConsent User consent data - */ -const getBidRequestData = (reqBidsConfigObj, callback, config, userConsent) => { - const altHost = config.params?.altHost ?? null; - const isDebug = config.params?.debug ?? false; +// enrichmentType tracks the overall enrichment type used in the current auction +let enrichmentType; - const bidders = new Set(); - reqBidsConfigObj.adUnits.forEach(adUnit => { - adUnit.bids.forEach(bid => { - bidders.add(bid.bidder); - }); - }); +// wurflId stores the WURFL ID from device data +let wurflId; - let host = WURFL_JS_HOST; - if (altHost) { - host = altHost; - } +// samplingRate tracks the beacon sampling rate (0-100) +let samplingRate; - const url = new URL(host); - url.pathname = WURFL_JS_ENDPOINT_PATH; +// tier stores the WURFL tier from wurfl_pbjs data +let tier; - if (isDebug) { - url.searchParams.set('debug', 'true') +// overQuota stores the over_quota flag from wurfl_pbjs data (possible values: 0, 1) +let overQuota; + +/** + * Safely gets an object from localStorage with JSON parsing + * @param {string} key The storage key + * @returns {Object|null} Parsed object or null if not found/invalid + */ +function getObjectFromStorage(key) { + if (!storage.hasLocalStorage() || !storage.localStorageIsEnabled()) { + return null; } - url.searchParams.set('mode', 'prebid') - url.searchParams.set('wurfl_id', 'true') + try { + const dataStr = storage.getDataFromLocalStorage(key); + return dataStr ? JSON.parse(dataStr) : null; + } catch (e) { + logger.logError(`Error parsing stored data for key ${key}:`, e); + return null; + } +} + +/** + * Safely sets an object to localStorage with JSON stringification + * @param {string} key The storage key + * @param {Object} data The data to store + * @returns {boolean} Success status + */ +function setObjectToStorage(key, data) { + if (!storage.hasLocalStorage() || !storage.localStorageIsEnabled()) { + return false; + } try { - loadExternalScript(url.toString(), MODULE_TYPE_RTD, MODULE_NAME, () => { - logger.logMessage('script injected'); - window.WURFLPromises.complete.then((res) => { - logger.logMessage('received data', res); - if (!res.wurfl_pbjs) { - logger.logError('invalid WURFL.js for Prebid response'); - } else { - enrichBidderRequests(reqBidsConfigObj, bidders, res); - } - callback(); - }); - }); - } catch (err) { - logger.logError(err); - callback(); + storage.setDataInLocalStorage(key, JSON.stringify(data)); + return true; + } catch (e) { + logger.logError(`Error storing data for key ${key}:`, e); + return false; } } /** - * enrichBidderRequests enriches the OpenRTB 2.0 device data with WURFL data for Business Edition + * enrichDeviceFPD enriches the global device object with device data * @param {Object} reqBidsConfigObj Bid request configuration object - * @param {Array} bidders List of bidders - * @param {Object} wjsResponse WURFL.js response + * @param {Object} deviceData Device data to enrich with */ -function enrichBidderRequests(reqBidsConfigObj, bidders, wjsResponse) { - const authBidders = wjsResponse.wurfl_pbjs?.authorized_bidders ?? {}; - const caps = wjsResponse.wurfl_pbjs?.caps ?? []; +function enrichDeviceFPD(reqBidsConfigObj, deviceData) { + if (!deviceData || !reqBidsConfigObj?.ortb2Fragments?.global) { + return; + } - bidders.forEach((bidderCode) => { - if (bidderCode in authBidders) { - // inject WURFL data - enrichedBidders.add(bidderCode); - const data = bidderData(wjsResponse.WURFL, caps, authBidders[bidderCode]); - data['enrich_device'] = true; - enrichBidderRequest(reqBidsConfigObj, bidderCode, data); + const prebidDevice = reqBidsConfigObj.ortb2Fragments.global.device || {}; + const enrichedDevice = {}; + + ORTB2_DEVICE_FIELDS.forEach(field => { + // Check if field already exists in prebid device + if (prebidDevice[field] !== undefined) { return; } - // inject WURFL low entropy data - const data = lowEntropyData(wjsResponse.WURFL, wjsResponse.wurfl_pbjs?.low_entropy_caps); - enrichBidderRequest(reqBidsConfigObj, bidderCode, data); + + // Check if deviceData has a valid value for this field + if (deviceData[field] === undefined) { + return; + } + + // Copy the field value from deviceData to enrichedDevice + enrichedDevice[field] = deviceData[field]; }); + + // Also copy ext field if present (contains ext.wurfl capabilities) + if (deviceData.ext) { + enrichedDevice.ext = deviceData.ext; + } + + // Use mergeDeep to properly merge into global device + mergeDeep(reqBidsConfigObj.ortb2Fragments.global, { device: enrichedDevice }); } /** - * bidderData returns the WURFL data for a bidder - * @param {Object} wurflData WURFL data - * @param {Array} caps Capability list - * @param {Array} filter Filter list - * @returns {Object} Bidder data + * enrichDeviceBidder enriches bidder-specific device data with WURFL data + * @param {Object} reqBidsConfigObj Bid request configuration object + * @param {Set} bidders Set of bidder codes + * @param {WurflJSDevice} wjsDevice WURFL.js device data with permissions and caps */ -export const bidderData = (wurflData, caps, filter) => { - const data = {}; - if ('wurfl_id' in wurflData) { - data['wurfl_id'] = wurflData.wurfl_id; +function enrichDeviceBidder(reqBidsConfigObj, bidders, wjsDevice) { + // Initialize bidder fragments if not present + if (!reqBidsConfigObj.ortb2Fragments.bidder) { + reqBidsConfigObj.ortb2Fragments.bidder = {}; } - caps.forEach((cap, index) => { - if (!filter.includes(index)) { + + const isOverQuota = wjsDevice._isOverQuota(); + + bidders.forEach((bidderCode) => { + const isAuthorized = wjsDevice._isAuthorized(bidderCode); + + if (!isAuthorized) { + // Over quota + unauthorized -> NO ENRICHMENT + if (isOverQuota) { + bidderEnrichment.set(bidderCode, ENRICHMENT_TYPE.NONE); + return; + } + // Under quota + unauthorized -> inherits from global no bidder enrichment + bidderEnrichment.set(bidderCode, ENRICHMENT_TYPE.WURFL_PUB); return; } - if (cap in wurflData) { - data[cap] = wurflData[cap]; + + // From here: bidder IS authorized + const bidderDevice = wjsDevice.Bidder(bidderCode); + bidderEnrichment.set(bidderCode, ENRICHMENT_TYPE.WURFL_SSP); + + // Edge case: authorized but no data (e.g., missing caps) + if (Object.keys(bidderDevice).length === 0) { + return; } + + // Authorized bidder with data to inject + const bd = reqBidsConfigObj.ortb2Fragments.bidder[bidderCode] || {}; + mergeDeep(bd, bidderDevice); + reqBidsConfigObj.ortb2Fragments.bidder[bidderCode] = bd; }); - return data; } /** - * lowEntropyData returns the WURFL low entropy data - * @param {Object} wurflData WURFL data - * @param {Array} lowEntropyCaps Low entropy capability list - * @returns {Object} Bidder data + * loadWurflJsAsync loads WURFL.js asynchronously and stores response to localStorage + * @param {Object} config Configuration for WURFL RTD submodule + * @param {Set} bidders Set of bidder codes */ -export const lowEntropyData = (wurflData, lowEntropyCaps) => { - const data = {}; - lowEntropyCaps.forEach((cap, _) => { - let value = wurflData[cap]; - if (cap === 'complete_device_name') { - value = value.replace(/Apple (iP(hone|ad|od)).*/, 'Apple iP$2'); - } - data[cap] = value; - }); - if ('model_name' in wurflData) { - data['model_name'] = wurflData.model_name.replace(/(iP(hone|ad|od)).*/, 'iP$2'); +function loadWurflJsAsync(config, bidders) { + const altHost = config.params?.altHost ?? null; + const isDebug = debugTurnedOn(); + + let host = WURFL_JS_HOST; + if (altHost) { + host = altHost; } - if ('brand_name' in wurflData) { - data['brand_name'] = wurflData.brand_name; + + const url = new URL(host); + url.pathname = WURFL_JS_ENDPOINT_PATH; + + // Start timing WURFL.js load + WurflDebugger.wurflJsLoadStart(); + + if (isDebug) { + url.searchParams.set('debug', 'true'); } - if ('wurfl_id' in wurflData) { - data['wurfl_id'] = wurflData.wurfl_id; + + url.searchParams.set('mode', 'prebid2'); + + // Add bidders list for server optimization + if (bidders && bidders.size > 0) { + url.searchParams.set('bidders', Array.from(bidders).join(',')); + } + + // Helper function to load WURFL.js script + const loadWurflJs = (scriptUrl) => { + try { + loadExternalScript(scriptUrl, MODULE_TYPE_RTD, MODULE_NAME, () => { + window.WURFLPromises.complete.then((res) => { + logger.logMessage('async WURFL.js data received', res); + if (res.wurfl_pbjs) { + // Create optimized cache object with only relevant device data + WurflDebugger.cacheWriteStart(); + const cacheData = { + WURFL: res.WURFL, + wurfl_pbjs: res.wurfl_pbjs, + expire_at: Date.now() + (res.wurfl_pbjs.ttl * 1000) + }; + setObjectToStorage(WURFL_RTD_STORAGE_KEY, cacheData); + WurflDebugger.cacheWriteStop(); + logger.logMessage('WURFL.js device cache stored to localStorage'); + } else { + logger.logError('invalid async WURFL.js for Prebid response'); + } + }).catch((err) => { + logger.logError('async WURFL.js promise error:', err); + }); + }); + } catch (err) { + logger.logError('async WURFL.js loading error:', err); + } + }; + + // Collect Client Hints if available, then load script + if (navigator?.userAgentData?.getHighEntropyValues) { + const hints = ['architecture', 'bitness', 'model', 'platformVersion', 'uaFullVersion', 'fullVersionList']; + navigator.userAgentData.getHighEntropyValues(hints) + .then(ch => { + if (ch !== null) { + url.searchParams.set('uach', JSON.stringify(ch)); + } + }) + .finally(() => { + loadWurflJs(url.toString()); + }); + } else { + // Load script immediately when Client Hints not available + loadWurflJs(url.toString()); } - return data; } + /** - * enrichBidderRequest enriches the bidder request with WURFL data - * @param {Object} reqBidsConfigObj Bid request configuration object - * @param {String} bidderCode Bidder code - * @param {Object} wurflData WURFL data + * shouldSample determines if an action should be taken based on sampling rate + * @param {number} rate Sampling rate from 0-100 (percentage) + * @returns {boolean} True if should proceed, false if should skip */ -export const enrichBidderRequest = (reqBidsConfigObj, bidderCode, wurflData) => { - const ortb2data = { - 'device': { - 'ext': {}, - }, - }; - - const device = reqBidsConfigObj.ortb2Fragments.global.device; - enrichOrtb2DeviceData('make', wurflData.brand_name, device, ortb2data); - enrichOrtb2DeviceData('model', wurflData.model_name, device, ortb2data); - if (wurflData.enrich_device) { - delete wurflData.enrich_device; - enrichOrtb2DeviceData('devicetype', makeOrtb2DeviceType(wurflData), device, ortb2data); - enrichOrtb2DeviceData('os', wurflData.advertised_device_os, device, ortb2data); - enrichOrtb2DeviceData('osv', wurflData.advertised_device_os_version, device, ortb2data); - enrichOrtb2DeviceData('hwv', wurflData.model_name, device, ortb2data); - enrichOrtb2DeviceData('h', wurflData.resolution_height, device, ortb2data); - enrichOrtb2DeviceData('w', wurflData.resolution_width, device, ortb2data); - enrichOrtb2DeviceData('ppi', wurflData.pixel_density, device, ortb2data); - enrichOrtb2DeviceData('pxratio', toNumber(wurflData.density_class), device, ortb2data); - enrichOrtb2DeviceData('js', toNumber(wurflData.ajax_support_javascript), device, ortb2data); - } - ortb2data.device.ext['wurfl'] = wurflData - mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, { [bidderCode]: ortb2data }); +function shouldSample(rate) { + if (rate >= 100) { + return true; + } + if (rate <= 0) { + return false; + } + const randomValue = Math.floor(Math.random() * 100); + return randomValue < rate; } /** - * makeOrtb2DeviceType returns the ortb2 device type based on WURFL data - * @param {Object} wurflData WURFL data - * @returns {Number} ortb2 device type - * @see https://www.scientiamobile.com/how-to-populate-iab-openrtb-device-object/ + * getConsentClass calculates the consent classification level + * @param {Object} userConsent User consent data + * @returns {number} Consent class (0, 1, or 2) */ -export function makeOrtb2DeviceType(wurflData) { - if (wurflData.is_mobile) { - if (!('is_phone' in wurflData) || !('is_tablet' in wurflData)) { - return undefined; - } - if (wurflData.is_phone || wurflData.is_tablet) { - return 1; +function getConsentClass(userConsent) { + // Default to no consent if userConsent is not provided or is an empty object + if (!userConsent || Object.keys(userConsent).length === 0) { + return CONSENT_CLASS.NO; + } + + // Check COPPA (Children's Privacy) + if (userConsent.coppa === true) { + return CONSENT_CLASS.NO; + } + + // Check USP/CCPA (US Privacy) + if (userConsent.usp && typeof userConsent.usp === 'string') { + if (userConsent.usp.substring(0, 2) === '1Y') { + return CONSENT_CLASS.NO; } - return 6; } - if (wurflData.is_full_desktop) { - return 2; + + // Check GDPR object exists + if (!userConsent.gdpr) { + return CONSENT_CLASS.FULL; // No GDPR data means not applicable + } + + // Check GDPR applicability - Note: might be in vendorData + const gdprApplies = userConsent.gdpr.gdprApplies === true || userConsent.gdpr.vendorData?.gdprApplies === true; + + if (!gdprApplies) { + return CONSENT_CLASS.FULL; + } + + // GDPR applies - evaluate purposes + const vendorData = userConsent.gdpr.vendorData; + + if (!vendorData || !vendorData.purpose) { + return CONSENT_CLASS.NO; + } + + const purposes = vendorData.purpose; + const consents = purposes.consents || {}; + const legitimateInterests = purposes.legitimateInterests || {}; + + // Count allowed purposes (7, 8, 10) + let allowedCount = 0; + + // Purpose 7: Measure ad performance + if (consents['7'] === true || legitimateInterests['7'] === true) { + allowedCount++; } - if (wurflData.is_connected_tv) { - return 3; + + // Purpose 8: Market research + if (consents['8'] === true || legitimateInterests['8'] === true) { + allowedCount++; } - if (wurflData.is_phone) { - return 4; + + // Purpose 10: Develop/improve products + if (consents['10'] === true || legitimateInterests['10'] === true) { + allowedCount++; } - if (wurflData.is_tablet) { - return 5; + + // Classify based on allowed purposes count + if (allowedCount === 0) { + return CONSENT_CLASS.NO; } - if (wurflData.is_ott) { - return 7; + if (allowedCount === 3) { + return CONSENT_CLASS.FULL; } - return undefined; + return CONSENT_CLASS.PARTIAL; +} + +// ==================== CLASSES ==================== + +// WurflDebugger object for performance tracking and debugging +const WurflDebugger = { + // Private timing start values + _moduleExecutionStart: null, + _cacheReadStart: null, + _lceDetectionStart: null, + _cacheWriteStart: null, + _wurflJsLoadStart: null, + + // Initialize WURFL debug tracking + init() { + if (!debugTurnedOn()) { + // Replace all methods (except init) with no-ops for zero overhead + Object.keys(this).forEach(key => { + if (typeof this[key] === 'function' && key !== 'init') { + this[key] = () => { }; + } + }); + return; + } + + // Full debug mode - create/reset window object for tracking + if (typeof window !== 'undefined') { + window.WurflRtdDebug = { + // Module version + version: MODULE_VERSION, + + // Prebid.js version + pbjsVersion: getGlobal().version, + + // Data source for current auction + dataSource: 'unknown', // 'cache' | 'lce' + + // Cache state + cacheExpired: false, // Whether the cache was expired when used + + // Simple timing measurements + moduleExecutionTime: null, // Total time from getBidRequestData start to callback + cacheReadTime: null, // Single cache read time (hit or miss) + lceDetectionTime: null, // LCE detection time (only if dataSource = 'lce') + cacheWriteTime: null, // Async cache write time (for future auctions) + wurflJsLoadTime: null, // Total time from WURFL.js load start to cache complete + + // The actual data used in current auction + data: { + // When dataSource = 'cache' + wurflData: null, // The cached WURFL device data + pbjsData: null, // The cached wurfl_pbjs data + + // When dataSource = 'lce' + lceDevice: null // The LCE-generated device object + }, + + // Beacon payload sent to analytics endpoint + beaconPayload: null + }; + } + }, + + // Module execution timing methods + moduleExecutionStart() { + this._moduleExecutionStart = performance.now(); + }, + + moduleExecutionStop() { + if (this._moduleExecutionStart === null) return; + const duration = performance.now() - this._moduleExecutionStart; + window.WurflRtdDebug.moduleExecutionTime = duration; + this._moduleExecutionStart = null; + }, + + // Cache read timing methods + cacheReadStart() { + this._cacheReadStart = performance.now(); + }, + + cacheReadStop() { + if (this._cacheReadStart === null) return; + const duration = performance.now() - this._cacheReadStart; + window.WurflRtdDebug.cacheReadTime = duration; + this._cacheReadStart = null; + }, + + // LCE detection timing methods + lceDetectionStart() { + this._lceDetectionStart = performance.now(); + }, + + lceDetectionStop() { + if (this._lceDetectionStart === null) return; + const duration = performance.now() - this._lceDetectionStart; + window.WurflRtdDebug.lceDetectionTime = duration; + this._lceDetectionStart = null; + }, + + // Cache write timing methods + cacheWriteStart() { + this._cacheWriteStart = performance.now(); + }, + + cacheWriteStop() { + if (this._cacheWriteStart === null) return; + const duration = performance.now() - this._cacheWriteStart; + window.WurflRtdDebug.cacheWriteTime = duration; + this._cacheWriteStart = null; + + // Calculate total WURFL.js load time (from load start to cache complete) + if (this._wurflJsLoadStart !== null) { + const totalLoadTime = performance.now() - this._wurflJsLoadStart; + window.WurflRtdDebug.wurflJsLoadTime = totalLoadTime; + this._wurflJsLoadStart = null; + } + + // Dispatch custom event when cache write data is available + if (typeof window !== 'undefined' && window.dispatchEvent) { + const event = new CustomEvent('wurflCacheWriteComplete', { + detail: { + duration: duration, + timestamp: Date.now(), + debugData: window.WurflRtdDebug + } + }); + window.dispatchEvent(event); + } + }, + + // WURFL.js load timing methods + wurflJsLoadStart() { + this._wurflJsLoadStart = performance.now(); + }, + + // Data tracking methods + setDataSource(source) { + window.WurflRtdDebug.dataSource = source; + }, + + setCacheData(wurflData, pbjsData) { + window.WurflRtdDebug.data.wurflData = wurflData; + window.WurflRtdDebug.data.pbjsData = pbjsData; + }, + + setLceData(lceDevice) { + window.WurflRtdDebug.data.lceDevice = lceDevice; + }, + + setCacheExpired(expired) { + window.WurflRtdDebug.cacheExpired = expired; + }, + + setBeaconPayload(payload) { + window.WurflRtdDebug.beaconPayload = payload; + } +}; + +// ==================== WURFL JS DEVICE MODULE ==================== +const WurflJSDevice = { + // Private properties + _wurflData: null, // WURFL data containing capability values (from window.WURFL) + _pbjsData: null, // wurfl_pbjs data with caps array and permissions (from response) + _basicCaps: null, // Cached basic capabilities (computed once) + _pubCaps: null, // Cached publisher capabilities (computed once) + _device: null, // Cached device object (computed once) + + // Constructor from WURFL.js local cache + fromCache(res) { + this._wurflData = res.WURFL || {}; + this._pbjsData = res.wurfl_pbjs || {}; + this._basicCaps = null; + this._pubCaps = null; + this._device = null; + return this; + }, + + // Private method - converts a given value to a number + _toNumber(value) { + if (value === '' || value === null) { + return undefined; + } + const num = Number(value); + return Number.isNaN(num) ? undefined : num; + }, + + // Private method - filters capabilities based on indices + _filterCaps(indexes) { + const data = {}; + const caps = this._pbjsData.caps; // Array of capability names + const wurflData = this._wurflData; // WURFL data containing capability values + + if (!indexes || !caps || !wurflData) { + return data; + } + + indexes.forEach((index) => { + const capName = caps[index]; // Get capability name by index + if (capName && capName in wurflData) { + data[capName] = wurflData[capName]; // Get value from WURFL data + } + }); + + return data; + }, + + // Private method - gets basic capabilities + _getBasicCaps() { + if (this._basicCaps !== null) { + return this._basicCaps; + } + const basicCaps = this._pbjsData.global?.basic_set?.cap_indices || []; + this._basicCaps = this._filterCaps(basicCaps); + return this._basicCaps; + }, + + // Private method - gets publisher capabilities + _getPubCaps() { + if (this._pubCaps !== null) { + return this._pubCaps; + } + const pubCaps = this._pbjsData.global?.publisher?.cap_indices || []; + this._pubCaps = this._filterCaps(pubCaps); + return this._pubCaps; + }, + + // Private method - gets bidder-specific capabilities + _getBidderCaps(bidderCode) { + const bidderCaps = this._pbjsData.bidders?.[bidderCode]?.cap_indices || []; + return this._filterCaps(bidderCaps); + }, + + // Private method - checks if bidder is authorized + _isAuthorized(bidderCode) { + return !!(this._pbjsData.bidders && bidderCode in this._pbjsData.bidders); + }, + + // Private method - checks if over quota + _isOverQuota() { + return this._pbjsData.over_quota === 1; + }, + + // Private method - returns the ortb2 device type based on WURFL data + _makeOrtb2DeviceType(wurflData) { + if (('is_ott' in wurflData) && (wurflData.is_ott)) { + return ORTB2_DEVICE_TYPE.SET_TOP_BOX; + } + if (('is_console' in wurflData) && (wurflData.is_console)) { + return ORTB2_DEVICE_TYPE.CONNECTED_DEVICE; + } + if (('physical_form_factor' in wurflData) && (wurflData.physical_form_factor === 'out_of_home_device')) { + return ORTB2_DEVICE_TYPE.OOH_DEVICE; + } + if (!('form_factor' in wurflData)) { + return undefined; + } + switch (wurflData.form_factor) { + case 'Desktop': + return ORTB2_DEVICE_TYPE.PERSONAL_COMPUTER; + case 'Smartphone': + return ORTB2_DEVICE_TYPE.PHONE; + case 'Feature Phone': + return ORTB2_DEVICE_TYPE.PHONE; + case 'Tablet': + return ORTB2_DEVICE_TYPE.TABLET; + case 'Smart-TV': + return ORTB2_DEVICE_TYPE.CONNECTED_TV; + case 'Other Non-Mobile': + return ORTB2_DEVICE_TYPE.CONNECTED_DEVICE; + case 'Other Mobile': + return ORTB2_DEVICE_TYPE.MOBILE_OR_TABLET; + default: + return undefined; + } + }, + + // Public API - returns device object for First Party Data (global) + // When under quota: returns device fields + ext.wurfl(basic+pub) + // When over quota: returns device fields only + FPD() { + if (this._device !== null) { + return this._device; + } + + const wd = this._wurflData; + if (!wd) { + this._device = {}; + return this._device; + } + + this._device = { + make: wd.brand_name, + model: wd.model_name, + devicetype: this._makeOrtb2DeviceType(wd), + os: wd.advertised_device_os, + osv: wd.advertised_device_os_version, + hwv: wd.model_name, + h: wd.resolution_height, + w: wd.resolution_width, + ppi: wd.pixel_density, + pxratio: this._toNumber(wd.density_class), + js: this._toNumber(wd.ajax_support_javascript) + }; + + const isOverQuota = this._isOverQuota(); + if (!isOverQuota) { + const basicCaps = this._getBasicCaps(); + const pubCaps = this._getPubCaps(); + this._device.ext = { + wurfl: { + ...basicCaps, + ...pubCaps + } + }; + } + + return this._device; + }, + + // Public API - returns device with bidder-specific ext data + Bidder(bidderCode) { + const isAuthorized = this._isAuthorized(bidderCode); + const isOverQuota = this._isOverQuota(); + + // When unauthorized return empty + if (!isAuthorized) { + return {}; + } + + // Start with empty device, populate only if publisher is over quota + // When over quota, we send device data to each authorized bidder individually + let fpdDevice = {}; + if (isOverQuota) { + fpdDevice = this.FPD(); + } + + if (!this._pbjsData.caps) { + return { device: fpdDevice }; + } + + // For authorized bidders: basic + pub + bidder-specific caps + const wurflData = { + ...(isOverQuota ? this._getBasicCaps() : {}), + ...this._getBidderCaps(bidderCode) + }; + + return { + device: { + ...fpdDevice, + ext: { + wurfl: wurflData + } + } + }; + } +}; +// ==================== END WURFL JS DEVICE MODULE ==================== + +// ==================== WURFL LCE DEVICE MODULE ==================== +const WurflLCEDevice = { + // Private mappings for device detection + _desktopMapping: new Map([ + ['Windows NT', 'Windows'], + ['Macintosh; Intel Mac OS X', 'macOS'], + ['Mozilla/5.0 (X11; Linux', 'Linux'], + ['X11; Ubuntu; Linux x86_64', 'Linux'], + ['Mozilla/5.0 (X11; CrOS', 'ChromeOS'], + ]), + + _tabletMapping: new Map([ + ['iPad; CPU OS ', 'iPadOS'], + ]), + + _smartphoneMapping: new Map([ + ['Android', 'Android'], + ['iPhone; CPU iPhone OS', 'iOS'], + ]), + + _smarttvMapping: new Map([ + ['Web0S', 'LG webOS'], + ['SMART-TV; Linux; Tizen', 'Tizen'], + ]), + + _ottMapping: new Map([ + ['Roku', 'Roku OS'], + ['Xbox', 'Windows'], + ['PLAYSTATION', 'PlayStation OS'], + ['PlayStation', 'PlayStation OS'], + ]), + + _makeMapping: new Map([ + ['motorola', 'Motorola'], + [' moto ', 'Motorola'], + ['Android', 'Generic'], + ['iPad', 'Apple'], + ['iPhone', 'Apple'], + ['Firefox', 'Mozilla'], + ['Edge', 'Microsoft'], + ['Chrome', 'Google'], + ]), + + _modelMapping: new Map([ + ['Android', 'Android'], + ['iPad', 'iPad'], + ['iPhone', 'iPhone'], + ['Firefox', 'Firefox'], + ['Edge', 'Edge'], + ['Chrome', 'Chrome'], + ]), + + // Private helper methods + _parseOsVersion(ua, osName) { + switch (osName) { + case 'Windows': { + const matches = ua.match(/Windows NT ([\d.]+)/); + return matches ? matches[1] : ''; + } + case 'macOS': { + const matches = ua.match(/Mac OS X ([\d_]+)/); + return matches ? matches[1].replace(/_/g, '.') : ''; + } + case 'iOS': { + // iOS 26 specific logic + const matches1 = ua.match(/iPhone; CPU iPhone OS 18[\d_]+ like Mac OS X\).+(?:Version|FBSV)\/(26[\d.]+)/); + if (matches1) { + return matches1[1]; + } + // iOS 18.x and lower + const matches2 = ua.match(/iPhone; CPU iPhone OS ([\d_]+) like Mac OS X/); + return matches2 ? matches2[1].replace(/_/g, '.') : ''; + } + case 'iPadOS': { + // iOS 26 specific logic + const matches1 = ua.match(/iPad; CPU OS 18[\d_]+ like Mac OS X\).+(?:Version|FBSV)\/(26[\d.]+)/); + if (matches1) { + return matches1[1]; + } + // iOS 18.x and lower + const matches2 = ua.match(/iPad; CPU OS ([\d_]+) like Mac OS X/); + return matches2 ? matches2[1].replace(/_/g, '.') : ''; + } + case 'Android': { + // For Android UAs with a decimal + const matches1 = ua.match(/Android ([\d.]+)/); + if (matches1) { + return matches1[1]; + } + // For Android UAs without a decimal + const matches2 = ua.match(/Android ([\d]+)/); + return matches2 ? matches2[1] : ''; + } + case 'ChromeOS': { + const matches = ua.match(/CrOS x86_64 ([\d.]+)/); + return matches ? matches[1] : ''; + } + case 'Tizen': { + const matches = ua.match(/Tizen ([\d.]+)/); + return matches ? matches[1] : ''; + } + case 'Roku OS': { + const matches = ua.match(/Roku\/DVP [\dA-Z]+ [\d.]+\/([\d.]+)/); + return matches ? matches[1] : ''; + } + case 'PlayStation OS': { + // PS4 + const matches1 = ua.match(/PlayStation \d\/([\d.]+)/); + if (matches1) { + return matches1[1]; + } + // PS3 + const matches2 = ua.match(/PLAYSTATION \d ([\d.]+)/); + return matches2 ? matches2[1] : ''; + } + case 'Linux': + case 'LG webOS': + default: + return ''; + } + }, + + _makeDeviceInfo(deviceType, osName, ua) { + return { deviceType, osName, osVersion: this._parseOsVersion(ua, osName) }; + }, + + _getDeviceInfo(ua) { + // Iterate over ottMapping + // Should remove above Desktop + for (const [osToken, osName] of this._ottMapping) { + if (ua.includes(osToken)) { + return this._makeDeviceInfo(ORTB2_DEVICE_TYPE.SET_TOP_BOX, osName, ua); + } + } + // Iterate over desktopMapping + for (const [osToken, osName] of this._desktopMapping) { + if (ua.includes(osToken)) { + return this._makeDeviceInfo(ORTB2_DEVICE_TYPE.PERSONAL_COMPUTER, osName, ua); + } + } + // Iterate over tabletMapping + for (const [osToken, osName] of this._tabletMapping) { + if (ua.includes(osToken)) { + return this._makeDeviceInfo(ORTB2_DEVICE_TYPE.TABLET, osName, ua); + } + } + // Android Tablets + if (ua.includes('Android') && !ua.includes('Mobile Safari') && ua.includes('Safari')) { + return this._makeDeviceInfo(ORTB2_DEVICE_TYPE.TABLET, 'Android', ua); + } + // Iterate over smartphoneMapping + for (const [osToken, osName] of this._smartphoneMapping) { + if (ua.includes(osToken)) { + return this._makeDeviceInfo(ORTB2_DEVICE_TYPE.PHONE, osName, ua); + } + } + // Iterate over smarttvMapping + for (const [osToken, osName] of this._smarttvMapping) { + if (ua.includes(osToken)) { + return this._makeDeviceInfo(ORTB2_DEVICE_TYPE.CONNECTED_TV, osName, ua); + } + } + return { deviceType: '', osName: '', osVersion: '' }; + }, + + _getDevicePixelRatioValue(osName) { + switch (osName) { + case 'Android': + return 2.0; + case 'iOS': + return 3.0; + case 'iPadOS': + return 2.0; + default: + return 1.0; + } + }, + + _getMake(ua) { + for (const [makeToken, brandName] of this._makeMapping) { + if (ua.includes(makeToken)) { + return brandName; + } + } + return 'Generic'; + }, + + _getModel(ua) { + for (const [modelToken, modelName] of this._modelMapping) { + if (ua.includes(modelToken)) { + return modelName; + } + } + return ''; + }, + + _getUserAgent() { + return window.navigator?.userAgent || ''; + }, + + _isRobot(useragent) { + const botTokens = ['+http', 'Googlebot', 'BingPreview', 'Yahoo! Slurp']; + for (const botToken of botTokens) { + if (useragent.includes(botToken)) { + return true; + } + } + return false; + }, + + // Public API - returns device object for First Party Data (global) + FPD() { + // Early exit - check window exists + if (typeof window === 'undefined') { + return { js: 1 }; + } + + const device = { js: 1 }; + const useragent = this._getUserAgent(); + + // Only process UA-dependent properties if we have a UA + if (useragent) { + // Get device info + const deviceInfo = this._getDeviceInfo(useragent); + if (deviceInfo.deviceType !== undefined) { + device.devicetype = deviceInfo.deviceType; + } + if (deviceInfo.osName !== undefined) { + device.os = deviceInfo.osName; + } + if (deviceInfo.osVersion !== undefined) { + device.osv = deviceInfo.osVersion; + } + + // Make/model + const make = this._getMake(useragent); + if (make !== undefined) { + device.make = make; + } + + const model = this._getModel(useragent); + if (model !== undefined) { + device.model = model; + device.hwv = model; + } + + // Device pixel ratio based on OS + const pixelRatio = this._getDevicePixelRatioValue(deviceInfo.osName); + if (pixelRatio !== undefined) { + device.pxratio = pixelRatio; + } + } + + // Add ext.wurfl with is_robot detection + if (useragent) { + device.ext = { + wurfl: { + is_robot: this._isRobot(useragent) + } + }; + } + + return device; + } +}; +// ==================== END WURFL LCE DEVICE MODULE ==================== + +// ==================== A/B TEST MANAGER ==================== + +const ABTestManager = { + _enabled: false, + _name: null, + _variant: null, + _excludeLCE: true, + _enrichmentType: null, + + /** + * Initializes A/B test configuration + * @param {Object} params Configuration params from config.params + */ + init(params) { + this._enabled = false; + this._name = null; + this._variant = null; + this._excludeLCE = true; + this._enrichmentType = null; + + const abTestEnabled = params?.abTest ?? false; + if (!abTestEnabled) { + return; + } + + this._enabled = true; + this._name = params?.abName ?? AB_TEST.DEFAULT_NAME; + this._excludeLCE = params?.abExcludeLCE ?? true; + + const split = params?.abSplit ?? AB_TEST.DEFAULT_SPLIT; + this._variant = this._computeVariant(split); + + logger.logMessage(`A/B test "${this._name}": user in ${this._variant} group (exclude_lce: ${this._excludeLCE})`); + }, + + /** + * _computeVariant determines A/B test variant assignment based on split + * @param {number} split Treatment group split from 0-1 (float, e.g., 0.5 = 50% treatment) + * @returns {string} AB_TEST.TREATMENT_GROUP or AB_TEST.CONTROL_GROUP + */ + _computeVariant(split) { + if (split >= 1) { + return AB_TEST.TREATMENT_GROUP; + } + if (split <= 0) { + return AB_TEST.CONTROL_GROUP; + } + return Math.random() < split ? AB_TEST.TREATMENT_GROUP : AB_TEST.CONTROL_GROUP; + }, + + /** + * Sets the enrichment type encountered in current auction + * @param {string} enrichmentType 'lce' or 'wurfl' + */ + setEnrichmentType(enrichmentType) { + this._enrichmentType = enrichmentType; + }, + + /** + * Checks if A/B test is enabled for current auction + * @returns {boolean} True if A/B test should be applied + */ + isEnabled() { + if (!this._enabled) return false; + if (this._enrichmentType === AB_TEST.ENRICHMENT_TYPE_LCE && this._excludeLCE) { + return false; + } + return true; + }, + + /** + * Checks if enrichment should be skipped (control group) + * @returns {boolean} True if enrichment should be skipped + */ + isInControlGroup() { + if (!this.isEnabled()) { + return false; + } + return (this._variant === AB_TEST.CONTROL_GROUP) + }, + + /** + * Gets beacon payload fields (returns null if not active for auction) + * @returns {Object|null} + */ + getBeaconPayload() { + if (!this.isEnabled()) { + return null; + } + + return { + ab_name: this._name, + ab_variant: this._variant + }; + } +}; + +// ==================== END A/B TEST MANAGER MODULE ==================== + +// ==================== EXPORTED FUNCTIONS ==================== + +/** + * init initializes the WURFL RTD submodule + * @param {Object} config Configuration for WURFL RTD submodule + * @param {Object} userConsent User consent data + */ +const init = (config, userConsent) => { + // Initialize debugger based on global debug flag + WurflDebugger.init(); + + // Initialize module state + bidderEnrichment = new Map(); + enrichmentType = ENRICHMENT_TYPE.UNKNOWN; + wurflId = ''; + samplingRate = DEFAULT_SAMPLING_RATE; + tier = ''; + overQuota = DEFAULT_OVER_QUOTA; + + logger.logMessage('initialized', { version: MODULE_VERSION }); + + // A/B testing: initialize ABTestManager + ABTestManager.init(config?.params); + + return true; } /** - * enrichOrtb2DeviceData enriches the ortb2data device data with WURFL data. - * Note: it does not overrides properties set by Prebid.js - * @param {String} key the device property key - * @param {any} value the value of the device property - * @param {Object} device the ortb2 device object from Prebid.js - * @param {Object} ortb2data the ortb2 device data enrchiced with WURFL data + * getBidRequestData enriches the OpenRTB 2.0 device data with WURFL data + * @param {Object} reqBidsConfigObj Bid request configuration object + * @param {Function} callback Called on completion + * @param {Object} config Configuration for WURFL RTD submodule + * @param {Object} userConsent User consent data */ -function enrichOrtb2DeviceData(key, value, device, ortb2data) { - if (device?.[key] !== undefined) { - // value already defined by Prebid.js, do not overrides +const getBidRequestData = (reqBidsConfigObj, callback, config, userConsent) => { + // Start module execution timing + WurflDebugger.moduleExecutionStart(); + + // Extract bidders from request configuration and set default enrichment + const bidders = new Set(); + reqBidsConfigObj.adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + bidders.add(bid.bidder); + bidderEnrichment.set(bid.bidder, ENRICHMENT_TYPE.UNKNOWN); + }); + }); + + // Determine enrichment type based on cache availability + WurflDebugger.cacheReadStart(); + const cachedWurflData = getObjectFromStorage(WURFL_RTD_STORAGE_KEY); + WurflDebugger.cacheReadStop(); + + const abEnrichmentType = cachedWurflData ? AB_TEST.ENRICHMENT_TYPE_WURFL : AB_TEST.ENRICHMENT_TYPE_LCE; + ABTestManager.setEnrichmentType(abEnrichmentType); + + // A/B test: Skip enrichment for control group + if (ABTestManager.isInControlGroup()) { + logger.logMessage('A/B test control group: skipping enrichment'); + enrichmentType = ENRICHMENT_TYPE.NONE; + bidders.forEach(bidder => bidderEnrichment.set(bidder, ENRICHMENT_TYPE.NONE)); + WurflDebugger.moduleExecutionStop(); + callback(); return; } - if (value === undefined) { + + // Priority 1: Check if WURFL.js response is cached + if (cachedWurflData) { + const isExpired = cachedWurflData.expire_at && Date.now() > cachedWurflData.expire_at; + + WurflDebugger.setDataSource('cache'); + WurflDebugger.setCacheExpired(isExpired); + WurflDebugger.setCacheData(cachedWurflData.WURFL, cachedWurflData.wurfl_pbjs); + + const wjsDevice = WurflJSDevice.fromCache(cachedWurflData); + if (wjsDevice._isOverQuota()) { + enrichmentType = ENRICHMENT_TYPE.NONE; + } else { + enrichDeviceFPD(reqBidsConfigObj, wjsDevice.FPD()); + enrichmentType = ENRICHMENT_TYPE.WURFL_PUB; + } + enrichDeviceBidder(reqBidsConfigObj, bidders, wjsDevice); + + // Store WURFL ID for analytics + wurflId = cachedWurflData.WURFL?.wurfl_id || ''; + + // Store sampling rate for beacon + samplingRate = cachedWurflData.wurfl_pbjs?.sampling_rate ?? DEFAULT_SAMPLING_RATE; + + // Store tier for beacon + tier = cachedWurflData.wurfl_pbjs?.tier ?? ''; + + // Store over_quota for beacon + overQuota = cachedWurflData.wurfl_pbjs?.over_quota ?? DEFAULT_OVER_QUOTA; + + // If expired, refresh cache async + if (isExpired) { + loadWurflJsAsync(config, bidders); + } + + logger.logMessage('enrichment completed', { + type: enrichmentType, + dataSource: 'cache', + cacheExpired: isExpired, + bidders: Object.fromEntries(bidderEnrichment), + totalBidders: bidderEnrichment.size + }); + + WurflDebugger.moduleExecutionStop(); + callback(); return; } - ortb2data.device[key] = value; -} -/** - * toNumber converts a given value to a number. - * Returns `undefined` if the conversion results in `NaN`. - * @param {any} value - The value to convert to a number. - * @returns {number|undefined} The converted number, or `undefined` if the conversion fails. - */ -export function toNumber(value) { - if (value === '' || value === null) { - return undefined; + // Priority 2: return LCE data + WurflDebugger.setDataSource('lce'); + WurflDebugger.lceDetectionStart(); + + let lceDevice; + try { + lceDevice = WurflLCEDevice.FPD(); + enrichmentType = ENRICHMENT_TYPE.LCE; + } catch (e) { + logger.logError('Error generating LCE device data:', e); + lceDevice = { js: 1 }; + enrichmentType = ENRICHMENT_TYPE.LCE_ERROR; } - const num = Number(value); - return Number.isNaN(num) ? undefined : num; + + WurflDebugger.lceDetectionStop(); + WurflDebugger.setLceData(lceDevice); + enrichDeviceFPD(reqBidsConfigObj, lceDevice); + + // Set enrichment type for all bidders + bidders.forEach(bidder => bidderEnrichment.set(bidder, enrichmentType)); + + // Set default sampling rate for LCE + samplingRate = DEFAULT_SAMPLING_RATE; + + // Set default tier for LCE + tier = ''; + + // Set default over_quota for LCE + overQuota = DEFAULT_OVER_QUOTA; + + // Load WURFL.js async for future requests + loadWurflJsAsync(config, bidders); + + logger.logMessage('enrichment completed', { + type: enrichmentType, + dataSource: 'lce', + bidders: Object.fromEntries(bidderEnrichment), + totalBidders: bidderEnrichment.size + }); + + WurflDebugger.moduleExecutionStop(); + callback(); } /** @@ -266,23 +1279,135 @@ export function toNumber(value) { * @param {Object} userConsent User consent data */ function onAuctionEndEvent(auctionDetails, config, userConsent) { - const altHost = config.params?.altHost ?? null; - - let host = WURFL_JS_HOST; - if (altHost) { - host = altHost; + // Apply sampling + if (!shouldSample(samplingRate)) { + logger.logMessage(`beacon skipped due to sampling (rate: ${samplingRate}%)`); + return; } - const url = new URL(host); - url.pathname = STATS_ENDPOINT_PATH; + const statsHost = config.params?.statsHost ?? null; + + let host = STATS_HOST; + if (statsHost) { + host = statsHost; + } - if (enrichedBidders.size === 0) { + let url; + try { + url = new URL(host); + url.pathname = STATS_ENDPOINT_PATH; + } catch (e) { + logger.logError('Invalid stats host URL:', host); return; } - var payload = JSON.stringify({ bidders: [...enrichedBidders] }); + // Calculate consent class + let consentClass; + try { + consentClass = getConsentClass(userConsent); + logger.logMessage('consent class', consentClass); + } catch (e) { + logger.logError('Error calculating consent class:', e); + consentClass = CONSENT_CLASS.ERROR; + } + + // Build a lookup object for winning bid request IDs + const winningBids = getGlobal().getHighestCpmBids() || []; + const winningBidIds = {}; + for (let i = 0; i < winningBids.length; i++) { + const bid = winningBids[i]; + winningBidIds[bid.requestId] = true; + } + + // Build a lookup object for bid responses: "adUnitCode:bidderCode" -> bid + const bidResponseMap = {}; + const bidsReceived = auctionDetails.bidsReceived || []; + for (let i = 0; i < bidsReceived.length; i++) { + const bid = bidsReceived[i]; + const adUnitCode = bid.adUnitCode; + const bidderCode = bid.bidder || bid.bidderCode; + const key = adUnitCode + ':' + bidderCode; + bidResponseMap[key] = bid; + } + + // Build ad units array with all bidders (including non-responders) + const adUnits = []; + + if (auctionDetails.adUnits) { + for (let i = 0; i < auctionDetails.adUnits.length; i++) { + const adUnit = auctionDetails.adUnits[i]; + const adUnitCode = adUnit.code; + const bidders = []; + + // Check each bidder configured for this ad unit + const bids = adUnit.bids || []; + for (let j = 0; j < bids.length; j++) { + const bidConfig = bids[j]; + const bidderCode = bidConfig.bidder; + const key = adUnitCode + ':' + bidderCode; + const bidResponse = bidResponseMap[key]; + + if (bidResponse) { + // Bidder responded - include full data + const isWinner = winningBidIds[bidResponse.requestId] === true; + bidders.push({ + bidder: bidderCode, + bdr_enrich: bidderEnrichment.get(bidderCode), + cpm: bidResponse.cpm, + currency: bidResponse.currency, + won: isWinner + }); + } else { + // Bidder didn't respond - include without cpm/currency + bidders.push({ + bidder: bidderCode, + bdr_enrich: bidderEnrichment.get(bidderCode), + won: false + }); + } + } + + adUnits.push({ + ad_unit_code: adUnitCode, + bidders: bidders + }); + } + } + + logger.logMessage('auction completed', { + bidsReceived: auctionDetails.bidsReceived ? auctionDetails.bidsReceived.length : 0, + bidsWon: winningBids.length, + adUnits: adUnits.length + }); + + // Build complete payload + const payloadData = { + version: MODULE_VERSION, + domain: typeof window !== 'undefined' ? window.location.hostname : '', + path: typeof window !== 'undefined' ? window.location.pathname : '', + sampling_rate: samplingRate, + enrichment: enrichmentType, + wurfl_id: wurflId, + tier: tier, + over_quota: overQuota, + consent_class: consentClass, + ad_units: adUnits + }; + + // Add A/B test fields if enabled + const abPayload = ABTestManager.getBeaconPayload(); + if (abPayload) { + payloadData.ab_name = abPayload.ab_name; + payloadData.ab_variant = abPayload.ab_variant; + } + + const payload = JSON.stringify(payloadData); + + // Both sendBeacon and fetch send as text/plain to avoid CORS preflight requests. + // Server must parse body as JSON regardless of Content-Type header. const sentBeacon = sendBeacon(url.toString(), payload); if (sentBeacon) { + WurflDebugger.setBeaconPayload(payloadData); return; } @@ -291,9 +1416,15 @@ function onAuctionEndEvent(auctionDetails, config, userConsent) { body: payload, mode: 'no-cors', keepalive: true + }).catch((e) => { + logger.logError('Failed to send beacon via fetch:', e); }); + + WurflDebugger.setBeaconPayload(payloadData); } +// ==================== MODULE EXPORT ==================== + // The WURFL submodule export const wurflSubmodule = { name: MODULE_NAME, diff --git a/modules/wurflRtdProvider.md b/modules/wurflRtdProvider.md index abee06c02e7..30651a6ddea 100644 --- a/modules/wurflRtdProvider.md +++ b/modules/wurflRtdProvider.md @@ -13,6 +13,8 @@ The module sets the WURFL data in `device.ext.wurfl` and all the bidder adapters For a more detailed analysis bidders can subscribe to detect iPhone and iPad models and receive additional [WURFL device capabilities](https://www.scientiamobile.com/capabilities/?products%5B%5D=wurfl-js). +**Note:** This module loads a dynamically generated JavaScript from prebid.wurflcloud.com + ## User-Agent Client Hints WURFL.js is fully compatible with Chromium's User-Agent Client Hints (UA-CH) initiative. If User-Agent Client Hints are absent in the HTTP headers that WURFL.js receives, the service will automatically fall back to using the User-Agent Client Hints' JS API to fetch [high entropy client hint values](https://wicg.github.io/ua-client-hints/#getHighEntropyValues) from the client device. However, we recommend that you explicitly opt-in/advertise support for User-Agent Client Hints on your website and delegate them to the WURFL.js service for the fastest detection experience. Our documentation regarding implementing User-Agent Client Hint support [is available here](https://docs.scientiamobile.com/guides/implementing-useragent-clienthints). @@ -20,6 +22,7 @@ WURFL.js is fully compatible with Chromium's User-Agent Client Hints (UA-CH) ini ## Usage ### Build + ``` gulp build --modules="wurflRtdProvider,appnexusBidAdapter,..." ``` @@ -33,28 +36,57 @@ This module is configured as part of the `realTimeData.dataProviders` ```javascript var TIMEOUT = 1000; pbjs.setConfig({ - realTimeData: { - auctionDelay: TIMEOUT, - dataProviders: [{ - name: 'wurfl', - waitForIt: true, - params: { - debug: false - } - }] - } + realTimeData: { + auctionDelay: TIMEOUT, + dataProviders: [ + { + name: "wurfl", + }, + ], + }, }); ``` ### Parameters -| Name | Type | Description | Default | -| :------------------------ | :------------ | :--------------------------------------------------------------- |:----------------- | -| name | String | Real time data module name | Always 'wurfl' | -| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | -| params | Object | | | -| params.altHost | String | Alternate host to connect to WURFL.js | | -| params.debug | Boolean | Enable debug | `false` | +| Name | Type | Description | Default | +| :------------------ | :------ | :--------------------------------------------------------------- | :------------- | +| name | String | Real time data module name | Always 'wurfl' | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | +| params | Object | | | +| params.altHost | String | Alternate host to connect to WURFL.js | | +| params.abTest | Boolean | Enable A/B testing mode | `false` | +| params.abName | String | A/B test name identifier | `'unknown'` | +| params.abSplit | Number | Fraction of users in treatment group (0-1) | `0.5` | +| params.abExcludeLCE | Boolean | Don't apply A/B testing to LCE bids | `true` | + +### A/B Testing + +The WURFL RTD module supports A/B testing to measure the impact of WURFL enrichment on ad performance: + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 1000, + dataProviders: [ + { + name: "wurfl", + waitForIt: true, + params: { + abTest: true, + abName: "pub_test_sept23", + abSplit: 0.5, // 50% treatment, 50% control + }, + }, + ], + }, +}); +``` + +- **Treatment group** (`abSplit` \* 100%): Module enabled, bid requests enriched with WURFL device data +- **Control group** ((1 - `abSplit`) \* 100%): Module disabled, no enrichment occurs +- Assignment is random on each page load based on `Math.random()` +- Example: `abSplit: 0.75` means 75% get WURFL enrichment, 25% don't ## Testing diff --git a/modules/yahooAdsBidAdapter.js b/modules/yahooAdsBidAdapter.js index caed513aa6a..b2b92c6ba95 100644 --- a/modules/yahooAdsBidAdapter.js +++ b/modules/yahooAdsBidAdapter.js @@ -490,11 +490,16 @@ function appendFirstPartyData(outBoundBidRequest, bid) { const allowedUserStrings = ['id', 'buyeruid', 'gender', 'keywords', 'customdata']; const allowedUserNumbers = ['yob']; const allowedUserArrays = ['data']; - const allowedUserObjects = ['ext']; outBoundBidRequest.user = validateAppendObject('string', allowedUserStrings, userObject, outBoundBidRequest.user); outBoundBidRequest.user = validateAppendObject('number', allowedUserNumbers, userObject, outBoundBidRequest.user); outBoundBidRequest.user = validateAppendObject('array', allowedUserArrays, userObject, outBoundBidRequest.user); - outBoundBidRequest.user.ext = validateAppendObject('object', allowedUserObjects, userObject, outBoundBidRequest.user.ext); + // Merge ext properties from ortb2.user.ext into existing user.ext instead of nesting + if (userObject.ext && isPlainObject(userObject.ext)) { + outBoundBidRequest.user.ext = { + ...outBoundBidRequest.user.ext, + ...userObject.ext + }; + } }; return outBoundBidRequest; diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index f606194e132..d249a5e3bd3 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -15,9 +15,9 @@ import { parseQueryStringParameters, parseUrl } from '../src/utils.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {Renderer} from '../src/Renderer.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { Renderer } from '../src/Renderer.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -42,8 +42,6 @@ const OPENRTB_VIDEO_BIDPARAMS = ['mimes', 'startdelay', 'placement', 'plcmt', 's 'playbackmethod', 'maxduration', 'minduration', 'pos', 'skip', 'skippable']; const OPENRTB_VIDEO_SITEPARAMS = ['name', 'domain', 'cat', 'keywords']; const LOCAL_WINDOW = getWindowTop(); -const DEFAULT_PLAYBACK_METHOD = 2; -const DEFAULT_START_DELAY = 0; const VAST_TIMEOUT = 15000; const MAX_BANNER_REQUEST_URL_LENGTH = 8000; const BANNER_REQUEST_PROPERTIES_TO_REDUCE = ['description', 'title', 'pr', 'page_url']; @@ -96,7 +94,8 @@ export const spec = { cmp: deepAccess(bidderRequest, 'gdprConsent.consentString') || '', gpp: deepAccess(bidderRequest, 'gppConsent.gppString') || '', gpp_sid: - deepAccess(bidderRequest, 'gppConsent.applicableSections') || []}), + deepAccess(bidderRequest, 'gppConsent.applicableSections') || [] + }), us_privacy: deepAccess(bidderRequest, 'uspConsent') || '', }; if (topicsData) { @@ -211,7 +210,7 @@ export const spec = { return bids; }, - getUserSyncs: function(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { + getUserSyncs: function (syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { const syncs = []; const gdprFlag = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; const gdprString = `&gdpr_consent=${encodeURIComponent((gdprConsent.consentString || ''))}`; @@ -505,10 +504,6 @@ function openRtbImpression(bidRequest) { imp.video.skip = 1; delete imp.video.skippable; } - if (imp.video.plcmt !== 1 || imp.video.placement !== 1) { - imp.video.startdelay = DEFAULT_START_DELAY; - imp.video.playbackmethod = [ DEFAULT_PLAYBACK_METHOD ]; - } if (gpid) { imp.ext.gpid = gpid; } diff --git a/package-lock.json b/package-lock.json index 7ba69e619d8..26967a28f7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prebid.js", - "version": "10.16.0", - "lockfileVersion": 3, + "version": "10.21.0", + "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prebid.js", - "version": "10.16.0", + "version": "10.21.0", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.28.4", @@ -25,6 +25,7 @@ "iab-adcom": "^1.0.6", "iab-native": "^1.0.0", "iab-openrtb": "^1.0.1", + "karma-safarinative-launcher": "^1.1.0", "klona": "^2.0.6", "live-connect-js": "^7.2.0" }, @@ -32,6 +33,7 @@ "@babel/eslint-parser": "^7.16.5", "@babel/plugin-transform-runtime": "^7.27.4", "@babel/register": "^7.28.3", + "@chiragrupani/karma-chromium-edge-launcher": "^2.4.1", "@eslint/compat": "^1.3.1", "@types/google-publisher-tag": "^1.20250210.0", "@wdio/browserstack-service": "^9.19.1", @@ -1610,9 +1612,14 @@ "uuid": "9.0.1" } }, + "node_modules/@chiragrupani/karma-chromium-edge-launcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@chiragrupani/karma-chromium-edge-launcher/-/karma-chromium-edge-launcher-2.4.1.tgz", + "integrity": "sha512-HwTlN4dk7dnL9m5nEonq7cI3Wa787wYfGVWeb4oWPMySIEhFpA7/BYQ8zMbpQ4YkSQxVnvY1502aWdbI3w7DeA==", + "dev": true + }, "node_modules/@colors/colors": { "version": "1.5.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.1.90" @@ -1673,597 +1680,781 @@ "node": ">=16" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", + "node_modules/@esbuild/android-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", + "node_modules/@esbuild/android-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18" } }, - "node_modules/@eslint/compat": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.1.tgz", - "integrity": "sha512-k8MHony59I5EPic6EQTCNOuPoVBnoYXkP+20xvwFjN7t0qI3ImyvyBgg+hIVPwC8JaxVjjUZld+cLfBLFDLucg==", + "node_modules/@esbuild/android-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "peerDependencies": { - "eslint": "^8.40 || 9" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" } }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "node_modules/@esbuild/linux-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": ">=18" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "cpu": [ + "mips64el" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.15.1", - "levn": "^0.4.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" } }, - "node_modules/@gulp-sourcemaps/identity-map": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^6.4.1", - "normalize-path": "^3.0.0", - "postcss": "^7.0.16", - "source-map": "^0.6.0", - "through2": "^3.0.1" - }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.10" + "node": ">=18" } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/acorn": { - "version": "6.4.2", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.4.0" + "node": ">=18" } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/through2": { - "version": "3.0.2", + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "2 || 3" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@gulp-sourcemaps/map-sources": { - "version": "1.0.0", + "node_modules/@esbuild/linux-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "normalize-path": "^2.0.1", - "through2": "^2.0.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.10" + "node": ">=18" } }, - "node_modules/@gulp-sourcemaps/map-sources/node_modules/normalize-path": { - "version": "2.1.1", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/@gulp-sourcemaps/map-sources/node_modules/through2": { - "version": "2.0.5", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@gulpjs/messages": { - "version": "1.1.0", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=10.13.0" + "node": ">=18" } }, - "node_modules/@gulpjs/to-absolute-glob": { - "version": "4.0.0", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "is-negated-glob": "^1.0.0" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=10.13.0" + "node": ">=18" } }, - "node_modules/@hapi/boom": { - "version": "9.1.4", + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "9.x.x" + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@hapi/cryptiles": { - "version": "5.1.0", + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/boom": "9.x.x" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.0.0" + "node": ">=18" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "BSD-3-Clause" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", + "node_modules/@esbuild/win32-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanfs/node": { - "version": "0.16.6", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": ">=18.18.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=18.18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@humanwhocodes/gitignore-to-minimatch": { - "version": "1.0.2", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", "dev": true, - "license": "Apache-2.0", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", + "node_modules/@eslint/compat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.1.tgz", + "integrity": "sha512-k8MHony59I5EPic6EQTCNOuPoVBnoYXkP+20xvwFjN7t0qI3ImyvyBgg+hIVPwC8JaxVjjUZld+cLfBLFDLucg==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=12.22" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "eslint": "^8.40 || 9" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", - "engines": { - "node": ">=18.18" + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@inquirer/checkbox": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.0.tgz", - "integrity": "sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA==", + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" + "@types/json-schema": "^7.0.15" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@inquirer/confirm": { - "version": "5.1.14", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.14.tgz", - "integrity": "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==", + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/type": "^3.0.8" + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@inquirer/core": { - "version": "10.1.15", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", - "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", - "dependencies": { - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "ansi-escapes": "^4.3.2", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" - }, "engines": { "node": ">=18" }, - "peerDependencies": { - "@types/node": ">=18" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@inquirer/core/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "node_modules/@eslint/js": { + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, - "license": "ISC", + "license": "MIT", "engines": { - "node": ">=14" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://eslint.org/donate" } }, - "node_modules/@inquirer/editor": { - "version": "4.2.16", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.16.tgz", - "integrity": "sha512-iSzLjT4C6YKp2DU0fr8T7a97FnRRxMO6CushJnW5ktxLNM2iNeuyUuUA5255eOLPORoGYCrVnuDOEBdGkHGkpw==", + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/external-editor": "^1.0.0", - "@inquirer/type": "^3.0.8" + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@inquirer/expand": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.17.tgz", - "integrity": "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw==", + "node_modules/@gulp-sourcemaps/identity-map": { + "version": "2.0.1", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" + "acorn": "^6.4.1", + "normalize-path": "^3.0.0", + "postcss": "^7.0.16", + "source-map": "^0.6.0", + "through2": "^3.0.1" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" + "node": ">= 0.10" + } + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/acorn": { + "version": "6.4.2", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "engines": { + "node": ">=0.4.0" } }, - "node_modules/@inquirer/external-editor": { + "node_modules/@gulp-sourcemaps/identity-map/node_modules/through2": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/@gulp-sourcemaps/map-sources": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.0.tgz", - "integrity": "sha512-5v3YXc5ZMfL6OJqXPrX9csb4l7NlQA2doO1yynUjpUChT9hg4JcuBVP0RbsEJ/3SL/sxWEyFjT2W69ZhtoBWqg==", "dev": true, "license": "MIT", "dependencies": { - "chardet": "^2.1.0", - "iconv-lite": "^0.6.3" + "normalize-path": "^2.0.1", + "through2": "^2.0.3" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" + "node": ">= 0.10" } }, - "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "node_modules/@gulp-sourcemaps/map-sources/node_modules/normalize-path": { + "version": "2.1.1", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "remove-trailing-separator": "^1.0.1" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/@inquirer/figures": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", - "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", + "node_modules/@gulp-sourcemaps/map-sources/node_modules/through2": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/@gulpjs/messages": { + "version": "1.1.0", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=10.13.0" } }, - "node_modules/@inquirer/input": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.1.tgz", - "integrity": "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow==", + "node_modules/@gulpjs/to-absolute-glob": { + "version": "4.0.0", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/type": "^3.0.8" + "is-negated-glob": "^1.0.0" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": ">=10.13.0" } }, - "node_modules/@inquirer/number": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.17.tgz", - "integrity": "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg==", + "node_modules/@hapi/boom": { + "version": "9.1.4", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/type": "^3.0.8" + "@hapi/hoek": "9.x.x" + } + }, + "node_modules/@hapi/cryptiles": { + "version": "5.1.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "9.x.x" }, "engines": { - "node": ">=18" + "node": ">=12.0.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" }, - "peerDependencies": { - "@types/node": ">=18" + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@inquirer/password": { - "version": "4.0.17", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.17.tgz", - "integrity": "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA==", + "node_modules/@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "dev": true, + "license": "Apache-2.0", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.0.tgz", + "integrity": "sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA==", "dev": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", - "ansi-escapes": "^4.3.2" + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" }, "engines": { "node": ">=18" @@ -2277,23 +2468,15 @@ } } }, - "node_modules/@inquirer/prompts": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.0.tgz", - "integrity": "sha512-JHwGbQ6wjf1dxxnalDYpZwZxUEosT+6CPGD9Zh4sm9WXdtUp9XODCQD3NjSTmu+0OAyxWXNOqf0spjIymJa2Tw==", + "node_modules/@inquirer/confirm": { + "version": "5.1.14", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.14.tgz", + "integrity": "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.2.0", - "@inquirer/confirm": "^5.1.14", - "@inquirer/editor": "^4.2.15", - "@inquirer/expand": "^4.0.17", - "@inquirer/input": "^4.2.1", - "@inquirer/number": "^3.0.17", - "@inquirer/password": "^4.0.17", - "@inquirer/rawlist": "^4.1.5", - "@inquirer/search": "^3.1.0", - "@inquirer/select": "^4.3.1" + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" }, "engines": { "node": ">=18" @@ -2307,15 +2490,20 @@ } } }, - "node_modules/@inquirer/rawlist": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.5.tgz", - "integrity": "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA==", + "node_modules/@inquirer/core": { + "version": "10.1.15", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", + "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -2330,17 +2518,29 @@ } } }, - "node_modules/@inquirer/search": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.0.tgz", - "integrity": "sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q==", + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.16", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.16.tgz", + "integrity": "sha512-iSzLjT4C6YKp2DU0fr8T7a97FnRRxMO6CushJnW5ktxLNM2iNeuyUuUA5255eOLPORoGYCrVnuDOEBdGkHGkpw==", "dev": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.1.15", - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "yoctocolors-cjs": "^2.1.2" + "@inquirer/external-editor": "^1.0.0", + "@inquirer/type": "^3.0.8" }, "engines": { "node": ">=18" @@ -2354,17 +2554,15 @@ } } }, - "node_modules/@inquirer/select": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.1.tgz", - "integrity": "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA==", + "node_modules/@inquirer/expand": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.17.tgz", + "integrity": "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw==", "dev": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.1.15", - "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", - "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -2379,70 +2577,279 @@ } } }, - "node_modules/@inquirer/type": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", - "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", + "node_modules/@inquirer/external-editor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.0.tgz", + "integrity": "sha512-5v3YXc5ZMfL6OJqXPrX9csb4l7NlQA2doO1yynUjpUChT9hg4JcuBVP0RbsEJ/3SL/sxWEyFjT2W69ZhtoBWqg==", "dev": true, "license": "MIT", + "dependencies": { + "chardet": "^2.1.0", + "iconv-lite": "^0.6.3" + }, "engines": { "node": ">=18" }, "peerDependencies": { "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, "engines": { - "node": "20 || >=22" + "node": ">=0.10.0" } }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "node_modules/@inquirer/figures": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", + "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", "dev": true, "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, "engines": { - "node": "20 || >=22" + "node": ">=18" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", + "node_modules/@inquirer/input": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.1.tgz", + "integrity": "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", + "node_modules/@inquirer/number": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.17.tgz", + "integrity": "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" - }, + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.17.tgz", + "integrity": "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.0.tgz", + "integrity": "sha512-JHwGbQ6wjf1dxxnalDYpZwZxUEosT+6CPGD9Zh4sm9WXdtUp9XODCQD3NjSTmu+0OAyxWXNOqf0spjIymJa2Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.2.0", + "@inquirer/confirm": "^5.1.14", + "@inquirer/editor": "^4.2.15", + "@inquirer/expand": "^4.0.17", + "@inquirer/input": "^4.2.1", + "@inquirer/number": "^3.0.17", + "@inquirer/password": "^4.0.17", + "@inquirer/rawlist": "^4.1.5", + "@inquirer/search": "^3.1.0", + "@inquirer/select": "^4.3.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.5.tgz", + "integrity": "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.0.tgz", + "integrity": "sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.1.tgz", + "integrity": "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", + "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } @@ -2987,7 +3394,6 @@ }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", - "dev": true, "license": "MIT" }, "node_modules/@stylistic/eslint-plugin": { @@ -3056,12 +3462,10 @@ }, "node_modules/@types/cookie": { "version": "0.4.1", - "dev": true, "license": "MIT" }, "node_modules/@types/cors": { "version": "2.8.17", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -3149,7 +3553,6 @@ }, "node_modules/@types/node": { "version": "20.14.2", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -6105,7 +6508,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6131,7 +6533,6 @@ }, "node_modules/anymatch": { "version": "3.1.3", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -6653,12 +7054,14 @@ } }, "node_modules/axios": { - "version": "1.9.0", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "dev": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -6768,7 +7171,6 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/bare-events": { @@ -6859,17 +7261,15 @@ }, "node_modules/base64id": { "version": "2.0.0", - "dev": true, "license": "MIT", "engines": { "node": "^4.5.0 || >= 5.9" } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.17.tgz", - "integrity": "sha512-j5zJcx6golJYTG6c05LUZ3Z8Gi+M62zRT/ycz4Xq4iCOdpcxwg7ngEYD4KA0eWZC7U17qh/Smq8bYbACJ0ipBA==", - "license": "Apache-2.0", + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", "bin": { "baseline-browser-mapping": "dist/cli.js" } @@ -6913,7 +7313,6 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6994,21 +7393,22 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "license": "MIT", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -7022,10 +7422,37 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "license": "MIT" }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/body/node_modules/bytes": { "version": "1.0.0", "dev": true @@ -7056,7 +7483,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -7065,7 +7491,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -7205,7 +7630,8 @@ }, "node_modules/bytes": { "version": "3.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "engines": { "node": ">= 0.8" } @@ -7274,9 +7700,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001754", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", - "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", "funding": [ { "type": "opencollective", @@ -7393,7 +7819,6 @@ }, "node_modules/chokidar": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -7718,7 +8143,6 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "dev": true, "license": "MIT" }, "node_modules/concat-with-sourcemaps": { @@ -7731,7 +8155,6 @@ }, "node_modules/connect": { "version": "3.7.0", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -7753,7 +8176,6 @@ }, "node_modules/connect/node_modules/debug": { "version": "2.6.9", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -7761,7 +8183,6 @@ }, "node_modules/connect/node_modules/finalhandler": { "version": "1.1.2", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -7778,12 +8199,10 @@ }, "node_modules/connect/node_modules/ms": { "version": "2.0.0", - "dev": true, "license": "MIT" }, "node_modules/connect/node_modules/on-finished": { "version": "2.3.0", - "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -7794,7 +8213,6 @@ }, "node_modules/connect/node_modules/statuses": { "version": "1.5.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -7886,7 +8304,6 @@ }, "node_modules/cors": { "version": "2.8.5", - "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4", @@ -7929,10 +8346,11 @@ "dev": true }, "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -8373,7 +8791,6 @@ }, "node_modules/custom-event": { "version": "1.0.1", - "dev": true, "license": "MIT" }, "node_modules/d": { @@ -8446,7 +8863,6 @@ }, "node_modules/date-format": { "version": "4.0.14", - "dev": true, "license": "MIT", "engines": { "node": ">=4.0" @@ -8725,7 +9141,6 @@ }, "node_modules/di": { "version": "0.0.1", - "dev": true, "license": "MIT" }, "node_modules/diff": { @@ -8763,7 +9178,6 @@ }, "node_modules/dom-serialize": { "version": "2.2.1", - "dev": true, "license": "MIT", "dependencies": { "custom-event": "~1.0.0", @@ -9048,7 +9462,6 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/emojis-list": { @@ -9061,7 +9474,6 @@ }, "node_modules/encodeurl": { "version": "1.0.2", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -9100,7 +9512,6 @@ }, "node_modules/engine.io": { "version": "6.6.2", - "dev": true, "license": "MIT", "dependencies": { "@types/cookie": "^0.4.1", @@ -9120,7 +9531,6 @@ }, "node_modules/engine.io-parser": { "version": "5.2.3", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -9128,7 +9538,6 @@ }, "node_modules/engine.io/node_modules/cookie": { "version": "0.7.2", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -9150,7 +9559,6 @@ }, "node_modules/ent": { "version": "2.2.0", - "dev": true, "license": "MIT" }, "node_modules/entities": { @@ -9439,9 +9847,10 @@ }, "node_modules/esbuild": { "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", "dev": true, "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -10517,7 +10926,6 @@ }, "node_modules/eventemitter3": { "version": "4.0.7", - "dev": true, "license": "MIT" }, "node_modules/events": { @@ -10638,37 +11046,38 @@ } }, "node_modules/express": { - "version": "4.21.2", - "license": "MIT", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -10695,53 +11104,10 @@ "node": ">= 0.8" } }, - "node_modules/express/node_modules/mime": { - "version": "1.6.0", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/express/node_modules/ms": { "version": "2.0.0", "license": "MIT" }, - "node_modules/express/node_modules/send": { - "version": "0.19.0", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/express/node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/send/node_modules/ms": { - "version": "2.1.3", - "license": "MIT" - }, "node_modules/ext": { "version": "1.7.0", "dev": true, @@ -10752,7 +11118,6 @@ }, "node_modules/extend": { "version": "3.0.2", - "dev": true, "license": "MIT" }, "node_modules/extend-shallow": { @@ -11032,7 +11397,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -11162,12 +11526,10 @@ }, "node_modules/flatted": { "version": "3.3.1", - "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { "version": "1.15.6", - "dev": true, "funding": [ { "type": "individual", @@ -11343,7 +11705,6 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -11476,7 +11837,6 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -11625,7 +11985,9 @@ "license": "ISC" }, "node_modules/glob": { - "version": "10.4.5", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -11645,7 +12007,6 @@ }, "node_modules/glob-parent": { "version": "5.1.2", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -12735,7 +13096,6 @@ }, "node_modules/http-proxy": { "version": "1.18.1", - "dev": true, "license": "MIT", "dependencies": { "eventemitter3": "^4.0.0", @@ -12820,7 +13180,8 @@ }, "node_modules/iconv-lite": { "version": "0.4.24", - "license": "MIT", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -12908,7 +13269,6 @@ }, "node_modules/inflight": { "version": "1.0.6", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -13080,7 +13440,6 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -13218,7 +13577,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13240,7 +13598,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13267,7 +13624,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -13312,7 +13668,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -13566,7 +13921,6 @@ }, "node_modules/isbinaryfile": { "version": "4.0.10", - "dev": true, "license": "MIT", "engines": { "node": ">= 8.0.0" @@ -14428,7 +14782,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -14552,7 +14908,6 @@ "version": "6.4.4", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", - "dev": true, "license": "MIT", "dependencies": { "@colors/colors": "1.5.0", @@ -14836,8 +15191,17 @@ }, "node_modules/karma-safari-launcher": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/karma-safari-launcher/-/karma-safari-launcher-1.0.0.tgz", + "integrity": "sha512-qmypLWd6F2qrDJfAETvXDfxHvKDk+nyIjpH9xIeI3/hENr0U3nuqkxaftq73PfXZ4aOuOChA6SnLW4m4AxfRjQ==", "dev": true, - "license": "MIT", + "peerDependencies": { + "karma": ">=0.9" + } + }, + "node_modules/karma-safarinative-launcher": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/karma-safarinative-launcher/-/karma-safarinative-launcher-1.1.0.tgz", + "integrity": "sha512-vdMjdQDHkSUbOZc8Zq2K5bBC0yJGFEgfrKRJTqt0Um0SC1Rt8drS2wcN6UA3h4LgsL3f1pMcmRSvKucbJE8Qdg==", "peerDependencies": { "karma": ">=0.9" } @@ -14954,7 +15318,6 @@ }, "node_modules/karma/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -14968,7 +15331,6 @@ }, "node_modules/karma/node_modules/cliui": { "version": "7.0.4", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -14978,7 +15340,6 @@ }, "node_modules/karma/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -14989,12 +15350,10 @@ }, "node_modules/karma/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/karma/node_modules/glob": { "version": "7.2.3", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -15013,7 +15372,6 @@ }, "node_modules/karma/node_modules/strip-ansi": { "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -15024,7 +15382,6 @@ }, "node_modules/karma/node_modules/wrap-ansi": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -15040,7 +15397,6 @@ }, "node_modules/karma/node_modules/yargs": { "version": "16.2.0", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^7.0.2", @@ -15057,7 +15413,6 @@ }, "node_modules/karma/node_modules/yargs-parser": { "version": "20.2.9", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -15320,7 +15675,6 @@ }, "node_modules/log4js": { "version": "6.9.1", - "dev": true, "license": "Apache-2.0", "dependencies": { "date-format": "^4.0.14", @@ -15541,7 +15895,6 @@ }, "node_modules/mime": { "version": "2.6.0", - "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -15568,15 +15921,17 @@ } }, "node_modules/min-document": { - "version": "2.19.0", + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", + "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", "dev": true, + "license": "MIT", "dependencies": { "dom-walk": "^0.1.0" } }, "node_modules/minimatch": { "version": "3.1.2", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -15587,7 +15942,6 @@ }, "node_modules/minimist": { "version": "1.2.8", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -15609,7 +15963,6 @@ }, "node_modules/mkdirp": { "version": "0.5.6", - "dev": true, "license": "MIT", "dependencies": { "minimist": "^1.2.6" @@ -15799,7 +16152,9 @@ } }, "node_modules/mocha/node_modules/js-yaml": { - "version": "4.1.0", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -16398,7 +16753,6 @@ }, "node_modules/normalize-path": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -16447,7 +16801,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -16615,7 +16968,6 @@ }, "node_modules/once": { "version": "1.4.0", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -16932,7 +17284,6 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -17036,7 +17387,6 @@ }, "node_modules/picomatch": { "version": "2.3.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -17416,17 +17766,17 @@ }, "node_modules/qjobs": { "version": "1.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.9" } }, "node_modules/qs": { - "version": "6.13.0", - "license": "BSD-3-Clause", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -17480,14 +17830,42 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "license": "MIT", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "engines": { "node": ">= 0.8" } @@ -17649,7 +18027,6 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -17808,7 +18185,6 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -17824,7 +18200,6 @@ }, "node_modules/requires-port": { "version": "1.0.0", - "dev": true, "license": "MIT" }, "node_modules/resolve": { @@ -17915,7 +18290,6 @@ }, "node_modules/rfdc": { "version": "1.4.1", - "dev": true, "license": "MIT" }, "node_modules/rgb2hex": { @@ -17925,7 +18299,6 @@ }, "node_modules/rimraf": { "version": "3.0.2", - "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -17939,7 +18312,6 @@ }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -18185,23 +18557,24 @@ } }, "node_modules/send": { - "version": "0.16.2", - "dev": true, + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "license": "MIT", "dependencies": { "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", + "depd": "2.0.0", + "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" }, "engines": { "node": ">= 0.8.0" @@ -18209,80 +18582,34 @@ }, "node_modules/send/node_modules/debug": { "version": "2.6.9", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" } }, - "node_modules/send/node_modules/depd": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/destroy": { - "version": "1.0.4", - "dev": true, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/send/node_modules/http-errors": { - "version": "1.6.3", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/inherits": { - "version": "2.0.3", - "dev": true, - "license": "ISC" - }, "node_modules/send/node_modules/mime": { - "version": "1.4.1", - "dev": true, + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "license": "MIT", "bin": { "mime": "cli.js" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/send/node_modules/on-finished": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" }, "engines": { - "node": ">= 0.8" + "node": ">=4" } }, - "node_modules/send/node_modules/setprototypeof": { - "version": "1.1.0", - "dev": true, - "license": "ISC" - }, - "node_modules/send/node_modules/statuses": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/serialize-error": { "version": "12.0.0", @@ -18391,17 +18718,6 @@ "node": ">= 0.8.0" } }, - "node_modules/serve-static/node_modules/debug": { - "version": "2.6.9", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-static/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "license": "MIT" - }, "node_modules/serve-static/node_modules/encodeurl": { "version": "2.0.0", "license": "MIT", @@ -18409,49 +18725,6 @@ "node": ">= 0.8" } }, - "node_modules/serve-static/node_modules/mime": { - "version": "1.6.0", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/serve-static/node_modules/ms": { - "version": "2.1.3", - "license": "MIT" - }, - "node_modules/serve-static/node_modules/send": { - "version": "0.19.0", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-static/node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/set-function-length": { "version": "1.2.2", "dev": true, @@ -18687,7 +18960,6 @@ }, "node_modules/socket.io": { "version": "4.8.0", - "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.4", @@ -18704,7 +18976,6 @@ }, "node_modules/socket.io-adapter": { "version": "2.5.5", - "dev": true, "license": "MIT", "dependencies": { "debug": "~4.3.4", @@ -18713,7 +18984,6 @@ }, "node_modules/socket.io-parser": { "version": "4.2.4", - "dev": true, "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", @@ -18764,7 +19034,6 @@ }, "node_modules/source-map": { "version": "0.6.1", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -19021,7 +19290,6 @@ }, "node_modules/streamroller": { "version": "3.1.5", - "dev": true, "license": "MIT", "dependencies": { "date-format": "^4.0.14", @@ -19034,7 +19302,6 @@ }, "node_modules/streamroller/node_modules/fs-extra": { "version": "8.1.0", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -19047,7 +19314,6 @@ }, "node_modules/streamroller/node_modules/jsonfile": { "version": "4.0.0", - "dev": true, "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" @@ -19055,7 +19321,6 @@ }, "node_modules/streamroller/node_modules/universalify": { "version": "0.1.2", - "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -19095,7 +19360,6 @@ }, "node_modules/string-width": { "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -19133,7 +19397,6 @@ }, "node_modules/string-width/node_modules/strip-ansi": { "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -19393,7 +19656,9 @@ } }, "node_modules/tar-fs": { - "version": "3.0.9", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "dev": true, "license": "MIT", "dependencies": { @@ -19717,7 +19982,6 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz", "integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=14.14" @@ -19738,7 +20002,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -20059,7 +20322,6 @@ }, "node_modules/ua-parser-js": { "version": "0.7.38", - "dev": true, "funding": [ { "type": "opencollective", @@ -20147,16 +20409,16 @@ } }, "node_modules/undici": { - "version": "6.21.3", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", "dev": true, - "license": "MIT", "engines": { "node": ">=18.17" } }, "node_modules/undici-types": { "version": "5.26.5", - "dev": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -20636,7 +20898,6 @@ }, "node_modules/void-elements": { "version": "2.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -21690,12 +21951,10 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "dev": true, "license": "ISC" }, "node_modules/ws": { "version": "8.17.1", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -21722,7 +21981,6 @@ }, "node_modules/y18n": { "version": "5.0.8", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -21906,5 +22164,14091 @@ "url": "https://github.com/sponsors/colinhacks" } } + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.27.1", + "requires": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@babel/compat-data": { + "version": "7.27.5" + }, + "@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + } + }, + "@babel/eslint-parser": { + "version": "7.24.7", + "dev": true, + "requires": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + } + }, + "@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "requires": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "requires": { + "@babel/types": "^7.27.3" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.27.2", + "requires": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.27.1", + "requires": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.27.1", + "requires": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.6.4", + "requires": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + } + }, + "@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==" + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "requires": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + } + }, + "@babel/helper-module-imports": { + "version": "7.27.1", + "requires": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + } + }, + "@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "requires": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "requires": { + "@babel/types": "^7.27.1" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.27.1" + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "requires": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + } + }, + "@babel/helper-replace-supers": { + "version": "7.27.1", + "requires": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "requires": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + } + }, + "@babel/helper-string-parser": { + "version": "7.27.1" + }, + "@babel/helper-validator-identifier": { + "version": "7.27.1" + }, + "@babel/helper-validator-option": { + "version": "7.27.1" + }, + "@babel/helper-wrap-function": { + "version": "7.27.1", + "requires": { + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + } + }, + "@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "requires": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + } + }, + "@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "requires": { + "@babel/types": "^7.28.4" + } + }, + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + } + }, + "@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + } + }, + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "requires": {} + }, + "@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-async-generator-functions": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.27.1" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "requires": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.27.5", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-class-static-block": { + "version": "7.27.1", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.27.1", + "requires": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.27.1", + "globals": "^11.1.0" + }, + "dependencies": { + "globals": { + "version": "11.12.0" + } + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.27.3", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.27.1", + "requires": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + } + }, + "@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-logical-assignment-operators": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "requires": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "requires": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.27.1", + "requires": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "requires": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-object-rest-spread": { + "version": "7.27.3", + "requires": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.3", + "@babel/plugin-transform-parameters": "^7.27.1" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + } + }, + "@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-optional-chaining": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "requires": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.27.5", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.27.4", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.1.tgz", + "integrity": "sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/preset-env": { + "version": "7.27.2", + "requires": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.27.1", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.27.1", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-classes": "^7.27.1", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.27.2", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.27.1", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", + "semver": "^6.3.1" + } + }, + "@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/preset-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" + } + }, + "@babel/register": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.28.3.tgz", + "integrity": "sha512-CieDOtd8u208eI49bYl4z1J22ySFw87IGwE+IswFEExH7e3rLgKb0WNQeumnacQ1+VoDJLYI5QFA3AJZuyZQfA==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "dependencies": { + "find-cache-dir": { + "version": "2.1.0", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "p-locate": { + "version": "3.0.0", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "dev": true + }, + "pify": { + "version": "4.0.1", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "semver": { + "version": "5.7.2", + "dev": true + } + } + }, + "@babel/runtime": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==" + }, + "@babel/template": { + "version": "7.27.2", + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + } + }, + "@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + } + }, + "@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "requires": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + } + }, + "@browserstack/ai-sdk-node": { + "version": "1.5.17", + "dev": true, + "requires": { + "axios": "^1.7.4", + "uuid": "9.0.1" + } + }, + "@chiragrupani/karma-chromium-edge-launcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@chiragrupani/karma-chromium-edge-launcher/-/karma-chromium-edge-launcher-2.4.1.tgz", + "integrity": "sha512-HwTlN4dk7dnL9m5nEonq7cI3Wa787wYfGVWeb4oWPMySIEhFpA7/BYQ8zMbpQ4YkSQxVnvY1502aWdbI3w7DeA==", + "dev": true + }, + "@colors/colors": { + "version": "1.5.0" + }, + "@discoveryjs/json-ext": { + "version": "0.5.7", + "dev": true + }, + "@emnapi/core": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "dev": true, + "optional": true, + "requires": { + "@emnapi/wasi-threads": "1.0.4", + "tslib": "^2.4.0" + } + }, + "@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.4.0" + } + }, + "@emnapi/wasi-threads": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", + "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.4.0" + } + }, + "@es-joy/jsdoccomment": { + "version": "0.49.0", + "dev": true, + "requires": { + "comment-parser": "1.4.1", + "esquery": "^1.6.0", + "jsdoc-type-pratt-parser": "~4.1.0" + } + }, + "@esbuild/aix-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "dev": true, + "optional": true + }, + "@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.4.3" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "3.4.3", + "dev": true + } + } + }, + "@eslint-community/regexpp": { + "version": "4.12.1", + "dev": true + }, + "@eslint/compat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.1.tgz", + "integrity": "sha512-k8MHony59I5EPic6EQTCNOuPoVBnoYXkP+20xvwFjN7t0qI3ImyvyBgg+hIVPwC8JaxVjjUZld+cLfBLFDLucg==", + "dev": true, + "requires": {} + }, + "@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "requires": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + } + }, + "@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true + }, + "@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.15" + } + }, + "@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + } + } + }, + "@eslint/js": { + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "dev": true + }, + "@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true + }, + "@eslint/plugin-kit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "dev": true, + "requires": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + } + }, + "@gulp-sourcemaps/identity-map": { + "version": "2.0.1", + "dev": true, + "requires": { + "acorn": "^6.4.1", + "normalize-path": "^3.0.0", + "postcss": "^7.0.16", + "source-map": "^0.6.0", + "through2": "^3.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.2", + "dev": true + }, + "through2": { + "version": "3.0.2", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + } + } + }, + "@gulp-sourcemaps/map-sources": { + "version": "1.0.0", + "dev": true, + "requires": { + "normalize-path": "^2.0.1", + "through2": "^2.0.3" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "through2": { + "version": "2.0.5", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "@gulpjs/messages": { + "version": "1.1.0", + "dev": true + }, + "@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "dev": true, + "requires": { + "is-negated-glob": "^1.0.0" + } + }, + "@hapi/boom": { + "version": "9.1.4", + "dev": true, + "requires": { + "@hapi/hoek": "9.x.x" + } + }, + "@hapi/cryptiles": { + "version": "5.1.0", + "dev": true, + "requires": { + "@hapi/boom": "9.x.x" + } + }, + "@hapi/hoek": { + "version": "9.3.0", + "dev": true + }, + "@humanfs/core": { + "version": "0.19.1", + "dev": true + }, + "@humanfs/node": { + "version": "0.16.6", + "dev": true, + "requires": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "dependencies": { + "@humanwhocodes/retry": { + "version": "0.3.1", + "dev": true + } + } + }, + "@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "dev": true + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true + }, + "@humanwhocodes/retry": { + "version": "0.4.2", + "dev": true + }, + "@inquirer/checkbox": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.0.tgz", + "integrity": "sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/confirm": { + "version": "5.1.14", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.14.tgz", + "integrity": "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + } + }, + "@inquirer/core": { + "version": "10.1.15", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", + "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", + "dev": true, + "requires": { + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } + }, + "@inquirer/editor": { + "version": "4.2.16", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.16.tgz", + "integrity": "sha512-iSzLjT4C6YKp2DU0fr8T7a97FnRRxMO6CushJnW5ktxLNM2iNeuyUuUA5255eOLPORoGYCrVnuDOEBdGkHGkpw==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.15", + "@inquirer/external-editor": "^1.0.0", + "@inquirer/type": "^3.0.8" + } + }, + "@inquirer/expand": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.17.tgz", + "integrity": "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/external-editor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.0.tgz", + "integrity": "sha512-5v3YXc5ZMfL6OJqXPrX9csb4l7NlQA2doO1yynUjpUChT9hg4JcuBVP0RbsEJ/3SL/sxWEyFjT2W69ZhtoBWqg==", + "dev": true, + "requires": { + "chardet": "^2.1.0", + "iconv-lite": "^0.6.3" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "@inquirer/figures": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", + "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", + "dev": true + }, + "@inquirer/input": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.1.tgz", + "integrity": "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + } + }, + "@inquirer/number": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.17.tgz", + "integrity": "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + } + }, + "@inquirer/password": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.17.tgz", + "integrity": "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2" + } + }, + "@inquirer/prompts": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.0.tgz", + "integrity": "sha512-JHwGbQ6wjf1dxxnalDYpZwZxUEosT+6CPGD9Zh4sm9WXdtUp9XODCQD3NjSTmu+0OAyxWXNOqf0spjIymJa2Tw==", + "dev": true, + "requires": { + "@inquirer/checkbox": "^4.2.0", + "@inquirer/confirm": "^5.1.14", + "@inquirer/editor": "^4.2.15", + "@inquirer/expand": "^4.0.17", + "@inquirer/input": "^4.2.1", + "@inquirer/number": "^3.0.17", + "@inquirer/password": "^4.0.17", + "@inquirer/rawlist": "^4.1.5", + "@inquirer/search": "^3.1.0", + "@inquirer/select": "^4.3.1" + } + }, + "@inquirer/rawlist": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.5.tgz", + "integrity": "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/search": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.0.tgz", + "integrity": "sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/select": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.1.tgz", + "integrity": "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/type": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", + "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", + "dev": true, + "requires": {} + }, + "@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true + }, + "@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "requires": { + "@isaacs/balanced-match": "^4.0.1" + } + }, + "@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "6.2.1", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "dev": true + }, + "@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "peer": true + }, + "@jest/expect-utils": { + "version": "29.7.0", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3" + } + }, + "@jest/get-type": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", + "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", + "dev": true, + "peer": true + }, + "@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "peer": true, + "requires": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + } + }, + "@jest/schemas": { + "version": "29.6.3", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/types": { + "version": "29.6.3", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2" + }, + "@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.0" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "optional": true, + "requires": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "dev": true, + "requires": { + "eslint-scope": "5.1.1" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true + }, + "@open-draft/until": { + "version": "1.0.3", + "dev": true + }, + "@percy/appium-app": { + "version": "2.1.0", + "dev": true, + "requires": { + "@percy/sdk-utils": "^1.30.9", + "tmp": "^0.2.3" + } + }, + "@percy/sdk-utils": { + "version": "1.31.0", + "dev": true + }, + "@percy/selenium-webdriver": { + "version": "2.2.3", + "dev": true, + "requires": { + "@percy/sdk-utils": "^1.30.9", + "node-request-interceptor": "^0.6.3" + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "optional": true + }, + "@pkgr/core": { + "version": "0.1.1", + "dev": true + }, + "@polka/url": { + "version": "1.0.0-next.25", + "dev": true + }, + "@promptbook/utils": { + "version": "0.50.0-10", + "dev": true, + "requires": { + "moment": "2.30.1", + "prettier": "2.8.1", + "spacetrim": "0.11.25" + } + }, + "@puppeteer/browsers": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz", + "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==", + "dev": true, + "requires": { + "debug": "^4.4.1", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.2", + "tar-fs": "^3.0.8", + "yargs": "^17.7.2" + }, + "dependencies": { + "debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + } + } + }, + "@rtsao/scc": { + "version": "1.1.0", + "dev": true + }, + "@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true + }, + "@sinclair/typebox": { + "version": "0.27.8", + "dev": true + }, + "@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.1", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "13.0.5", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1" + } + }, + "@sinonjs/samsam": { + "version": "8.0.2", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + }, + "dependencies": { + "type-detect": { + "version": "4.1.0", + "dev": true + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.3", + "dev": true + }, + "@socket.io/component-emitter": { + "version": "3.1.2" + }, + "@stylistic/eslint-plugin": { + "version": "2.11.0", + "dev": true, + "requires": { + "@typescript-eslint/utils": "^8.13.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.2.0", + "dev": true + }, + "estraverse": { + "version": "5.3.0", + "dev": true + }, + "picomatch": { + "version": "4.0.2", + "dev": true + } + } + }, + "@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "dev": true + }, + "@tybys/wasm-util": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "dev": true, + "optional": true, + "requires": { + "tslib": "^2.4.0" + } + }, + "@types/cookie": { + "version": "0.4.1" + }, + "@types/cors": { + "version": "2.8.17", + "requires": { + "@types/node": "*" + } + }, + "@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "@types/expect": { + "version": "1.20.4", + "dev": true + }, + "@types/gitconfiglocal": { + "version": "2.0.3", + "dev": true + }, + "@types/google-publisher-tag": { + "version": "1.20250428.0", + "resolved": "https://registry.npmjs.org/@types/google-publisher-tag/-/google-publisher-tag-1.20250428.0.tgz", + "integrity": "sha512-W+aTMsM4e8PE/TkH/RkMbmmwEFg2si9eUugS5/lt88wkEClqcALi+3WLXW39Xgzu89+3igi/RNIpPLKdt6W7Dg==", + "dev": true + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.3", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.4", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/json-schema": { + "version": "7.0.15" + }, + "@types/json5": { + "version": "0.0.29", + "dev": true + }, + "@types/mocha": { + "version": "10.0.6", + "dev": true + }, + "@types/node": { + "version": "20.14.2", + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/normalize-package-data": { + "version": "2.4.4", + "dev": true + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.5", + "dev": true + }, + "@types/stack-utils": { + "version": "2.0.3", + "dev": true + }, + "@types/triple-beam": { + "version": "1.3.5", + "dev": true + }, + "@types/vinyl": { + "version": "2.0.12", + "dev": true, + "requires": { + "@types/expect": "^1.20.4", + "@types/node": "*" + } + }, + "@types/which": { + "version": "2.0.2", + "dev": true + }, + "@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/yargs": { + "version": "17.0.33", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.3", + "dev": true + }, + "@types/yauzl": { + "version": "2.10.3", + "dev": true, + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", + "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/type-utils": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "dependencies": { + "ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true + } + } + }, + "@typescript-eslint/parser": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", + "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/project-service": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", + "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", + "dev": true, + "requires": { + "@typescript-eslint/tsconfig-utils": "^8.39.0", + "@typescript-eslint/types": "^8.39.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", + "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0" + } + }, + "@typescript-eslint/tsconfig-utils": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", + "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", + "dev": true, + "requires": {} + }, + "@typescript-eslint/type-utils": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", + "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + } + }, + "@typescript-eslint/types": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", + "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", + "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", + "dev": true, + "requires": { + "@typescript-eslint/project-service": "8.39.0", + "@typescript-eslint/tsconfig-utils": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true + } + } + }, + "@typescript-eslint/utils": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", + "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", + "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.39.0", + "eslint-visitor-keys": "^4.2.1" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true + } + } + }, + "@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "dev": true, + "optional": true, + "requires": { + "@napi-rs/wasm-runtime": "^0.2.11" + } + }, + "@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "dev": true, + "optional": true + }, + "@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "dev": true, + "optional": true + }, + "@videojs/http-streaming": { + "version": "2.16.3", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "3.0.5", + "aes-decrypter": "3.1.3", + "global": "^4.4.0", + "m3u8-parser": "4.8.0", + "mpd-parser": "^0.22.1", + "mux.js": "6.0.1", + "video.js": "^6 || ^7" + } + }, + "@videojs/vhs-utils": { + "version": "3.0.5", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + } + }, + "@videojs/xhr": { + "version": "2.6.0", + "dev": true, + "requires": { + "@babel/runtime": "^7.5.5", + "global": "~4.4.0", + "is-function": "^1.0.1" + } + }, + "@vitest/pretty-format": { + "version": "2.1.9", + "dev": true, + "requires": { + "tinyrainbow": "^1.2.0" + } + }, + "@vitest/snapshot": { + "version": "2.1.9", + "dev": true, + "requires": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + } + }, + "@wdio/browserstack-service": { + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/@wdio/browserstack-service/-/browserstack-service-9.19.1.tgz", + "integrity": "sha512-9goHyn4PckZXp1GlYgTGslCFd+2BJPcrRjzWD8ziXJ7WWIC8/94+Y3Sp3HTnlZayrdSIjTXtHKZODP1qugVcWg==", + "dev": true, + "requires": { + "@browserstack/ai-sdk-node": "1.5.17", + "@percy/appium-app": "^2.0.9", + "@percy/selenium-webdriver": "^2.2.2", + "@types/gitconfiglocal": "^2.0.1", + "@wdio/logger": "9.18.0", + "@wdio/reporter": "9.19.1", + "@wdio/types": "9.19.1", + "browserstack-local": "^1.5.1", + "chalk": "^5.3.0", + "csv-writer": "^1.6.0", + "formdata-node": "5.0.1", + "git-repo-info": "^2.1.1", + "gitconfiglocal": "^2.1.0", + "undici": "^6.21.3", + "uuid": "^11.1.0", + "webdriverio": "9.19.1", + "winston-transport": "^4.5.0", + "yauzl": "^3.0.0" + }, + "dependencies": { + "@wdio/logger": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.18.0.tgz", + "integrity": "sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "safe-regex2": "^5.0.0", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/reporter": { + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-9.19.1.tgz", + "integrity": "sha512-nv5TZg+rUUlC3NGNDP2DGzpd2grU/3D27M9MsPV37TjGLccEVZYlbu2BnK3Y9mSqXWt7CZuFC4GXBF+5vQG6gw==", + "dev": true, + "requires": { + "@types/node": "^20.1.0", + "@wdio/logger": "9.18.0", + "@wdio/types": "9.19.1", + "diff": "^8.0.2", + "object-inspect": "^1.12.0" + } + }, + "@wdio/types": { + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.19.1.tgz", + "integrity": "sha512-Q1HVcXiWMHp3ze2NN1BvpsfEh/j6GtAeMHhHW4p2IWUfRZlZqTfiJ+95LmkwXOG2gw9yndT8NkJigAz8v7WVYQ==", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "chalk": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", + "dev": true + }, + "diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "dev": true + }, + "uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "dev": true + } + } + }, + "@wdio/cli": { + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-9.19.1.tgz", + "integrity": "sha512-1JDvutIp1mYk2f3KaBdcHAMw9UQlAMFnXbB4byuOMgik75HIgF+mrsasHj8wzfJTm9BbLwQ2h/6yGLHPTXvc0g==", + "dev": true, + "requires": { + "@vitest/snapshot": "^2.1.1", + "@wdio/config": "9.19.1", + "@wdio/globals": "9.17.0", + "@wdio/logger": "9.18.0", + "@wdio/protocols": "9.16.2", + "@wdio/types": "9.19.1", + "@wdio/utils": "9.19.1", + "async-exit-hook": "^2.0.1", + "chalk": "^5.4.1", + "chokidar": "^4.0.0", + "create-wdio": "9.18.2", + "dotenv": "^17.2.0", + "import-meta-resolve": "^4.0.0", + "lodash.flattendeep": "^4.4.0", + "lodash.pickby": "^4.6.0", + "lodash.union": "^4.6.0", + "read-pkg-up": "^10.0.0", + "tsx": "^4.7.2", + "webdriverio": "9.19.1", + "yargs": "^17.7.2" + }, + "dependencies": { + "@jest/expect-utils": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz", + "integrity": "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==", + "dev": true, + "peer": true, + "requires": { + "@jest/get-type": "30.0.1" + } + }, + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "peer": true, + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dev": true, + "peer": true, + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "peer": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "peer": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "@sinclair/typebox": { + "version": "0.34.38", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", + "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", + "dev": true, + "peer": true + }, + "@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "peer": true, + "requires": { + "tinyrainbow": "^2.0.0" + } + }, + "@wdio/config": { + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.19.1.tgz", + "integrity": "sha512-BeTB2paSjaij3cf1NXQzX9CZmdj5jz2/xdUhkJlCeGmGn1KjWu5BjMO+exuiy+zln7dOJjev8f0jlg8e8f1EbQ==", + "dev": true, + "requires": { + "@wdio/logger": "9.18.0", + "@wdio/types": "9.19.1", + "@wdio/utils": "9.19.1", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0" + } + }, + "@wdio/globals": { + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-9.17.0.tgz", + "integrity": "sha512-i38o7wlipLllNrk2hzdDfAmk6nrqm3lR2MtAgWgtHbwznZAKkB84KpkNFfmUXw5Kg3iP1zKlSjwZpKqenuLc+Q==", + "dev": true, + "requires": {} + }, + "@wdio/logger": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.18.0.tgz", + "integrity": "sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "safe-regex2": "^5.0.0", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/protocols": { + "version": "9.16.2", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.16.2.tgz", + "integrity": "sha512-h3k97/lzmyw5MowqceAuY3HX/wGJojXHkiPXA3WlhGPCaa2h4+GovV2nJtRvknCKsE7UHA1xB5SWeI8MzloBew==", + "dev": true + }, + "@wdio/types": { + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.19.1.tgz", + "integrity": "sha512-Q1HVcXiWMHp3ze2NN1BvpsfEh/j6GtAeMHhHW4p2IWUfRZlZqTfiJ+95LmkwXOG2gw9yndT8NkJigAz8v7WVYQ==", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/utils": { + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.19.1.tgz", + "integrity": "sha512-wWx5uPCgdZQxFIemAFVk/aa3JLwqrTsvEJsPlV3lCRpLeQ67V8aUPvvNAzE+RhX67qvelwwsvX8RrPdLDfnnYw==", + "dev": true, + "requires": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.18.0", + "@wdio/types": "9.19.1", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^6.1.2", + "geckodriver": "^5.0.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "mitt": "^3.0.1", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + } + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true + }, + "chalk": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz", + "integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==", + "dev": true + }, + "chokidar": { + "version": "4.0.3", + "dev": true, + "requires": { + "readdirp": "^4.0.1" + } + }, + "ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "peer": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "peer": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "peer": true + }, + "expect": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz", + "integrity": "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==", + "dev": true, + "peer": true, + "requires": { + "@jest/expect-utils": "30.0.5", + "@jest/get-type": "30.0.1", + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + } + }, + "expect-webdriverio": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/expect-webdriverio/-/expect-webdriverio-5.4.1.tgz", + "integrity": "sha512-jH4qhahRNGPWbCCVcCpLDl/kvFJ1eOzVnrd1K/sG1RhKr6bZsgZQUiOE3bafVqSOfKP+ay8bM/VagP4+XsO9Xw==", + "dev": true, + "peer": true, + "requires": { + "@vitest/snapshot": "^3.2.4", + "expect": "^30.0.0", + "jest-matcher-utils": "^30.0.0", + "lodash.isequal": "^4.5.0" + }, + "dependencies": { + "@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "peer": true, + "requires": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + } + } + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "peer": true + }, + "jest-diff": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz", + "integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==", + "dev": true, + "peer": true, + "requires": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "pretty-format": "30.0.5" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "peer": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "peer": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-matcher-utils": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz", + "integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==", + "dev": true, + "peer": true, + "requires": { + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "jest-diff": "30.0.5", + "pretty-format": "30.0.5" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "peer": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "peer": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-message-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", + "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", + "dev": true, + "peer": true, + "requires": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.5", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "peer": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "peer": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "jest-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "dev": true, + "peer": true, + "requires": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "peer": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "peer": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "peer": true + }, + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "peer": true + }, + "pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "dev": true, + "peer": true, + "requires": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + } + }, + "readdirp": { + "version": "4.1.2", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "peer": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "peer": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "peer": true + }, + "yargs": { + "version": "17.7.2", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + } + } + }, + "@wdio/concise-reporter": { + "version": "8.38.2", + "dev": true, + "requires": { + "@wdio/reporter": "8.38.2", + "@wdio/types": "8.38.2", + "chalk": "^5.0.1", + "pretty-ms": "^7.0.1" + }, + "dependencies": { + "chalk": { + "version": "5.3.0", + "dev": true + } + } + }, + "@wdio/config": { + "version": "9.15.0", + "dev": true, + "requires": { + "@wdio/logger": "9.15.0", + "@wdio/types": "9.15.0", + "@wdio/utils": "9.15.0", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0" + }, + "dependencies": { + "@wdio/logger": { + "version": "9.15.0", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/types": { + "version": "9.15.0", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/utils": { + "version": "9.15.0", + "dev": true, + "requires": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.15.0", + "@wdio/types": "9.15.0", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^6.1.1", + "geckodriver": "^5.0.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + } + }, + "chalk": { + "version": "5.4.1", + "dev": true + } + } + }, + "@wdio/dot-reporter": { + "version": "9.15.0", + "dev": true, + "requires": { + "@wdio/reporter": "9.15.0", + "@wdio/types": "9.15.0", + "chalk": "^5.0.1" + }, + "dependencies": { + "@wdio/logger": { + "version": "9.15.0", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/reporter": { + "version": "9.15.0", + "dev": true, + "requires": { + "@types/node": "^20.1.0", + "@wdio/logger": "9.15.0", + "@wdio/types": "9.15.0", + "diff": "^7.0.0", + "object-inspect": "^1.12.0" + } + }, + "@wdio/types": { + "version": "9.15.0", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "chalk": { + "version": "5.4.1", + "dev": true + }, + "diff": { + "version": "7.0.0", + "dev": true + } + } + }, + "@wdio/globals": { + "version": "9.15.0", + "dev": true, + "requires": { + "expect-webdriverio": "^5.1.0", + "webdriverio": "9.15.0" + }, + "dependencies": { + "@vitest/pretty-format": { + "version": "3.2.3", + "dev": true, + "optional": true, + "requires": { + "tinyrainbow": "^2.0.0" + } + }, + "@vitest/snapshot": { + "version": "3.2.3", + "dev": true, + "optional": true, + "requires": { + "@vitest/pretty-format": "3.2.3", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + } + }, + "@wdio/logger": { + "version": "9.15.0", + "dev": true, + "optional": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/types": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.15.0.tgz", + "integrity": "sha512-hR0Dm9TsrjtgOLWOjUMYTOB1hWIlnDzFgZt7XGOzI9Ig8Qa+TDfZSFaZukGxqLIZS/eGhxpnunSHaTAXwJIxYA==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/utils": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.15.0.tgz", + "integrity": "sha512-XuT1PE1nh4wwJfQW6IN4UT6+iv0+Yf4zhgMh5et04OX6tfrIXkWdx2SDimghDtRukp9i85DvIGWjdPEoQFQdaA==", + "dev": true, + "optional": true, + "requires": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.15.0", + "@wdio/types": "9.15.0", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^6.1.1", + "geckodriver": "^5.0.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + } + }, + "chalk": { + "version": "5.4.1", + "dev": true, + "optional": true + }, + "expect-webdriverio": { + "version": "5.3.0", + "dev": true, + "optional": true, + "requires": { + "@vitest/snapshot": "^3.2.1", + "expect": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "lodash.isequal": "^4.5.0" + } + }, + "htmlfy": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.6.7.tgz", + "integrity": "sha512-r8hRd+oIM10lufovN+zr3VKPTYEIvIwqXGucidh2XQufmiw6sbUXFUFjWlfjo3AnefIDTyzykVzQ8IUVuT1peQ==", + "dev": true, + "optional": true + }, + "pathe": { + "version": "2.0.3", + "dev": true, + "optional": true + }, + "serialize-error": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", + "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", + "dev": true, + "optional": true, + "requires": { + "type-fest": "^2.12.2" + } + }, + "tinyrainbow": { + "version": "2.0.0", + "dev": true, + "optional": true + }, + "type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "optional": true + }, + "webdriver": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.15.0.tgz", + "integrity": "sha512-JCW5xvhZtL6kjbckdePgVYMOlvWbh22F1VFkIf9pw3prwXI2EHED5Eq/nfDnNfHiqr0AfFKWmIDPziSafrVv4Q==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.15.0", + "@wdio/logger": "9.15.0", + "@wdio/protocols": "9.15.0", + "@wdio/types": "9.15.0", + "@wdio/utils": "9.15.0", + "deepmerge-ts": "^7.0.3", + "undici": "^6.20.1", + "ws": "^8.8.0" + } + }, + "webdriverio": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.15.0.tgz", + "integrity": "sha512-910g6ktwXdAKGyhgCPGw9BzIKOEBBYMFN1bLwC3bW/3mFlxGHO/n70c7Sg9hrsu9VWTzv6m+1Clf27B9uz4a/Q==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.15.0", + "@wdio/logger": "9.15.0", + "@wdio/protocols": "9.15.0", + "@wdio/repl": "9.4.4", + "@wdio/types": "9.15.0", + "@wdio/utils": "9.15.0", + "archiver": "^7.0.1", + "aria-query": "^5.3.0", + "cheerio": "^1.0.0-rc.12", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.6.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.3", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.15.0" + } + } + } + }, + "@wdio/local-runner": { + "version": "9.15.0", + "dev": true, + "requires": { + "@types/node": "^20.1.0", + "@wdio/logger": "9.15.0", + "@wdio/repl": "9.4.4", + "@wdio/runner": "9.15.0", + "@wdio/types": "9.15.0", + "async-exit-hook": "^2.0.1", + "split2": "^4.1.0", + "stream-buffers": "^3.0.2" + }, + "dependencies": { + "@wdio/logger": { + "version": "9.15.0", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/types": { + "version": "9.15.0", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "chalk": { + "version": "5.4.1", + "dev": true + } + } + }, + "@wdio/logger": { + "version": "8.38.0", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + }, + "dependencies": { + "chalk": { + "version": "5.3.0", + "dev": true + } + } + }, + "@wdio/mocha-framework": { + "version": "9.12.6", + "dev": true, + "requires": { + "@types/mocha": "^10.0.6", + "@types/node": "^20.11.28", + "@wdio/logger": "9.4.4", + "@wdio/types": "9.12.6", + "@wdio/utils": "9.12.6", + "mocha": "^10.3.0" + }, + "dependencies": { + "@wdio/logger": { + "version": "9.4.4", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/types": { + "version": "9.12.6", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "chalk": { + "version": "5.4.1", + "dev": true + } + } + }, + "@wdio/protocols": { + "version": "9.15.0", + "dev": true + }, + "@wdio/repl": { + "version": "9.4.4", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/reporter": { + "version": "8.38.2", + "dev": true, + "requires": { + "@types/node": "^20.1.0", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.38.2", + "diff": "^5.0.0", + "object-inspect": "^1.12.0" + } + }, + "@wdio/runner": { + "version": "9.15.0", + "dev": true, + "requires": { + "@types/node": "^20.11.28", + "@wdio/config": "9.15.0", + "@wdio/dot-reporter": "9.15.0", + "@wdio/globals": "9.15.0", + "@wdio/logger": "9.15.0", + "@wdio/types": "9.15.0", + "@wdio/utils": "9.15.0", + "deepmerge-ts": "^7.0.3", + "expect-webdriverio": "^5.1.0", + "webdriver": "9.15.0", + "webdriverio": "9.15.0" + }, + "dependencies": { + "@vitest/pretty-format": { + "version": "3.2.3", + "dev": true, + "requires": { + "tinyrainbow": "^2.0.0" + } + }, + "@vitest/snapshot": { + "version": "3.2.3", + "dev": true, + "requires": { + "@vitest/pretty-format": "3.2.3", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + } + }, + "@wdio/logger": { + "version": "9.15.0", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/types": { + "version": "9.15.0", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/utils": { + "version": "9.15.0", + "dev": true, + "requires": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.15.0", + "@wdio/types": "9.15.0", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^6.1.1", + "geckodriver": "^5.0.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + } + }, + "chalk": { + "version": "5.4.1", + "dev": true + }, + "expect-webdriverio": { + "version": "5.3.0", + "dev": true, + "requires": { + "@vitest/snapshot": "^3.2.1", + "expect": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "lodash.isequal": "^4.5.0" + } + }, + "htmlfy": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.6.7.tgz", + "integrity": "sha512-r8hRd+oIM10lufovN+zr3VKPTYEIvIwqXGucidh2XQufmiw6sbUXFUFjWlfjo3AnefIDTyzykVzQ8IUVuT1peQ==", + "dev": true + }, + "pathe": { + "version": "2.0.3", + "dev": true + }, + "serialize-error": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", + "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", + "dev": true, + "requires": { + "type-fest": "^2.12.2" + } + }, + "tinyrainbow": { + "version": "2.0.0", + "dev": true + }, + "type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true + }, + "webdriver": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.15.0.tgz", + "integrity": "sha512-JCW5xvhZtL6kjbckdePgVYMOlvWbh22F1VFkIf9pw3prwXI2EHED5Eq/nfDnNfHiqr0AfFKWmIDPziSafrVv4Q==", + "dev": true, + "requires": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.15.0", + "@wdio/logger": "9.15.0", + "@wdio/protocols": "9.15.0", + "@wdio/types": "9.15.0", + "@wdio/utils": "9.15.0", + "deepmerge-ts": "^7.0.3", + "undici": "^6.20.1", + "ws": "^8.8.0" + } + }, + "webdriverio": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.15.0.tgz", + "integrity": "sha512-910g6ktwXdAKGyhgCPGw9BzIKOEBBYMFN1bLwC3bW/3mFlxGHO/n70c7Sg9hrsu9VWTzv6m+1Clf27B9uz4a/Q==", + "dev": true, + "requires": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.15.0", + "@wdio/logger": "9.15.0", + "@wdio/protocols": "9.15.0", + "@wdio/repl": "9.4.4", + "@wdio/types": "9.15.0", + "@wdio/utils": "9.15.0", + "archiver": "^7.0.1", + "aria-query": "^5.3.0", + "cheerio": "^1.0.0-rc.12", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.6.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.3", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.15.0" + } + } + } + }, + "@wdio/spec-reporter": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.43.0.tgz", + "integrity": "sha512-Qy5LsGrGHbXJdj2PveNV7CD5g1XpLPvd7wH8bAd9OtuZRTPknyZqYSvB8TlZIV/SfiNlWEJ/05mI/FcixNJ6Xg==", + "dev": true, + "requires": { + "@wdio/reporter": "8.43.0", + "@wdio/types": "8.41.0", + "chalk": "^5.1.2", + "easy-table": "^1.2.0", + "pretty-ms": "^7.0.0" + }, + "dependencies": { + "@types/node": { + "version": "22.18.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz", + "integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==", + "dev": true, + "requires": { + "undici-types": "~6.21.0" + } + }, + "@wdio/reporter": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.43.0.tgz", + "integrity": "sha512-0ph8SabdMrgDmuLUEwA7yvkrvlCw4/EXttb3CGucjSkuiSbZNLhdTMXpyPoewh2soa253fIpnx79HztOsOzn5Q==", + "dev": true, + "requires": { + "@types/node": "^22.2.0", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.41.0", + "diff": "^7.0.0", + "object-inspect": "^1.12.0" + } + }, + "@wdio/types": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.41.0.tgz", + "integrity": "sha512-t4NaNTvJZci3Xv/yUZPH4eTL0hxrVTf5wdwNnYIBrzMnlRDbNefjQ0P7FM7ZjQCLaH92AEH6t/XanUId7Webug==", + "dev": true, + "requires": { + "@types/node": "^22.2.0" + } + }, + "chalk": { + "version": "5.3.0", + "dev": true + }, + "diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true + }, + "undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + } + } + }, + "@wdio/types": { + "version": "8.38.2", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/utils": { + "version": "9.12.6", + "dev": true, + "requires": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.4.4", + "@wdio/types": "9.12.6", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^6.1.1", + "geckodriver": "^5.0.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + }, + "dependencies": { + "@wdio/logger": { + "version": "9.4.4", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/types": { + "version": "9.12.6", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "chalk": { + "version": "5.4.1", + "dev": true + } + } + }, + "@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "@xmldom/xmldom": { + "version": "0.8.10", + "dev": true + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "@zip.js/zip.js": { + "version": "2.7.60", + "dev": true + }, + "abbrev": { + "version": "1.0.9", + "dev": true + }, + "abort-controller": { + "version": "3.0.0", + "dev": true, + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "accepts": { + "version": "1.3.8", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true + }, + "acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "requires": {} + }, + "acorn-jsx": { + "version": "5.3.2", + "dev": true, + "requires": {} + }, + "acorn-walk": { + "version": "8.3.2", + "dev": true + }, + "aes-decrypter": { + "version": "3.1.3", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "agent-base": { + "version": "6.0.2", + "dev": true, + "requires": { + "debug": "4" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, + "ajv-keywords": { + "version": "3.5.2", + "dev": true, + "requires": {} + }, + "amdefine": { + "version": "1.0.1", + "dev": true, + "optional": true + }, + "ansi-colors": { + "version": "4.1.3", + "dev": true + }, + "ansi-cyan": { + "version": "0.1.1", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } + } + }, + "ansi-gray": { + "version": "0.1.1", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-red": { + "version": "0.1.1", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-regex": { + "version": "5.0.1" + }, + "ansi-styles": { + "version": "3.2.1", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "ansi-wrap": { + "version": "0.1.0" + }, + "anymatch": { + "version": "3.1.3", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "archiver": { + "version": "7.0.1", + "dev": true, + "requires": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "dependencies": { + "async": { + "version": "3.2.6", + "dev": true + }, + "buffer": { + "version": "6.0.3", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-crc32": { + "version": "1.0.0", + "dev": true + }, + "readable-stream": { + "version": "4.5.2", + "dev": true, + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "archiver-utils": { + "version": "5.0.2", + "dev": true, + "requires": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "is-stream": { + "version": "2.0.1", + "dev": true + }, + "readable-stream": { + "version": "4.5.2", + "dev": true, + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "are-docs-informative": { + "version": "0.0.2", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "aria-query": { + "version": "5.3.0", + "dev": true, + "requires": { + "dequal": "^2.0.3" + } + }, + "arr-diff": { + "version": "1.1.0", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1", + "array-slice": "^0.2.3" + }, + "dependencies": { + "array-slice": { + "version": "0.2.3", + "dev": true + } + } + }, + "arr-flatten": { + "version": "1.1.0", + "dev": true + }, + "arr-union": { + "version": "2.1.0", + "dev": true + }, + "array-buffer-byte-length": { + "version": "1.0.2", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + } + }, + "array-differ": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-4.0.0.tgz", + "integrity": "sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw==", + "dev": true + }, + "array-each": { + "version": "1.0.1", + "dev": true + }, + "array-flatten": { + "version": "1.1.1" + }, + "array-includes": { + "version": "3.1.8", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + } + }, + "array-slice": { + "version": "1.1.0", + "dev": true + }, + "array-union": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", + "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", + "dev": true + }, + "array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "array.prototype.findlastindex": { + "version": "1.2.5", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "array.prototype.flat": { + "version": "1.3.2", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flatmap": { + "version": "1.3.3", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + } + }, + "array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "arraybuffer.prototype.slice": { + "version": "1.0.4", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + } + }, + "assert": { + "version": "2.1.0", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, + "assertion-error": { + "version": "1.1.0", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0" + }, + "ast-types": { + "version": "0.13.4", + "dev": true, + "requires": { + "tslib": "^2.0.1" + } + }, + "async": { + "version": "1.5.2", + "dev": true + }, + "async-done": { + "version": "2.0.0", + "dev": true, + "requires": { + "end-of-stream": "^1.4.4", + "once": "^1.4.0", + "stream-exhaust": "^1.0.2" + } + }, + "async-exit-hook": { + "version": "2.0.1", + "dev": true + }, + "async-function": { + "version": "1.0.0", + "dev": true + }, + "async-settle": { + "version": "2.0.0", + "dev": true, + "requires": { + "async-done": "^2.0.0" + } + }, + "asynckit": { + "version": "0.4.0", + "dev": true + }, + "atob": { + "version": "2.1.2", + "dev": true + }, + "available-typed-arrays": { + "version": "1.0.7", + "dev": true, + "requires": { + "possible-typed-array-names": "^1.0.0" + } + }, + "axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "dev": true, + "requires": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "b4a": { + "version": "1.6.6", + "dev": true + }, + "babel-loader": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.4.1.tgz", + "integrity": "sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==", + "dev": true, + "requires": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.4", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "dependencies": { + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "requires": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.11.1", + "requires": { + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "requires": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + } + }, + "bach": { + "version": "2.0.1", + "dev": true, + "requires": { + "async-done": "^2.0.0", + "async-settle": "^2.0.0", + "now-and-later": "^3.0.0" + } + }, + "balanced-match": { + "version": "1.0.2" + }, + "bare-events": { + "version": "2.5.4", + "dev": true, + "optional": true + }, + "bare-fs": { + "version": "4.1.2", + "dev": true, + "optional": true, + "requires": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" + } + }, + "bare-os": { + "version": "3.6.1", + "dev": true, + "optional": true + }, + "bare-path": { + "version": "3.0.0", + "dev": true, + "optional": true, + "requires": { + "bare-os": "^3.0.1" + } + }, + "bare-stream": { + "version": "2.6.5", + "dev": true, + "optional": true, + "requires": { + "streamx": "^2.21.0" + } + }, + "base64-js": { + "version": "1.5.1", + "dev": true + }, + "base64id": { + "version": "2.0.0" + }, + "baseline-browser-mapping": { + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==" + }, + "basic-auth": { + "version": "2.0.1", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "dev": true + } + } + }, + "basic-ftp": { + "version": "5.0.5", + "dev": true + }, + "batch": { + "version": "0.6.1", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "dev": true + }, + "binary-extensions": { + "version": "2.3.0" + }, + "binaryextensions": { + "version": "2.3.0", + "dev": true + }, + "bl": { + "version": "5.1.0", + "dev": true, + "requires": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "3.6.2", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "bluebird": { + "version": "3.7.2" + }, + "body": { + "version": "5.1.0", + "dev": true, + "requires": { + "continuable-cache": "^0.3.1", + "error": "^7.0.0", + "raw-body": "~1.1.0", + "safe-json-parse": "~1.0.1" + }, + "dependencies": { + "bytes": { + "version": "1.0.0", + "dev": true + }, + "raw-body": { + "version": "1.1.7", + "dev": true, + "requires": { + "bytes": "1", + "string_decoder": "0.10" + } + }, + "string_decoder": { + "version": "0.10.31", + "dev": true + } + } + }, + "body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "requires": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "requires": { + "ms": "2.0.0" + } + }, + "http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "requires": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + } + }, + "ms": { + "version": "2.0.0" + }, + "statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" + } + } + }, + "boolbase": { + "version": "1.0.0", + "dev": true + }, + "brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.3", + "requires": { + "fill-range": "^7.1.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "dev": true + }, + "browserslist": { + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "requires": { + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + } + }, + "browserstack": { + "version": "1.5.3", + "dev": true, + "requires": { + "https-proxy-agent": "^2.2.1" + }, + "dependencies": { + "agent-base": { + "version": "4.3.0", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "debug": { + "version": "3.2.7", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "dev": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + } + } + }, + "browserstack-local": { + "version": "1.5.5", + "dev": true, + "requires": { + "agent-base": "^6.0.2", + "https-proxy-agent": "^5.0.1", + "is-running": "^2.1.0", + "ps-tree": "=1.2.0", + "temp-fs": "^0.9.9" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "dev": true + }, + "buffer-from": { + "version": "1.1.2", + "dev": true + }, + "bufferstreams": { + "version": "1.0.1", + "requires": { + "readable-stream": "^1.0.33" + }, + "dependencies": { + "isarray": { + "version": "0.0.1" + }, + "readable-stream": { + "version": "1.1.14", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31" + } + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.8", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + } + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, + "callsites": { + "version": "3.1.0", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "dev": true + }, + "can-autoplay": { + "version": "3.0.2", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==" + }, + "chai": { + "version": "4.4.1", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + } + }, + "chalk": { + "version": "2.4.2", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", + "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", + "dev": true + }, + "check-error": { + "version": "1.0.3", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } + }, + "cheerio": { + "version": "1.0.0", + "dev": true, + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "dependencies": { + "parse5": { + "version": "7.1.2", + "dev": true, + "requires": { + "entities": "^4.4.0" + } + } + } + }, + "cheerio-select": { + "version": "2.1.0", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + } + }, + "chokidar": { + "version": "3.6.0", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chrome-trace-event": { + "version": "1.0.4", + "dev": true + }, + "chromium-bidi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz", + "integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==", + "dev": true, + "requires": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + } + }, + "ci-info": { + "version": "3.9.0", + "dev": true + }, + "cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "clone": { + "version": "2.1.2", + "dev": true + }, + "clone-buffer": { + "version": "1.0.0", + "dev": true + }, + "clone-deep": { + "version": "4.0.1", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } + } + }, + "clone-stats": { + "version": "1.0.0", + "dev": true + }, + "cloneable-readable": { + "version": "1.1.3", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } + }, + "color-convert": { + "version": "1.9.3", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "dev": true + }, + "color-support": { + "version": "1.1.3", + "dev": true + }, + "colors": { + "version": "1.4.0", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "comment-parser": { + "version": "1.4.1", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "dev": true + }, + "compress-commons": { + "version": "6.0.2", + "dev": true, + "requires": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "is-stream": { + "version": "2.0.1", + "dev": true + }, + "readable-stream": { + "version": "4.5.2", + "dev": true, + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "concat-map": { + "version": "0.0.1" + }, + "concat-with-sourcemaps": { + "version": "1.1.0", + "dev": true, + "requires": { + "source-map": "^0.6.1" + } + }, + "connect": { + "version": "3.7.0", + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "requires": { + "ms": "2.0.0" + } + }, + "finalhandler": { + "version": "1.1.2", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "ms": { + "version": "2.0.0" + }, + "on-finished": { + "version": "2.3.0", + "requires": { + "ee-first": "1.1.1" + } + }, + "statuses": { + "version": "1.5.0" + } + } + }, + "connect-livereload": { + "version": "0.6.1", + "dev": true + }, + "consolidate": { + "version": "0.15.1", + "requires": { + "bluebird": "^3.1.1" + } + }, + "content-disposition": { + "version": "0.5.4", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5" + }, + "continuable-cache": { + "version": "0.3.1", + "dev": true + }, + "convert-source-map": { + "version": "2.0.0" + }, + "cookie": { + "version": "0.7.1" + }, + "cookie-signature": { + "version": "1.0.6" + }, + "copy-props": { + "version": "4.0.0", + "dev": true, + "requires": { + "each-props": "^3.0.0", + "is-plain-object": "^5.0.0" + } + }, + "core-js": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz", + "integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==" + }, + "core-js-compat": { + "version": "3.42.0", + "requires": { + "browserslist": "^4.24.4" + } + }, + "core-util-is": { + "version": "1.0.3" + }, + "cors": { + "version": "2.8.5", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "requires": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + } + } + }, + "crc-32": { + "version": "1.2.2", + "dev": true + }, + "crc32-stream": { + "version": "6.0.0", + "dev": true, + "requires": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.5.2", + "dev": true, + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "create-wdio": { + "version": "9.18.2", + "resolved": "https://registry.npmjs.org/create-wdio/-/create-wdio-9.18.2.tgz", + "integrity": "sha512-atf81YJfyTNAJXsNu3qhpqF4OO43tHGTpr88duAc1Hk4a0uXJAPUYLnYxshOuMnfmeAxlWD+NqGU7orRiXEuJg==", + "dev": true, + "requires": { + "chalk": "^5.3.0", + "commander": "^14.0.0", + "cross-spawn": "^7.0.3", + "ejs": "^3.1.10", + "execa": "^9.6.0", + "import-meta-resolve": "^4.1.0", + "inquirer": "^12.7.0", + "normalize-package-data": "^7.0.0", + "read-pkg-up": "^10.1.0", + "recursive-readdir": "^2.2.3", + "semver": "^7.6.3", + "type-fest": "^4.41.0", + "yargs": "^17.7.2" + }, + "dependencies": { + "chalk": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz", + "integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==", + "dev": true + }, + "commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "dev": true + }, + "execa": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz", + "integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==", + "dev": true, + "requires": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + } + }, + "get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "requires": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + } + }, + "hosted-git-info": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", + "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", + "dev": true, + "requires": { + "lru-cache": "^10.0.1" + } + }, + "is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true + }, + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "normalize-package-data": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-7.0.1.tgz", + "integrity": "sha512-linxNAT6M0ebEYZOx2tO6vBEFsVgnPpv+AVjk0wJHfaUIbq31Jm3T6vvZaarnOeWDh8ShnwXuaAyM7WT3RzErA==", + "dev": true, + "requires": { + "hosted-git-info": "^8.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + } + }, + "npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "requires": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + } + }, + "parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true + }, + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + }, + "pretty-ms": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", + "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", + "dev": true, + "requires": { + "parse-ms": "^4.0.0" + } + }, + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + } + } + }, + "cross-spawn": { + "version": "7.0.6", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "isexe": { + "version": "2.0.0", + "dev": true + }, + "which": { + "version": "2.0.2", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "crypto-js": { + "version": "4.2.0" + }, + "css": { + "version": "3.0.0", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, + "css-select": { + "version": "5.1.0", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-shorthand-properties": { + "version": "1.1.1", + "dev": true + }, + "css-value": { + "version": "0.0.1", + "dev": true + }, + "css-what": { + "version": "6.1.0", + "dev": true + }, + "csv-writer": { + "version": "1.6.0", + "dev": true + }, + "custom-event": { + "version": "1.0.1" + }, + "d": { + "version": "1.0.2", + "dev": true, + "requires": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + } + }, + "data-uri-to-buffer": { + "version": "4.0.1", + "dev": true + }, + "data-view-buffer": { + "version": "1.0.2", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + } + }, + "data-view-byte-length": { + "version": "1.0.2", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + } + }, + "data-view-byte-offset": { + "version": "1.0.1", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "date-format": { + "version": "4.0.14" + }, + "debounce": { + "version": "1.2.1", + "dev": true + }, + "debug": { + "version": "4.3.6", + "requires": { + "ms": "2.1.2" + } + }, + "debug-fabulous": { + "version": "1.1.0", + "dev": true, + "requires": { + "debug": "3.X", + "memoizee": "0.4.X", + "object-assign": "4.X" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "decamelize": { + "version": "6.0.0", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.2", + "dev": true + }, + "deep-eql": { + "version": "4.1.4", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-equal": { + "version": "2.2.3", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + } + }, + "deep-is": { + "version": "0.1.4", + "dev": true + }, + "deepmerge-ts": { + "version": "7.1.5", + "dev": true + }, + "defaults": { + "version": "1.0.4", + "dev": true, + "optional": true, + "requires": { + "clone": "^1.0.2" + }, + "dependencies": { + "clone": { + "version": "1.0.4", + "dev": true, + "optional": true + } + } + }, + "define-data-property": { + "version": "1.1.4", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "define-properties": { + "version": "1.2.1", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "degenerator": { + "version": "5.0.1", + "dev": true, + "requires": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "dependencies": { + "escodegen": { + "version": "2.1.0", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "dev": true + }, + "estraverse": { + "version": "5.3.0", + "dev": true + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "dev": true + }, + "depd": { + "version": "2.0.0" + }, + "dequal": { + "version": "2.0.3", + "dev": true + }, + "destroy": { + "version": "1.2.0" + }, + "detect-file": { + "version": "1.0.0", + "dev": true + }, + "detect-newline": { + "version": "2.1.0", + "dev": true + }, + "devtools-protocol": { + "version": "0.0.1464554", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz", + "integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==", + "dev": true + }, + "di": { + "version": "0.0.1" + }, + "diff": { + "version": "5.2.0", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "dev": true + }, + "dlv": { + "version": "1.1.3" + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-serialize": { + "version": "2.2.1", + "requires": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "dom-serializer": { + "version": "2.0.0", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "dom-walk": { + "version": "0.1.2", + "dev": true + }, + "domelementtype": { + "version": "2.3.0", + "dev": true + }, + "domhandler": { + "version": "5.0.3", + "dev": true, + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "dev": true, + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "dev": true + }, + "dset": { + "version": "3.1.4" + }, + "dunder-proto": { + "version": "1.0.1", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "duplexer": { + "version": "0.1.2", + "dev": true + }, + "duplexify": { + "version": "4.1.3", + "dev": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "each-props": { + "version": "3.0.0", + "dev": true, + "requires": { + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0" + } + }, + "eastasianwidth": { + "version": "0.2.0", + "dev": true + }, + "easy-table": { + "version": "1.2.0", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1", + "wcwidth": "^1.0.1" + } + }, + "edge-paths": { + "version": "3.0.5", + "dev": true, + "requires": { + "@types/which": "^2.0.1", + "which": "^2.0.2" + }, + "dependencies": { + "isexe": { + "version": "2.0.0", + "dev": true + }, + "which": { + "version": "2.0.2", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "edgedriver": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-6.1.2.tgz", + "integrity": "sha512-UvFqd/IR81iPyWMcxXbUNi+xKWR7JjfoHjfuwjqsj9UHQKn80RpQmS0jf+U25IPi+gKVPcpOSKm0XkqgGMq4zQ==", + "dev": true, + "requires": { + "@wdio/logger": "^9.1.3", + "@zip.js/zip.js": "^2.7.53", + "decamelize": "^6.0.0", + "edge-paths": "^3.0.5", + "fast-xml-parser": "^5.0.8", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^3.3.2", + "which": "^5.0.0" + }, + "dependencies": { + "@wdio/logger": { + "version": "9.15.0", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "agent-base": { + "version": "7.1.3", + "dev": true + }, + "chalk": { + "version": "5.4.1", + "dev": true + }, + "https-proxy-agent": { + "version": "7.0.6", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + } + } + }, + "ee-first": { + "version": "1.1.1" + }, + "ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "requires": { + "jake": "^10.8.5" + } + }, + "electron-to-chromium": { + "version": "1.5.237", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", + "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==" + }, + "emoji-regex": { + "version": "8.0.0" + }, + "emojis-list": { + "version": "3.0.0", + "dev": true + }, + "encodeurl": { + "version": "1.0.2" + }, + "encoding-sniffer": { + "version": "0.2.0", + "dev": true, + "requires": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "end-of-stream": { + "version": "1.4.4", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "engine.io": { + "version": "6.6.2", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "dependencies": { + "cookie": { + "version": "0.7.2" + } + } + }, + "engine.io-parser": { + "version": "5.2.3" + }, + "enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "ent": { + "version": "2.2.0" + }, + "entities": { + "version": "4.5.0", + "dev": true + }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true + }, + "errno": { + "version": "0.1.8", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error": { + "version": "7.2.1", + "dev": true, + "requires": { + "string-template": "~0.2.1" + } + }, + "error-ex": { + "version": "1.3.2", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.23.9", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + } + }, + "es-define-property": { + "version": "1.0.1" + }, + "es-errors": { + "version": "1.3.0" + }, + "es-get-iterator": { + "version": "1.1.3", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + } + }, + "es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + } + }, + "es-module-lexer": { + "version": "1.5.3", + "dev": true + }, + "es-object-atoms": { + "version": "1.1.1", + "requires": { + "es-errors": "^1.3.0" + } + }, + "es-set-tostringtag": { + "version": "2.1.0", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, + "es-shim-unscopables": { + "version": "1.0.2", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "es-to-primitive": { + "version": "1.3.0", + "dev": true, + "requires": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + } + }, + "es5-ext": { + "version": "0.10.64", + "dev": true, + "requires": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-promise": { + "version": "4.2.8" + }, + "es6-promisify": { + "version": "5.0.0", + "dev": true, + "requires": { + "es6-promise": "^4.0.3" + } + }, + "es6-symbol": { + "version": "3.1.4", + "dev": true, + "requires": { + "d": "^1.0.2", + "ext": "^1.7.0" + } + }, + "es6-weak-map": { + "version": "2.0.3", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "esbuild": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" + } + }, + "escalade": { + "version": "3.2.0" + }, + "escape-html": { + "version": "1.0.3" + }, + "escape-string-regexp": { + "version": "1.0.5", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "dev": true, + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + }, + "dependencies": { + "estraverse": { + "version": "1.9.3", + "dev": true + }, + "levn": { + "version": "0.3.0", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "optionator": { + "version": "0.8.3", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "prelude-ls": { + "version": "1.1.2", + "dev": true + }, + "source-map": { + "version": "0.2.0", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "type-check": { + "version": "0.3.2", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + } + } + }, + "eslint": { + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.31.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "dev": true + }, + "eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "has-flag": { + "version": "4.0.0", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "supports-color": { + "version": "7.2.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "dev": true + } + } + }, + "eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "requires": { + "semver": "^7.5.4" + }, + "dependencies": { + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true + } + } + }, + "eslint-import-context": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", + "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", + "dev": true, + "requires": { + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.2.0" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.9", + "dev": true, + "requires": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "requires": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "dependencies": { + "debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.12.0", + "dev": true, + "requires": { + "debug": "^3.2.7" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-plugin-chai-friendly": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-1.1.0.tgz", + "integrity": "sha512-+T1rClpDdXkgBAhC16vRQMI5umiWojVqkj9oUTdpma50+uByCZM/oBfxitZiOkjMRlm725mwFfz/RVgyDRvCKA==", + "dev": true, + "requires": {} + }, + "eslint-plugin-es-x": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", + "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.1.2", + "@eslint-community/regexpp": "^4.11.0", + "eslint-compat-utils": "^0.5.1" + } + }, + "eslint-plugin-import": { + "version": "2.31.0", + "dev": true, + "requires": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-plugin-import-x": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.16.1.tgz", + "integrity": "sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "^8.35.0", + "comment-parser": "^1.4.1", + "debug": "^4.4.1", + "eslint-import-context": "^0.1.9", + "is-glob": "^4.0.3", + "minimatch": "^9.0.3 || ^10.0.1", + "semver": "^7.7.2", + "stable-hash-x": "^0.2.0", + "unrs-resolver": "^1.9.2" + }, + "dependencies": { + "debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "requires": { + "@isaacs/brace-expansion": "^5.0.0" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true + } + } + }, + "eslint-plugin-jsdoc": { + "version": "50.6.6", + "dev": true, + "requires": { + "@es-joy/jsdoccomment": "~0.49.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.3.6", + "escape-string-regexp": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.6.0", + "parse-imports": "^2.1.1", + "semver": "^7.6.3", + "spdx-expression-parse": "^4.0.0", + "synckit": "^0.9.1" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "dev": true + }, + "semver": { + "version": "7.7.1", + "dev": true + }, + "spdx-expression-parse": { + "version": "4.0.0", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + } + } + }, + "eslint-plugin-n": { + "version": "17.21.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.21.3.tgz", + "integrity": "sha512-MtxYjDZhMQgsWRm/4xYLL0i2EhusWT7itDxlJ80l1NND2AL2Vi5Mvneqv/ikG9+zpran0VsVRXTEHrpLmUZRNw==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.5.0", + "enhanced-resolve": "^5.17.1", + "eslint-plugin-es-x": "^7.8.0", + "get-tsconfig": "^4.8.1", + "globals": "^15.11.0", + "globrex": "^0.1.2", + "ignore": "^5.3.2", + "semver": "^7.6.3", + "ts-declaration-location": "^1.0.6" + }, + "dependencies": { + "globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true + }, + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true + } + } + }, + "eslint-plugin-promise": { + "version": "7.2.1", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0" + } + }, + "eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "requires": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + } + } + }, + "eslint-scope": { + "version": "5.1.1", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-visitor-keys": { + "version": "2.1.0", + "dev": true + }, + "esniff": { + "version": "2.0.1", + "dev": true, + "requires": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + } + }, + "espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "requires": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true + } + } + }, + "esprima": { + "version": "2.7.3", + "dev": true + }, + "esquery": { + "version": "1.6.0", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "dev": true + }, + "esutils": { + "version": "2.0.3" + }, + "etag": { + "version": "1.8.1" + }, + "event-emitter": { + "version": "0.3.5", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "event-stream": { + "version": "3.3.4", + "dev": true, + "requires": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + }, + "dependencies": { + "map-stream": { + "version": "0.1.0", + "dev": true + } + } + }, + "event-target-shim": { + "version": "5.0.1", + "dev": true + }, + "eventemitter3": { + "version": "4.0.7" + }, + "events": { + "version": "3.3.0", + "dev": true + }, + "execa": { + "version": "1.0.0", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.6", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "isexe": { + "version": "2.0.0", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "dev": true + }, + "semver": { + "version": "5.7.2", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "dev": true + }, + "which": { + "version": "1.3.1", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "expect": { + "version": "29.7.0", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "requires": { + "ms": "2.0.0" + } + }, + "encodeurl": { + "version": "2.0.0" + }, + "ms": { + "version": "2.0.0" + } + } + }, + "ext": { + "version": "1.7.0", + "dev": true, + "requires": { + "type": "^2.7.2" + } + }, + "extend": { + "version": "3.0.2" + }, + "extend-shallow": { + "version": "1.1.4", + "dev": true, + "requires": { + "kind-of": "^1.1.0" + }, + "dependencies": { + "kind-of": { + "version": "1.1.0", + "dev": true + } + } + }, + "extract-zip": { + "version": "2.0.1", + "dev": true, + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "yauzl": { + "version": "2.10.0", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } + }, + "faker": { + "version": "5.5.3", + "dev": true + }, + "fancy-log": { + "version": "2.0.0", + "dev": true, + "requires": { + "color-support": "^1.1.3" + } + }, + "fast-deep-equal": { + "version": "3.1.3" + }, + "fast-fifo": { + "version": "1.3.2", + "dev": true + }, + "fast-glob": { + "version": "3.3.3", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "dev": true + }, + "fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==" + }, + "fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "dev": true, + "requires": { + "strnum": "^2.1.0" + } + }, + "fastest-levenshtein": { + "version": "1.0.16", + "dev": true + }, + "fastq": { + "version": "1.19.1", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "faye-websocket": { + "version": "0.10.0", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fd-slicer": { + "version": "1.1.0", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, + "fecha": { + "version": "4.2.3", + "dev": true + }, + "fetch-blob": { + "version": "3.2.0", + "dev": true, + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "dependencies": { + "web-streams-polyfill": { + "version": "3.3.3", + "dev": true + } + } + }, + "figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "requires": { + "is-unicode-supported": "^2.0.0" + } + }, + "file-entry-cache": { + "version": "8.0.0", + "dev": true, + "requires": { + "flat-cache": "^4.0.0" + } + }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "fill-range": { + "version": "7.1.1", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.3.1", + "requires": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "requires": { + "ms": "2.0.0" + } + }, + "encodeurl": { + "version": "2.0.0" + }, + "ms": { + "version": "2.0.0" + } + } + }, + "find-cache-dir": { + "version": "3.3.2", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "findup-sync": { + "version": "5.0.0", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + } + }, + "fined": { + "version": "2.0.0", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0", + "object.pick": "^1.3.0", + "parse-filepath": "^1.0.2" + } + }, + "flagged-respawn": { + "version": "2.0.0", + "dev": true + }, + "flat": { + "version": "5.0.2", + "dev": true + }, + "flat-cache": { + "version": "4.0.1", + "dev": true, + "requires": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + } + }, + "flatted": { + "version": "3.3.1" + }, + "follow-redirects": { + "version": "1.15.6" + }, + "for-each": { + "version": "0.3.5", + "dev": true, + "requires": { + "is-callable": "^1.2.7" + } + }, + "for-in": { + "version": "1.0.2", + "dev": true + }, + "for-own": { + "version": "1.0.0", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "foreground-child": { + "version": "3.3.0", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "dev": true + } + } + }, + "fork-stream": { + "version": "0.0.4", + "dev": true + }, + "form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + } + }, + "formdata-node": { + "version": "5.0.1", + "dev": true, + "requires": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + } + }, + "formdata-polyfill": { + "version": "4.0.10", + "dev": true, + "requires": { + "fetch-blob": "^3.1.2" + } + }, + "forwarded": { + "version": "0.2.0" + }, + "fresh": { + "version": "0.5.2" + }, + "from": { + "version": "0.1.7", + "dev": true + }, + "fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs-mkdirp-stream": { + "version": "2.0.1", + "dev": true, + "requires": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + } + }, + "fs-readfile-promise": { + "version": "3.0.1", + "requires": { + "graceful-fs": "^4.1.11" + } + }, + "fs.realpath": { + "version": "1.0.0" + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "optional": true + }, + "fun-hooks": { + "version": "1.1.0", + "requires": { + "typescript-tuple": "^2.2.1" + } + }, + "function-bind": { + "version": "1.1.2" + }, + "function.prototype.name": { + "version": "1.1.8", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + } + }, + "functions-have-names": { + "version": "1.2.3", + "dev": true + }, + "geckodriver": { + "version": "5.0.0", + "dev": true, + "requires": { + "@wdio/logger": "^9.1.3", + "@zip.js/zip.js": "^2.7.53", + "decamelize": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^3.3.2", + "tar-fs": "^3.0.6", + "which": "^5.0.0" + }, + "dependencies": { + "@wdio/logger": { + "version": "9.15.0", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + } + }, + "agent-base": { + "version": "7.1.3", + "dev": true + }, + "chalk": { + "version": "5.4.1", + "dev": true + }, + "https-proxy-agent": { + "version": "7.0.6", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + } + } + }, + "gensync": { + "version": "1.0.0-beta.2" + }, + "get-caller-file": { + "version": "2.0.5" + }, + "get-func-name": { + "version": "2.0.2", + "dev": true + }, + "get-intrinsic": { + "version": "1.3.0", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-package-type": { + "version": "0.1.0", + "dev": true + }, + "get-port": { + "version": "7.1.0", + "dev": true + }, + "get-proto": { + "version": "1.0.1", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-symbol-description": { + "version": "1.1.0", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + } + }, + "get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "requires": { + "resolve-pkg-maps": "^1.0.0" + } + }, + "get-uri": { + "version": "6.0.4", + "dev": true, + "requires": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "dependencies": { + "data-uri-to-buffer": { + "version": "6.0.2", + "dev": true + } + } + }, + "git-repo-info": { + "version": "2.1.1", + "dev": true + }, + "gitconfiglocal": { + "version": "2.1.0", + "dev": true, + "requires": { + "ini": "^1.3.2" + }, + "dependencies": { + "ini": { + "version": "1.3.8", + "dev": true + } + } + }, + "glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.5", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "glob-parent": { + "version": "5.1.2", + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-stream": { + "version": "8.0.3", + "dev": true, + "requires": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" + }, + "dependencies": { + "glob-parent": { + "version": "6.0.2", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + } + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "glob-watcher": { + "version": "6.0.0", + "dev": true, + "requires": { + "async-done": "^2.0.0", + "chokidar": "^3.5.3" + } + }, + "global": { + "version": "4.4.0", + "dev": true, + "requires": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "global-modules": { + "version": "1.0.0", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "dependencies": { + "ini": { + "version": "1.3.8", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "dev": true + }, + "which": { + "version": "1.3.1", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "globals": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "dev": true + }, + "globalthis": { + "version": "1.0.4", + "dev": true, + "requires": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + } + }, + "globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, + "glogg": { + "version": "2.2.0", + "dev": true, + "requires": { + "sparkles": "^2.1.0" + } + }, + "gopd": { + "version": "1.2.0" + }, + "graceful-fs": { + "version": "4.2.11" + }, + "grapheme-splitter": { + "version": "1.0.4", + "dev": true + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "gulp": { + "version": "5.0.1", + "dev": true, + "requires": { + "glob-watcher": "^6.0.0", + "gulp-cli": "^3.1.0", + "undertaker": "^2.0.0", + "vinyl-fs": "^4.0.2" + } + }, + "gulp-babel": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gulp-babel/-/gulp-babel-8.0.0.tgz", + "integrity": "sha512-oomaIqDXxFkg7lbpBou/gnUkX51/Y/M2ZfSjL2hdqXTAlSWZcgZtd2o0cOH0r/eE8LWD0+Q/PsLsr2DKOoqToQ==", + "requires": { + "plugin-error": "^1.0.1", + "replace-ext": "^1.0.0", + "through2": "^2.0.0", + "vinyl-sourcemaps-apply": "^0.2.0" + }, + "dependencies": { + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "requires": { + "ansi-wrap": "^0.1.0" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + }, + "replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==" + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "gulp-clean": { + "version": "0.4.0", + "dev": true, + "requires": { + "fancy-log": "^1.3.2", + "plugin-error": "^0.1.2", + "rimraf": "^2.6.2", + "through2": "^2.0.3", + "vinyl": "^2.1.0" + }, + "dependencies": { + "fancy-log": { + "version": "1.3.3", + "dev": true, + "requires": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" + } + }, + "glob": { + "version": "7.2.3", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "plugin-error": { + "version": "0.1.2", + "dev": true, + "requires": { + "ansi-cyan": "^0.1.1", + "ansi-red": "^0.1.1", + "arr-diff": "^1.0.1", + "arr-union": "^2.0.1", + "extend-shallow": "^1.1.2" + } + }, + "rimraf": { + "version": "2.7.1", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "through2": { + "version": "2.0.5", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "gulp-cli": { + "version": "3.1.0", + "dev": true, + "requires": { + "@gulpjs/messages": "^1.1.0", + "chalk": "^4.1.2", + "copy-props": "^4.0.0", + "gulplog": "^2.2.0", + "interpret": "^3.1.1", + "liftoff": "^5.0.1", + "mute-stdout": "^2.0.0", + "replace-homedir": "^2.0.0", + "semver-greatest-satisfied-range": "^2.0.0", + "string-width": "^4.2.3", + "v8flags": "^4.0.0", + "yargs": "^16.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cliui": { + "version": "7.0.4", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "supports-color": { + "version": "7.2.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "dev": true + } + } + }, + "gulp-concat": { + "version": "2.6.1", + "dev": true, + "requires": { + "concat-with-sourcemaps": "^1.0.0", + "through2": "^2.0.0", + "vinyl": "^2.0.0" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "gulp-connect": { + "version": "5.7.0", + "dev": true, + "requires": { + "ansi-colors": "^2.0.5", + "connect": "^3.6.6", + "connect-livereload": "^0.6.0", + "fancy-log": "^1.3.2", + "map-stream": "^0.0.7", + "send": "0.19.0", + "serve-index": "^1.9.1", + "serve-static": "^1.13.2", + "tiny-lr": "^1.1.1" + }, + "dependencies": { + "ansi-colors": { + "version": "2.0.5", + "dev": true + }, + "fancy-log": { + "version": "1.3.3", + "dev": true, + "requires": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" + } + } + } + }, + "gulp-filter": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-9.0.1.tgz", + "integrity": "sha512-knVYL8h9bfYIeft3VokVTkuaWJkQJMrFCS3yVjZQC6BGg+1dZFoeUY++B9D2X4eFpeNTx9StWK0qnDby3NO3PA==", + "dev": true, + "requires": { + "multimatch": "^7.0.0", + "plugin-error": "^2.0.1", + "slash": "^5.1.0", + "streamfilter": "^3.0.0", + "to-absolute-glob": "^3.0.0" + } + }, + "gulp-if": { + "version": "3.0.0", + "dev": true, + "requires": { + "gulp-match": "^1.1.0", + "ternary-stream": "^3.0.0", + "through2": "^3.0.1" + }, + "dependencies": { + "through2": { + "version": "3.0.2", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + } + } + }, + "gulp-js-escape": { + "version": "1.0.1", + "dev": true, + "requires": { + "through2": "^0.6.3" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "dev": true + }, + "through2": { + "version": "0.6.5", + "dev": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + } + } + }, + "gulp-match": { + "version": "1.1.0", + "dev": true, + "requires": { + "minimatch": "^3.0.3" + } + }, + "gulp-rename": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-2.1.0.tgz", + "integrity": "sha512-dGuzuH8jQGqCMqC544IEPhs5+O2l+IkdoSZsgd4kY97M1CxQeI3qrmweQBIrxLBbjbe/8uEWK8HHcNBc3OCy4g==", + "dev": true + }, + "gulp-replace": { + "version": "1.1.4", + "dev": true, + "requires": { + "@types/node": "*", + "@types/vinyl": "^2.0.4", + "istextorbinary": "^3.0.0", + "replacestream": "^4.0.3", + "yargs-parser": ">=5.0.0-security.0" + } + }, + "gulp-sourcemaps": { + "version": "3.0.0", + "dev": true, + "requires": { + "@gulp-sourcemaps/identity-map": "^2.0.1", + "@gulp-sourcemaps/map-sources": "^1.0.0", + "acorn": "^6.4.1", + "convert-source-map": "^1.0.0", + "css": "^3.0.0", + "debug-fabulous": "^1.0.0", + "detect-newline": "^2.0.0", + "graceful-fs": "^4.0.0", + "source-map": "^0.6.0", + "strip-bom-string": "^1.0.0", + "through2": "^2.0.0" + }, + "dependencies": { + "acorn": { + "version": "6.4.2", + "dev": true + }, + "convert-source-map": { + "version": "1.9.0", + "dev": true + }, + "through2": { + "version": "2.0.5", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "gulp-tap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gulp-tap/-/gulp-tap-2.0.0.tgz", + "integrity": "sha512-U5/v1bTozx672QHzrvzPe6fPl2io7Wqyrx2y30AG53eMU/idH4BrY/b2yikOkdyhjDqGgPoMUMnpBg9e9LK8Nw==", + "dev": true, + "requires": { + "through2": "^3.0.1" + }, + "dependencies": { + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + } + } + }, + "gulp-wrap": { + "version": "0.15.0", + "requires": { + "consolidate": "^0.15.1", + "es6-promise": "^4.2.6", + "fs-readfile-promise": "^3.0.1", + "js-yaml": "^3.13.0", + "lodash": "^4.17.11", + "node.extend": "2.0.2", + "plugin-error": "^1.0.1", + "through2": "^3.0.1", + "tryit": "^1.0.1", + "vinyl-bufferstream": "^1.0.1" + }, + "dependencies": { + "ansi-colors": { + "version": "1.1.0", + "requires": { + "ansi-wrap": "^0.1.0" + } + }, + "arr-diff": { + "version": "4.0.0" + }, + "arr-union": { + "version": "3.1.0" + }, + "extend-shallow": { + "version": "3.0.2", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "plugin-error": { + "version": "1.0.1", + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + }, + "through2": { + "version": "3.0.2", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + } + } + }, + "gulplog": { + "version": "2.2.0", + "dev": true, + "requires": { + "glogg": "^2.2.0" + } + }, + "gzip-size": { + "version": "6.0.0", + "dev": true, + "requires": { + "duplexer": "^0.1.2" + } + }, + "handlebars": { + "version": "4.7.8", + "dev": true, + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, + "has": { + "version": "1.0.4" + }, + "has-bigints": { + "version": "1.0.2", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.2", + "dev": true, + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.2.0", + "dev": true, + "requires": { + "dunder-proto": "^1.0.0" + } + }, + "has-symbols": { + "version": "1.1.0" + }, + "has-tostringtag": { + "version": "1.0.2", + "dev": true, + "requires": { + "has-symbols": "^1.0.3" + } + }, + "hasown": { + "version": "2.0.2", + "requires": { + "function-bind": "^1.1.2" + } + }, + "he": { + "version": "1.2.0", + "dev": true + }, + "headers-utils": { + "version": "1.2.5", + "dev": true + }, + "homedir-polyfill": { + "version": "1.0.3", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hosted-git-info": { + "version": "7.0.2", + "dev": true, + "requires": { + "lru-cache": "^10.0.1" + }, + "dependencies": { + "lru-cache": { + "version": "10.2.2", + "dev": true + } + } + }, + "html-escaper": { + "version": "2.0.2", + "dev": true + }, + "htmlfy": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.8.1.tgz", + "integrity": "sha512-xWROBw9+MEGwxpotll0h672KCaLrKKiCYzsyN8ZgL9cQbVumFnyvsk2JqiB9ELAV1GLj1GG/jxZUjV9OZZi/yQ==", + "dev": true + }, + "htmlparser2": { + "version": "9.1.0", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "http-errors": { + "version": "2.0.0", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "http-parser-js": { + "version": "0.5.10", + "dev": true + }, + "http-proxy": { + "version": "1.18.1", + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-agent": { + "version": "7.0.2", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "dependencies": { + "agent-base": { + "version": "7.1.1", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + } + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true + }, + "iab-adcom": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/iab-adcom/-/iab-adcom-1.0.6.tgz", + "integrity": "sha512-XAJdidfrFgZNKmHqcXD3Zhqik2rdSmOs+PGgeVfPWgthxvzNBQxkZnKkW3QAau6mrLjtJc8yOQC6awcEv7gryA==" + }, + "iab-native": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/iab-native/-/iab-native-1.0.0.tgz", + "integrity": "sha512-AxGYpKGRcyG5pbEAqj+ssxNwZAfxC0pRwyKc0MYoKjm0UeOoUNCWrZV0HGimcQii6ebe6MRqBQEeENyHM4qTdQ==" + }, + "iab-openrtb": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/iab-openrtb/-/iab-openrtb-1.0.1.tgz", + "integrity": "sha512-egawJx6+pMh/6uA/hak1y+R2+XCSH2jxteSkWlY98/XdQQftaMUMllUFNMKrHwq9lgCI70Me06g4JCCnV6E62g==", + "requires": { + "iab-adcom": "1.0.6" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "dev": true + }, + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true + }, + "immediate": { + "version": "3.0.6", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "dev": true + } + } + }, + "import-meta-resolve": { + "version": "4.1.0", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "dev": true + }, + "individual": { + "version": "2.0.0", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4" + }, + "inquirer": { + "version": "12.9.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.9.0.tgz", + "integrity": "sha512-LlFVmvWVCun7uEgPB3vups9NzBrjJn48kRNtFGw3xU1H5UXExTEz/oF1JGLaB0fvlkUB+W6JfgLcSEaSdH7RPA==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.15", + "@inquirer/prompts": "^7.8.0", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "mute-stream": "^2.0.0", + "run-async": "^4.0.5", + "rxjs": "^7.8.2" + } + }, + "internal-slot": { + "version": "1.1.0", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + } + }, + "interpret": { + "version": "3.1.1", + "dev": true + }, + "ip-address": { + "version": "9.0.5", + "dev": true, + "requires": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.3", + "dev": true + } + } + }, + "ipaddr.js": { + "version": "1.9.1" + }, + "is": { + "version": "3.3.0" + }, + "is-absolute": { + "version": "1.0.0", + "dev": true, + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-arguments": { + "version": "1.1.1", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-array-buffer": { + "version": "3.0.5", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + } + }, + "is-arrayish": { + "version": "0.2.1", + "dev": true + }, + "is-async-function": { + "version": "2.1.1", + "dev": true, + "requires": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + } + }, + "is-bigint": { + "version": "1.1.0", + "dev": true, + "requires": { + "has-bigints": "^1.0.2" + } + }, + "is-binary-path": { + "version": "2.1.0", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.2.2", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + } + }, + "is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "requires": { + "semver": "^7.7.1" + }, + "dependencies": { + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true + } + } + }, + "is-callable": { + "version": "1.2.7", + "dev": true + }, + "is-core-module": { + "version": "2.15.1", + "requires": { + "hasown": "^2.0.2" + } + }, + "is-data-view": { + "version": "1.0.2", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + } + }, + "is-date-object": { + "version": "1.1.0", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + } + }, + "is-docker": { + "version": "2.2.1", + "dev": true + }, + "is-extendable": { + "version": "1.0.1", + "requires": { + "is-plain-object": "^2.0.4" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "requires": { + "isobject": "^3.0.1" + } + } + } + }, + "is-extglob": { + "version": "2.1.1" + }, + "is-finalizationregistry": { + "version": "1.1.1", + "dev": true, + "requires": { + "call-bound": "^1.0.3" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0" + }, + "is-function": { + "version": "1.0.2", + "dev": true + }, + "is-generator-function": { + "version": "1.0.10", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.3", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-map": { + "version": "2.0.3", + "dev": true + }, + "is-nan": { + "version": "1.3.2", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "is-negated-glob": { + "version": "1.0.0", + "dev": true + }, + "is-number": { + "version": "7.0.0" + }, + "is-number-object": { + "version": "1.1.1", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + } + }, + "is-plain-obj": { + "version": "4.1.0", + "dev": true + }, + "is-plain-object": { + "version": "5.0.0", + "dev": true + }, + "is-promise": { + "version": "2.2.2", + "dev": true + }, + "is-regex": { + "version": "1.2.1", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, + "is-relative": { + "version": "1.0.0", + "dev": true, + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "is-running": { + "version": "2.1.0", + "dev": true + }, + "is-set": { + "version": "2.0.3", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.4", + "dev": true, + "requires": { + "call-bound": "^1.0.3" + } + }, + "is-stream": { + "version": "1.1.0", + "dev": true + }, + "is-string": { + "version": "1.1.1", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + } + }, + "is-symbol": { + "version": "1.1.1", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + } + }, + "is-typed-array": { + "version": "1.1.15", + "dev": true, + "requires": { + "which-typed-array": "^1.1.16" + } + }, + "is-unc-path": { + "version": "1.0.0", + "dev": true, + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true + }, + "is-valid-glob": { + "version": "1.0.0", + "dev": true + }, + "is-weakmap": { + "version": "2.0.2", + "dev": true + }, + "is-weakref": { + "version": "1.1.1", + "dev": true, + "requires": { + "call-bound": "^1.0.3" + } + }, + "is-weakset": { + "version": "2.0.3", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + } + }, + "is-windows": { + "version": "1.0.2", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "2.0.5", + "dev": true + }, + "isbinaryfile": { + "version": "4.0.10" + }, + "isexe": { + "version": "3.1.1", + "dev": true + }, + "isobject": { + "version": "3.0.1" + }, + "istanbul": { + "version": "0.4.5", + "dev": true, + "requires": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "dev": true + }, + "resolve": { + "version": "1.1.7", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + }, + "which": { + "version": "1.3.1", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "5.2.1", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "dev": true + }, + "make-dir": { + "version": "4.0.0", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "semver": { + "version": "7.6.2", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.1.7", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "istextorbinary": { + "version": "3.3.0", + "dev": true, + "requires": { + "binaryextensions": "^2.2.0", + "textextensions": "^3.2.0" + } + }, + "iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + } + }, + "jackspeak": { + "version": "3.4.3", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "requires": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "dependencies": { + "async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + } + } + }, + "jest-diff": { + "version": "29.7.0", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-get-type": { + "version": "29.6.3", + "dev": true + }, + "jest-matcher-utils": { + "version": "29.7.0", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-message-util": { + "version": "29.7.0", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "dev": true + }, + "slash": { + "version": "3.0.0", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-mock": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "dev": true, + "peer": true, + "requires": { + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-util": "30.0.5" + }, + "dependencies": { + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "peer": true, + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dev": true, + "peer": true, + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, + "@sinclair/typebox": { + "version": "0.34.38", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", + "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", + "dev": true, + "peer": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "peer": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "peer": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "peer": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "peer": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "peer": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "peer": true + }, + "jest-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "dev": true, + "peer": true, + "requires": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + } + }, + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "peer": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "peer": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "peer": true + }, + "jest-util": { + "version": "29.7.0", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0" + }, + "js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1" + } + } + }, + "jsbn": { + "version": "1.1.0", + "dev": true + }, + "jsdoc-type-pratt-parser": { + "version": "4.1.0", + "dev": true + }, + "jsesc": { + "version": "3.1.0" + }, + "json-buffer": { + "version": "3.0.1", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "3.0.2", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true + }, + "json5": { + "version": "2.2.3" + }, + "jsonfile": { + "version": "6.1.0", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "requires": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + } + }, + "jszip": { + "version": "3.10.1", + "dev": true, + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "just-extend": { + "version": "6.2.0", + "dev": true + }, + "karma": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", + "requires": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "requires": { + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "7.0.4", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4" + }, + "glob": { + "version": "7.2.3", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9" + } + } + }, + "karma-babel-preprocessor": { + "version": "8.0.2", + "dev": true, + "requires": {} + }, + "karma-browserstack-launcher": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/karma-browserstack-launcher/-/karma-browserstack-launcher-1.6.0.tgz", + "integrity": "sha512-Y/UWPdHZkHIVH2To4GWHCTzmrsB6H7PBWy6pw+TWz5sr4HW2mcE+Uj6qWgoVNxvQU1Pfn5LQQzI6EQ65p8QbiQ==", + "dev": true, + "requires": { + "browserstack": "~1.5.1", + "browserstack-local": "^1.3.7", + "q": "~1.5.0" + } + }, + "karma-chai": { + "version": "0.1.0", + "dev": true, + "requires": {} + }, + "karma-chrome-launcher": { + "version": "3.2.0", + "dev": true, + "requires": { + "which": "^1.2.1" + }, + "dependencies": { + "isexe": { + "version": "2.0.0", + "dev": true + }, + "which": { + "version": "1.3.1", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "karma-coverage": { + "version": "2.2.1", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + } + }, + "karma-coverage-istanbul-reporter": { + "version": "3.0.3", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^3.0.2", + "minimatch": "^3.0.4" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "istanbul-lib-coverage": { + "version": "2.0.5", + "dev": true + } + } + }, + "make-dir": { + "version": "2.1.0", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.7.2", + "dev": true + } + } + }, + "karma-firefox-launcher": { + "version": "2.1.3", + "dev": true, + "requires": { + "is-wsl": "^2.2.0", + "which": "^3.0.0" + }, + "dependencies": { + "isexe": { + "version": "2.0.0", + "dev": true + }, + "which": { + "version": "3.0.1", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "karma-mocha": { + "version": "2.0.1", + "dev": true, + "requires": { + "minimist": "^1.2.3" + } + }, + "karma-mocha-reporter": { + "version": "2.2.5", + "dev": true, + "requires": { + "chalk": "^2.1.0", + "log-symbols": "^2.1.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.1", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "karma-opera-launcher": { + "version": "1.0.0", + "dev": true, + "requires": {} + }, + "karma-safari-launcher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/karma-safari-launcher/-/karma-safari-launcher-1.0.0.tgz", + "integrity": "sha512-qmypLWd6F2qrDJfAETvXDfxHvKDk+nyIjpH9xIeI3/hENr0U3nuqkxaftq73PfXZ4aOuOChA6SnLW4m4AxfRjQ==", + "dev": true, + "requires": {} + }, + "karma-safarinative-launcher": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/karma-safarinative-launcher/-/karma-safarinative-launcher-1.1.0.tgz", + "integrity": "sha512-vdMjdQDHkSUbOZc8Zq2K5bBC0yJGFEgfrKRJTqt0Um0SC1Rt8drS2wcN6UA3h4LgsL3f1pMcmRSvKucbJE8Qdg==", + "requires": {} + }, + "karma-script-launcher": { + "version": "1.0.0", + "dev": true, + "requires": {} + }, + "karma-sinon": { + "version": "1.0.5", + "dev": true, + "requires": {} + }, + "karma-sourcemap-loader": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz", + "integrity": "sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.10" + } + }, + "karma-spec-reporter": { + "version": "0.0.32", + "dev": true, + "requires": { + "colors": "^1.1.2" + } + }, + "karma-webpack": { + "version": "5.0.1", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^9.0.3", + "webpack-merge": "^4.1.5" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "minimatch": { + "version": "9.0.4", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + } + } + } + } + }, + "keycode": { + "version": "2.2.1", + "dev": true + }, + "keyv": { + "version": "4.5.4", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "kind-of": { + "version": "6.0.3", + "dev": true + }, + "klona": { + "version": "2.0.6" + }, + "last-run": { + "version": "2.0.0", + "dev": true + }, + "lazystream": { + "version": "1.0.1", + "dev": true, + "requires": { + "readable-stream": "^2.0.5" + } + }, + "lead": { + "version": "4.0.0", + "dev": true + }, + "levn": { + "version": "0.4.1", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lie": { + "version": "3.3.0", + "dev": true, + "requires": { + "immediate": "~3.0.5" + } + }, + "liftoff": { + "version": "5.0.1", + "dev": true, + "requires": { + "extend": "^3.0.2", + "findup-sync": "^5.0.0", + "fined": "^2.0.0", + "flagged-respawn": "^2.0.0", + "is-plain-object": "^5.0.0", + "rechoir": "^0.8.0", + "resolve": "^1.20.0" + } + }, + "lines-and-columns": { + "version": "2.0.4", + "dev": true + }, + "live-connect-common": { + "version": "4.1.0" + }, + "live-connect-js": { + "version": "7.2.0", + "requires": { + "live-connect-common": "^v4.1.0", + "tiny-hashes": "1.0.1" + } + }, + "livereload-js": { + "version": "2.4.0", + "dev": true + }, + "loader-runner": { + "version": "4.3.0", + "dev": true + }, + "loader-utils": { + "version": "2.0.4", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "locate-app": { + "version": "2.4.15", + "dev": true, + "requires": { + "@promptbook/utils": "0.50.0-10", + "type-fest": "2.13.0", + "userhome": "1.0.0" + }, + "dependencies": { + "type-fest": { + "version": "2.13.0", + "dev": true + } + } + }, + "locate-path": { + "version": "5.0.0", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21" + }, + "lodash.clone": { + "version": "4.5.0", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8" + }, + "lodash.flattendeep": { + "version": "4.4.0", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "dev": true + }, + "lodash.pickby": { + "version": "4.6.0", + "dev": true + }, + "lodash.some": { + "version": "4.6.0", + "dev": true + }, + "lodash.union": { + "version": "4.6.0", + "dev": true + }, + "lodash.zip": { + "version": "4.2.0", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, + "log4js": { + "version": "6.9.1", + "requires": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + } + }, + "logform": { + "version": "2.6.0", + "dev": true, + "requires": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "@colors/colors": { + "version": "1.6.0", + "dev": true + } + } + }, + "loglevel": { + "version": "1.9.1", + "dev": true + }, + "loglevel-plugin-prefix": { + "version": "0.8.4", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "loupe": { + "version": "2.3.7", + "dev": true, + "requires": { + "get-func-name": "^2.0.1" + } + }, + "lru-cache": { + "version": "5.1.1", + "requires": { + "yallist": "^3.0.2" + } + }, + "lru-queue": { + "version": "0.1.0", + "dev": true, + "requires": { + "es5-ext": "~0.10.2" + } + }, + "m3u8-parser": { + "version": "4.8.0", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0" + } + }, + "magic-string": { + "version": "0.30.17", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "make-dir": { + "version": "3.1.0", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "dev": true + }, + "map-stream": { + "version": "0.0.7", + "dev": true + }, + "math-intrinsics": { + "version": "1.1.0" + }, + "media-typer": { + "version": "0.3.0" + }, + "memoizee": { + "version": "0.4.17", + "dev": true, + "requires": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + } + }, + "memory-fs": { + "version": "0.5.0", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "merge-descriptors": { + "version": "1.0.3" + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "dev": true + }, + "methods": { + "version": "1.1.2" + }, + "micromatch": { + "version": "4.0.8", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "2.6.0" + }, + "mime-db": { + "version": "1.52.0" + }, + "mime-types": { + "version": "2.1.35", + "requires": { + "mime-db": "1.52.0" + } + }, + "min-document": { + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", + "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", + "dev": true, + "requires": { + "dom-walk": "^0.1.0" + } + }, + "minimatch": { + "version": "3.1.2", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8" + }, + "minipass": { + "version": "7.1.2", + "dev": true + }, + "mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.6", + "requires": { + "minimist": "^1.2.6" + } + }, + "mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "dev": true + }, + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chalk": { + "version": "4.1.2", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "cliui": { + "version": "7.0.4", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "glob": { + "version": "8.1.0", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "has-flag": { + "version": "4.0.0", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "dev": true + }, + "js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "locate-path": { + "version": "6.0.0", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "log-symbols": { + "version": "4.1.0", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "minimatch": { + "version": "5.1.6", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "ms": { + "version": "2.1.3", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "strip-ansi": { + "version": "6.0.1", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "supports-color": { + "version": "8.1.1", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "dev": true + } + } + }, + "moment": { + "version": "2.30.1", + "dev": true + }, + "morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "dev": true, + "requires": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + } + } + }, + "mpd-parser": { + "version": "0.22.1", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + } + }, + "mrmime": { + "version": "2.0.0", + "dev": true + }, + "ms": { + "version": "2.1.2" + }, + "multimatch": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-7.0.0.tgz", + "integrity": "sha512-SYU3HBAdF4psHEL/+jXDKHO95/m5P2RvboHT2Y0WtTttvJLP4H/2WS9WlQPFvF6C8d6SpLw8vjCnQOnVIVOSJQ==", + "dev": true, + "requires": { + "array-differ": "^4.0.0", + "array-union": "^3.0.1", + "minimatch": "^9.0.3" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "mute-stdout": { + "version": "2.0.0", + "dev": true + }, + "mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true + }, + "mux.js": { + "version": "6.0.1", + "dev": true, + "requires": { + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" + } + }, + "napi-postinstall": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", + "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "dev": true + }, + "negotiator": { + "version": "0.6.3" + }, + "neo-async": { + "version": "2.6.2", + "dev": true + }, + "neostandard": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/neostandard/-/neostandard-0.12.2.tgz", + "integrity": "sha512-VZU8EZpSaNadp3rKEwBhVD1Kw8jE3AftQLkCyOaM7bWemL1LwsYRsBnAmXy2LjG9zO8t66qJdqB7ccwwORyrAg==", + "dev": true, + "requires": { + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", + "@stylistic/eslint-plugin": "2.11.0", + "eslint-import-resolver-typescript": "^3.10.1", + "eslint-plugin-import-x": "^4.16.1", + "eslint-plugin-n": "^17.20.0", + "eslint-plugin-promise": "^7.2.1", + "eslint-plugin-react": "^7.37.5", + "find-up": "^5.0.0", + "globals": "^15.15.0", + "peowly": "^1.3.2", + "typescript-eslint": "^8.35.1" + }, + "dependencies": { + "find-up": { + "version": "5.0.0", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "globals": { + "version": "15.15.0", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "yocto-queue": { + "version": "0.1.0", + "dev": true + } + } + }, + "netmask": { + "version": "2.0.2", + "dev": true + }, + "next-tick": { + "version": "1.1.0", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "dev": true + }, + "nise": { + "version": "6.1.1", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + }, + "dependencies": { + "path-to-regexp": { + "version": "8.2.0", + "dev": true + } + } + }, + "node-domexception": { + "version": "1.0.0", + "dev": true + }, + "node-fetch": { + "version": "3.3.2", + "dev": true, + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, + "node-html-parser": { + "version": "6.1.13", + "dev": true, + "requires": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node-releases": { + "version": "2.0.25", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.25.tgz", + "integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==" + }, + "node-request-interceptor": { + "version": "0.6.3", + "dev": true, + "requires": { + "@open-draft/until": "^1.0.3", + "debug": "^4.3.0", + "headers-utils": "^1.2.0", + "strict-event-emitter": "^0.1.0" + } + }, + "node.extend": { + "version": "2.0.2", + "requires": { + "has": "^1.0.3", + "is": "^3.2.1" + } + }, + "nopt": { + "version": "3.0.6", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "6.0.1", + "dev": true, + "requires": { + "hosted-git-info": "^7.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "dependencies": { + "semver": { + "version": "7.6.2", + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0" + }, + "now-and-later": { + "version": "3.0.0", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "dev": true, + "requires": { + "path-key": "^2.0.0" + }, + "dependencies": { + "path-key": { + "version": "2.0.1", + "dev": true + } + } + }, + "nth-check": { + "version": "2.1.1", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, + "object-assign": { + "version": "4.1.1" + }, + "object-inspect": { + "version": "1.13.4" + }, + "object-is": { + "version": "1.1.6", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + } + }, + "object-keys": { + "version": "1.1.1", + "dev": true + }, + "object.assign": { + "version": "4.1.7", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + } + }, + "object.defaults": { + "version": "1.1.0", + "dev": true, + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + } + }, + "object.fromentries": { + "version": "2.0.8", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + } + }, + "object.groupby": { + "version": "1.0.3", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + } + }, + "object.pick": { + "version": "1.3.0", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "object.values": { + "version": "1.2.1", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "on-finished": { + "version": "2.4.1", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true + }, + "once": { + "version": "1.4.0", + "requires": { + "wrappy": "1" + } + }, + "opener": { + "version": "1.5.2", + "dev": true + }, + "opn": { + "version": "5.5.0", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + }, + "dependencies": { + "is-wsl": { + "version": "1.1.0", + "dev": true + } + } + }, + "optionator": { + "version": "0.9.4", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + } + }, + "own-keys": { + "version": "1.0.1", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + } + }, + "p-finally": { + "version": "1.0.0", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "dev": true + }, + "pac-proxy-agent": { + "version": "7.2.0", + "dev": true, + "requires": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "dependencies": { + "agent-base": { + "version": "7.1.3", + "dev": true + }, + "https-proxy-agent": { + "version": "7.0.6", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + } + } + }, + "pac-resolver": { + "version": "7.0.1", + "dev": true, + "requires": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + } + }, + "package-json-from-dist": { + "version": "1.0.0", + "dev": true + }, + "pako": { + "version": "1.0.11", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-filepath": { + "version": "1.0.2", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + } + }, + "parse-imports": { + "version": "2.2.1", + "dev": true, + "requires": { + "es-module-lexer": "^1.5.3", + "slashes": "^3.0.12" + } + }, + "parse-json": { + "version": "7.1.1", + "dev": true, + "requires": { + "@babel/code-frame": "^7.21.4", + "error-ex": "^1.3.2", + "json-parse-even-better-errors": "^3.0.0", + "lines-and-columns": "^2.0.3", + "type-fest": "^3.8.0" + }, + "dependencies": { + "type-fest": { + "version": "3.13.1", + "dev": true + } + } + }, + "parse-ms": { + "version": "2.1.0", + "dev": true + }, + "parse-node-version": { + "version": "1.0.1", + "dev": true + }, + "parse-passwd": { + "version": "1.0.0", + "dev": true + }, + "parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "dev": true, + "requires": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "dependencies": { + "parse5": { + "version": "7.1.2", + "dev": true, + "requires": { + "entities": "^4.4.0" + } + } + } + }, + "parse5-parser-stream": { + "version": "7.1.2", + "dev": true, + "requires": { + "parse5": "^7.0.0" + }, + "dependencies": { + "parse5": { + "version": "7.1.2", + "dev": true, + "requires": { + "entities": "^4.4.0" + } + } + } + }, + "parseurl": { + "version": "1.3.3" + }, + "path-exists": { + "version": "4.0.0", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1" + }, + "path-key": { + "version": "3.1.1", + "dev": true + }, + "path-parse": { + "version": "1.0.7" + }, + "path-root": { + "version": "0.1.1", + "dev": true, + "requires": { + "path-root-regex": "^0.1.0" + } + }, + "path-root-regex": { + "version": "0.1.2", + "dev": true + }, + "path-scurry": { + "version": "1.11.1", + "dev": true, + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "dev": true + } + } + }, + "path-to-regexp": { + "version": "0.1.12" + }, + "pathe": { + "version": "1.1.2", + "dev": true + }, + "pathval": { + "version": "1.1.1", + "dev": true + }, + "pause-stream": { + "version": "0.0.11", + "dev": true, + "requires": { + "through": "~2.3" + } + }, + "pend": { + "version": "1.2.0", + "dev": true + }, + "peowly": { + "version": "1.3.2", + "dev": true + }, + "picocolors": { + "version": "1.1.1" + }, + "picomatch": { + "version": "2.3.1" + }, + "pirates": { + "version": "4.0.6", + "dev": true + }, + "pkcs7": { + "version": "1.0.4", + "dev": true, + "requires": { + "@babel/runtime": "^7.5.5" + } + }, + "pkg-dir": { + "version": "4.2.0", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "plugin-error": { + "version": "2.0.1", + "dev": true, + "requires": { + "ansi-colors": "^1.0.1" + }, + "dependencies": { + "ansi-colors": { + "version": "1.1.0", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + } + } + }, + "possible-typed-array-names": { + "version": "1.0.0", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "dev": true + } + } + }, + "prelude-ls": { + "version": "1.2.1", + "dev": true + }, + "prettier": { + "version": "2.8.1", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "dev": true + } + } + }, + "pretty-ms": { + "version": "7.0.1", + "dev": true, + "requires": { + "parse-ms": "^2.1.0" + } + }, + "process": { + "version": "0.11.10", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1" + }, + "progress": { + "version": "2.0.3", + "dev": true + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + } + } + }, + "proxy-addr": { + "version": "2.0.7", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "proxy-agent": { + "version": "6.5.0", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "dependencies": { + "agent-base": { + "version": "7.1.3", + "dev": true + }, + "https-proxy-agent": { + "version": "7.0.6", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + }, + "lru-cache": { + "version": "7.18.3", + "dev": true + } + } + }, + "proxy-from-env": { + "version": "1.1.0", + "dev": true + }, + "prr": { + "version": "1.0.1", + "dev": true + }, + "ps-tree": { + "version": "1.2.0", + "dev": true, + "requires": { + "event-stream": "=3.3.4" + } + }, + "pump": { + "version": "3.0.0", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.3.1", + "dev": true + }, + "puppeteer": { + "version": "24.11.2", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.11.2.tgz", + "integrity": "sha512-HopdRZWHa5zk0HSwd8hU+GlahQ3fmesTAqMIDHVY9HasCvppcYuHYXyjml0nlm+nbwVCqAQWV+dSmiNCrZGTGQ==", + "dev": true, + "requires": { + "@puppeteer/browsers": "2.10.5", + "chromium-bidi": "5.1.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1464554", + "puppeteer-core": "24.11.2", + "typed-query-selector": "^2.12.0" + } + }, + "puppeteer-core": { + "version": "24.11.2", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.11.2.tgz", + "integrity": "sha512-c49WifNb8hix+gQH17TldmD6TC/Md2HBaTJLHexIUq4sZvo2pyHY/Pp25qFQjibksBu/SJRYUY7JsoaepNbiRA==", + "dev": true, + "requires": { + "@puppeteer/browsers": "2.10.5", + "chromium-bidi": "5.1.0", + "debug": "^4.4.1", + "devtools-protocol": "0.0.1464554", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.3" + }, + "dependencies": { + "debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "requires": {} + } + } + }, + "q": { + "version": "1.5.1", + "dev": true + }, + "qjobs": { + "version": "1.2.0" + }, + "qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "requires": { + "side-channel": "^1.1.0" + } + }, + "query-selector-shadow-dom": { + "version": "1.0.1", + "dev": true + }, + "querystringify": { + "version": "2.2.0", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1" + }, + "raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "requires": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "dependencies": { + "http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "requires": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + } + }, + "statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" + } + } + }, + "react-is": { + "version": "18.3.1", + "dev": true + }, + "read-pkg": { + "version": "8.1.0", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.1", + "normalize-package-data": "^6.0.0", + "parse-json": "^7.0.0", + "type-fest": "^4.2.0" + } + }, + "read-pkg-up": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-10.1.0.tgz", + "integrity": "sha512-aNtBq4jR8NawpKJQldrQcSW9y/d+KWH4v24HWkHljOZ7H0av+YTGANBzRh9A5pw7v/bLVsLVPpOhJ7gHNVy8lA==", + "dev": true, + "requires": { + "find-up": "^6.3.0", + "read-pkg": "^8.1.0", + "type-fest": "^4.2.0" + }, + "dependencies": { + "find-up": { + "version": "6.3.0", + "dev": true, + "requires": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + } + }, + "locate-path": { + "version": "7.2.0", + "dev": true, + "requires": { + "p-locate": "^6.0.0" + } + }, + "p-limit": { + "version": "4.0.0", + "dev": true, + "requires": { + "yocto-queue": "^1.0.0" + } + }, + "p-locate": { + "version": "6.0.0", + "dev": true, + "requires": { + "p-limit": "^4.0.0" + } + }, + "path-exists": { + "version": "5.0.0", + "dev": true + } + } + }, + "readable-stream": { + "version": "2.3.8", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0" + }, + "safe-buffer": { + "version": "5.1.2" + } + } + }, + "readdir-glob": { + "version": "1.1.3", + "dev": true, + "requires": { + "minimatch": "^5.1.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "readdirp": { + "version": "3.6.0", + "requires": { + "picomatch": "^2.2.1" + } + }, + "rechoir": { + "version": "0.8.0", + "dev": true, + "requires": { + "resolve": "^1.20.0" + } + }, + "recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "dev": true, + "requires": { + "minimatch": "^3.0.5" + } + }, + "reflect.getprototypeof": { + "version": "1.0.10", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + } + }, + "regenerate": { + "version": "1.4.2" + }, + "regenerate-unicode-properties": { + "version": "10.2.0", + "requires": { + "regenerate": "^1.4.2" + } + }, + "regexp.prototype.flags": { + "version": "1.5.4", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + } + }, + "regexpu-core": { + "version": "6.2.0", + "requires": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + } + }, + "regjsgen": { + "version": "0.8.0" + }, + "regjsparser": { + "version": "0.12.0", + "requires": { + "jsesc": "~3.0.2" + }, + "dependencies": { + "jsesc": { + "version": "3.0.2" + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "dev": true + }, + "replace-ext": { + "version": "2.0.0", + "dev": true + }, + "replace-homedir": { + "version": "2.0.0", + "dev": true + }, + "replacestream": { + "version": "4.0.3", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.3", + "object-assign": "^4.0.1", + "readable-stream": "^2.0.2" + } + }, + "require-directory": { + "version": "2.1.1" + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "requires-port": { + "version": "1.0.0" + }, + "resolve": { + "version": "1.22.8", + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-dir": { + "version": "1.0.1", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "dev": true + }, + "resolve-options": { + "version": "2.0.0", + "dev": true, + "requires": { + "value-or-function": "^4.0.0" + } + }, + "resolve-pkg-maps": { + "version": "1.0.0", + "dev": true + }, + "resq": { + "version": "1.11.0", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1" + }, + "dependencies": { + "fast-deep-equal": { + "version": "2.0.1", + "dev": true + } + } + }, + "ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "dev": true + }, + "reusify": { + "version": "1.1.0", + "dev": true + }, + "rfdc": { + "version": "1.4.1" + }, + "rgb2hex": { + "version": "0.2.5", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "run-async": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.5.tgz", + "integrity": "sha512-oN9GTgxUNDBumHTTDmQ8dep6VIJbgj9S3dPP+9XylVLIK4xB9XTXtKWROd5pnhdXR9k0EgO1JRcNh0T+Ny2FsA==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rust-result": { + "version": "1.0.0", + "dev": true, + "requires": { + "individual": "^2.0.0" + } + }, + "rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "safaridriver": { + "version": "1.0.0", + "dev": true + }, + "safe-array-concat": { + "version": "1.1.3", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + } + }, + "safe-buffer": { + "version": "5.2.1" + }, + "safe-json-parse": { + "version": "1.0.1", + "dev": true + }, + "safe-push-apply": { + "version": "1.0.0", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + } + }, + "safe-regex-test": { + "version": "1.1.0", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + } + }, + "safe-regex2": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz", + "integrity": "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==", + "dev": true, + "requires": { + "ret": "~0.5.0" + } + }, + "safe-stable-stringify": { + "version": "2.4.3", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2" + }, + "schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, + "semver": { + "version": "6.3.1" + }, + "semver-greatest-satisfied-range": { + "version": "2.0.0", + "dev": true, + "requires": { + "sver": "^1.8.3" + } + }, + "send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serialize-error": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-12.0.0.tgz", + "integrity": "sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==", + "dev": true, + "requires": { + "type-fest": "^4.31.0" + } + }, + "serialize-javascript": { + "version": "6.0.2", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-index": { + "version": "1.9.1", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "dev": true + }, + "ms": { + "version": "2.0.0", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "dev": true + } + } + }, + "serve-static": { + "version": "1.16.2", + "requires": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0" + } + } + }, + "set-function-length": { + "version": "1.2.2", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, + "set-function-name": { + "version": "2.0.2", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + } + }, + "set-proto": { + "version": "1.0.0", + "dev": true, + "requires": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + } + }, + "setimmediate": { + "version": "1.0.5", + "dev": true + }, + "setprototypeof": { + "version": "1.2.0" + }, + "shallow-clone": { + "version": "3.0.1", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "shebang-command": { + "version": "2.0.0", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "dev": true + }, + "side-channel": { + "version": "1.1.0", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + } + }, + "signal-exit": { + "version": "3.0.7", + "dev": true + }, + "sinon": { + "version": "20.0.0", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.5", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "supports-color": "^7.2.0" + }, + "dependencies": { + "diff": { + "version": "7.0.0", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "sirv": { + "version": "2.0.4", + "dev": true, + "requires": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + } + }, + "slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true + }, + "slashes": { + "version": "3.0.12", + "dev": true + }, + "smart-buffer": { + "version": "4.2.0", + "dev": true + }, + "socket.io": { + "version": "4.8.0", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + } + }, + "socket.io-adapter": { + "version": "2.5.5", + "requires": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "socket.io-parser": { + "version": "4.2.4", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + } + }, + "socks": { + "version": "2.8.4", + "dev": true, + "requires": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "8.0.5", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "dependencies": { + "agent-base": { + "version": "7.1.3", + "dev": true + } + } + }, + "source-list-map": { + "version": "2.0.1", + "dev": true + }, + "source-map": { + "version": "0.6.1" + }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true + }, + "source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "requires": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "source-map-resolve": { + "version": "0.6.0", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "spacetrim": { + "version": "0.11.25", + "dev": true + }, + "sparkles": { + "version": "2.1.0", + "dev": true + }, + "spdx-correct": { + "version": "3.2.0", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.5.0", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.18", + "dev": true + }, + "split": { + "version": "0.3.3", + "dev": true, + "requires": { + "through": "2" + } + }, + "split2": { + "version": "4.2.0", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3" + }, + "stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true + }, + "stable-hash-x": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", + "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", + "dev": true + }, + "stack-utils": { + "version": "2.0.6", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "dev": true + } + } + }, + "statuses": { + "version": "2.0.1" + }, + "stop-iteration-iterator": { + "version": "1.0.0", + "dev": true, + "requires": { + "internal-slot": "^1.0.4" + } + }, + "stream-buffers": { + "version": "3.0.2", + "dev": true + }, + "stream-combiner": { + "version": "0.0.4", + "dev": true, + "requires": { + "duplexer": "~0.1.1" + } + }, + "stream-composer": { + "version": "1.0.2", + "dev": true, + "requires": { + "streamx": "^2.13.2" + } + }, + "stream-exhaust": { + "version": "1.0.2", + "dev": true + }, + "stream-shift": { + "version": "1.0.3", + "dev": true + }, + "streamfilter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/streamfilter/-/streamfilter-3.0.0.tgz", + "integrity": "sha512-kvKNfXCmUyC8lAXSSHCIXBUlo/lhsLcCU/OmzACZYpRUdtKIH68xYhm/+HI15jFJYtNJGYtCgn2wmIiExY1VwA==", + "dev": true, + "requires": { + "readable-stream": "^3.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "streamroller": { + "version": "3.1.5", + "requires": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "dependencies": { + "fs-extra": { + "version": "8.1.0", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "universalify": { + "version": "0.1.2" + } + } + }, + "streamx": { + "version": "2.22.0", + "dev": true, + "requires": { + "bare-events": "^2.2.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "strict-event-emitter": { + "version": "0.1.0", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2" + } + } + }, + "string-template": { + "version": "0.2.1", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.1", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.1", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + } + }, + "string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trim": { + "version": "1.2.10", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + } + }, + "string.prototype.trimend": { + "version": "1.0.9", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "string.prototype.trimstart": { + "version": "1.0.8", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "strip-ansi": { + "version": "7.1.0", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "dev": true + } + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "dev": true + }, + "strip-bom-string": { + "version": "1.0.0", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "dev": true + }, + "strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0" + }, + "sver": { + "version": "1.8.4", + "dev": true, + "requires": { + "semver": "^6.3.0" + } + }, + "synckit": { + "version": "0.9.2", + "dev": true, + "requires": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + } + }, + "tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true + }, + "tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "dev": true, + "requires": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "tar-stream": { + "version": "3.1.7", + "dev": true, + "requires": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "teex": { + "version": "1.0.1", + "dev": true, + "requires": { + "streamx": "^2.12.5" + } + }, + "temp-fs": { + "version": "0.9.9", + "dev": true, + "requires": { + "rimraf": "~2.5.2" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.5.4", + "dev": true, + "requires": { + "glob": "^7.0.5" + } + } + } + }, + "ternary-stream": { + "version": "3.0.0", + "dev": true, + "requires": { + "duplexify": "^4.1.1", + "fork-stream": "^0.0.4", + "merge-stream": "^2.0.0", + "through2": "^3.0.1" + }, + "dependencies": { + "through2": { + "version": "3.0.2", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + } + } + }, + "terser": { + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + } + }, + "terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + } + }, + "test-exclude": { + "version": "6.0.0", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "dependencies": { + "glob": { + "version": "7.2.3", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "text-decoder": { + "version": "1.1.0", + "dev": true, + "requires": { + "b4a": "^1.6.4" + } + }, + "textextensions": { + "version": "3.3.0", + "dev": true + }, + "through": { + "version": "2.3.8", + "dev": true + }, + "through2": { + "version": "4.0.2", + "dev": true, + "requires": { + "readable-stream": "3" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "time-stamp": { + "version": "1.1.0", + "dev": true + }, + "timers-ext": { + "version": "0.1.8", + "dev": true, + "requires": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + } + }, + "tiny-hashes": { + "version": "1.0.1" + }, + "tiny-lr": { + "version": "1.1.1", + "dev": true, + "requires": { + "body": "^5.1.0", + "debug": "^3.1.0", + "faye-websocket": "~0.10.0", + "livereload-js": "^2.3.0", + "object-assign": "^4.1.0", + "qs": "^6.4.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "requires": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "dependencies": { + "fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "requires": {} + }, + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true + } + } + }, + "tinyrainbow": { + "version": "1.2.0", + "dev": true + }, + "tmp": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz", + "integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==" + }, + "to-absolute-glob": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-3.0.0.tgz", + "integrity": "sha512-loO/XEWTRqpfcpI7+Jr2RR2Umaaozx1t6OSVWtMi0oy5F/Fxg3IC+D/TToDnxyAGs7uZBGT/6XmyDUxgsObJXA==", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "requires": { + "is-number": "^7.0.0" + } + }, + "to-through": { + "version": "3.0.0", + "dev": true, + "requires": { + "streamx": "^2.12.5" + } + }, + "toidentifier": { + "version": "1.0.1" + }, + "totalist": { + "version": "3.0.1", + "dev": true + }, + "triple-beam": { + "version": "1.4.1", + "dev": true + }, + "tryit": { + "version": "1.0.3" + }, + "ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "requires": {} + }, + "ts-declaration-location": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/ts-declaration-location/-/ts-declaration-location-1.0.7.tgz", + "integrity": "sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==", + "dev": true, + "requires": { + "picomatch": "^4.0.2" + }, + "dependencies": { + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true + } + } + }, + "tsconfig-paths": { + "version": "3.15.0", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + } + } + }, + "tslib": { + "version": "2.8.1", + "dev": true + }, + "tsx": { + "version": "4.19.3", + "dev": true, + "requires": { + "esbuild": "~0.25.0", + "fsevents": "~2.3.3", + "get-tsconfig": "^4.7.5" + } + }, + "type": { + "version": "2.7.3", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "dev": true + }, + "type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typed-array-buffer": { + "version": "1.0.3", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + } + }, + "typed-array-byte-length": { + "version": "1.0.3", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + } + }, + "typed-array-byte-offset": { + "version": "1.0.4", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + } + }, + "typed-array-length": { + "version": "1.0.7", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + } + }, + "typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "dev": true + }, + "typescript": { + "version": "5.8.2", + "dev": true + }, + "typescript-compare": { + "version": "0.0.2", + "requires": { + "typescript-logic": "^0.0.0" + } + }, + "typescript-eslint": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.0.tgz", + "integrity": "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==", + "dev": true, + "requires": { + "@typescript-eslint/eslint-plugin": "8.39.0", + "@typescript-eslint/parser": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0" + } + }, + "typescript-logic": { + "version": "0.0.0" + }, + "typescript-tuple": { + "version": "2.2.1", + "requires": { + "typescript-compare": "^0.0.2" + } + }, + "ua-parser-js": { + "version": "0.7.38" + }, + "uglify-js": { + "version": "3.18.0", + "dev": true, + "optional": true + }, + "unbox-primitive": { + "version": "1.1.0", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + } + }, + "unc-path-regex": { + "version": "0.1.2", + "dev": true + }, + "undertaker": { + "version": "2.0.0", + "dev": true, + "requires": { + "bach": "^2.0.1", + "fast-levenshtein": "^3.0.0", + "last-run": "^2.0.0", + "undertaker-registry": "^2.0.0" + }, + "dependencies": { + "fast-levenshtein": { + "version": "3.0.0", + "dev": true, + "requires": { + "fastest-levenshtein": "^1.0.7" + } + } + } + }, + "undertaker-registry": { + "version": "2.0.0", + "dev": true + }, + "undici": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "dev": true + }, + "undici-types": { + "version": "5.26.5" + }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.1" + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.2.0" + }, + "unicode-property-aliases-ecmascript": { + "version": "2.1.0" + }, + "unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true + }, + "universalify": { + "version": "2.0.1", + "dev": true + }, + "unpipe": { + "version": "1.0.0" + }, + "unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "requires": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1", + "napi-postinstall": "^0.3.0" + } + }, + "update-browserslist-db": { + "version": "1.1.3", + "requires": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + } + }, + "uri-js": { + "version": "4.4.1", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "url": { + "version": "0.11.3", + "dev": true, + "requires": { + "punycode": "^1.4.1", + "qs": "^6.11.2" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "dev": true + } + } + }, + "url-parse": { + "version": "1.5.10", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "url-toolkit": { + "version": "2.2.5", + "dev": true + }, + "urlpattern-polyfill": { + "version": "10.0.0", + "dev": true + }, + "userhome": { + "version": "1.0.0", + "dev": true + }, + "util": { + "version": "0.12.5", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "util-deprecate": { + "version": "1.0.2" + }, + "utils-merge": { + "version": "1.0.1" + }, + "uuid": { + "version": "9.0.1", + "dev": true + }, + "v8flags": { + "version": "4.0.1", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "value-or-function": { + "version": "4.0.0", + "dev": true + }, + "vary": { + "version": "1.1.2" + }, + "video.js": { + "version": "7.21.7", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.21.7.tgz", + "integrity": "sha512-T2s3WFAht7Zjr2OSJamND9x9Dn2O+Z5WuHGdh8jI5SYh5mkMdVTQ7vSRmA5PYpjXJ2ycch6jpMjkJEIEU2xxqw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/http-streaming": "2.16.3", + "@videojs/vhs-utils": "^3.0.4", + "@videojs/xhr": "2.6.0", + "aes-decrypter": "3.1.3", + "global": "^4.4.0", + "keycode": "^2.2.0", + "m3u8-parser": "4.8.0", + "mpd-parser": "0.22.1", + "mux.js": "6.0.1", + "safe-json-parse": "4.0.0", + "videojs-font": "3.2.0", + "videojs-vtt.js": "^0.15.5" + }, + "dependencies": { + "safe-json-parse": { + "version": "4.0.0", + "dev": true, + "requires": { + "rust-result": "^1.0.0" + } + } + } + }, + "videojs-contrib-ads": { + "version": "6.9.0", + "dev": true, + "requires": { + "global": "^4.3.2", + "video.js": "^6 || ^7" + } + }, + "videojs-font": { + "version": "3.2.0", + "dev": true + }, + "videojs-ima": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/videojs-ima/-/videojs-ima-2.4.0.tgz", + "integrity": "sha512-pP93nNmsjz+BgRIQ3KnQjiB3hgxsHGLweU+23Aq656C9N632t//4gbrnbDBa3XLosBNXrK4uKxuBTFi/6drKRQ==", + "dev": true, + "requires": { + "@hapi/cryptiles": "^5.1.0", + "can-autoplay": "^3.0.2", + "extend": ">=3.0.2", + "videojs-contrib-ads": "^6.9.0 || ^7" + } + }, + "videojs-playlist": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/videojs-playlist/-/videojs-playlist-5.2.0.tgz", + "integrity": "sha512-Kyx6C5r7zmj6y97RrIlyji8JUEt0kUEfVyB4P6VMyEFVyCGlOlzlgPw2verznBp4uDfjVPPuAJKvNJ7x9O5NJw==", + "dev": true, + "requires": { + "global": "^4.3.2", + "video.js": "^6 || ^7 || ^8" + } + }, + "videojs-vtt.js": { + "version": "0.15.5", + "dev": true, + "requires": { + "global": "^4.3.1" + } + }, + "vinyl": { + "version": "2.2.1", + "dev": true, + "requires": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "dependencies": { + "replace-ext": { + "version": "1.0.1", + "dev": true + } + } + }, + "vinyl-bufferstream": { + "version": "1.0.1", + "requires": { + "bufferstreams": "1.0.1" + } + }, + "vinyl-contents": { + "version": "2.0.0", + "dev": true, + "requires": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" + }, + "dependencies": { + "vinyl": { + "version": "3.0.1", + "dev": true, + "requires": { + "clone": "^2.1.2", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + } + } + } + }, + "vinyl-fs": { + "version": "4.0.2", + "dev": true, + "requires": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.3", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.1", + "vinyl-sourcemap": "^2.0.0" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "vinyl": { + "version": "3.0.1", + "dev": true, + "requires": { + "clone": "^2.1.2", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + } + } + } + }, + "vinyl-sourcemap": { + "version": "2.0.0", + "dev": true, + "requires": { + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + }, + "dependencies": { + "vinyl": { + "version": "3.0.1", + "dev": true, + "requires": { + "clone": "^2.1.2", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + } + } + } + }, + "vinyl-sourcemaps-apply": { + "version": "0.2.1", + "requires": { + "source-map": "^0.5.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7" + } + } + }, + "void-elements": { + "version": "2.0.1" + }, + "wait-port": { + "version": "1.1.0", + "dev": true, + "requires": { + "chalk": "^4.1.2", + "commander": "^9.3.0", + "debug": "^4.3.4" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "dev": true + }, + "commander": { + "version": "9.5.0", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "wcwidth": { + "version": "1.0.1", + "dev": true, + "optional": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "web-streams-polyfill": { + "version": "4.0.0-beta.3", + "dev": true + }, + "webdriver": { + "version": "9.19.2", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.19.2.tgz", + "integrity": "sha512-kw6dSwNzimU8/CkGVlM36pqWHZ7BhCwV4/d8fu6rpIYGeQbPwcNc4M90TfJuzYMA7Au3NdrwT/EVQgVLQ9Ju8Q==", + "dev": true, + "requires": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.19.2", + "@wdio/logger": "9.18.0", + "@wdio/protocols": "9.16.2", + "@wdio/types": "9.19.2", + "@wdio/utils": "9.19.2", + "deepmerge-ts": "^7.0.3", + "https-proxy-agent": "^7.0.6", + "undici": "^6.21.3", + "ws": "^8.8.0" + }, + "dependencies": { + "@wdio/config": { + "version": "9.19.2", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.19.2.tgz", + "integrity": "sha512-OVCzPQxav0QDk5rktQ6LYARZ5ueUuJXIqTXUpS3A9Jt6PF+ZUI5sbO/y+z+qHQXqDq+LkscmFsmkzgnoHzHcfg==", + "dev": true, + "requires": { + "@wdio/logger": "9.18.0", + "@wdio/types": "9.19.2", + "@wdio/utils": "9.19.2", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0" + } + }, + "@wdio/logger": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.18.0.tgz", + "integrity": "sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "safe-regex2": "^5.0.0", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/protocols": { + "version": "9.16.2", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.16.2.tgz", + "integrity": "sha512-h3k97/lzmyw5MowqceAuY3HX/wGJojXHkiPXA3WlhGPCaa2h4+GovV2nJtRvknCKsE7UHA1xB5SWeI8MzloBew==", + "dev": true + }, + "@wdio/types": { + "version": "9.19.2", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.19.2.tgz", + "integrity": "sha512-fBI7ljL+YcPXSXUhdk2+zVuz7IYP1aDMTq1eVmMme9GY0y67t0dCNPOt6xkCAEdL5dOcV6D2L1r6Cf/M2ifTvQ==", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/utils": { + "version": "9.19.2", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.19.2.tgz", + "integrity": "sha512-caimJiTsxDUfXn/gRAzcYTO3RydSl7XzD+QpjfWZYJjzr8a2XfNnj+Vdmr8gG4BSkiVHirW9mFCZeQp2eTD7rA==", + "dev": true, + "requires": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.18.0", + "@wdio/types": "9.19.2", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^6.1.2", + "geckodriver": "^5.0.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "mitt": "^3.0.1", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + } + }, + "agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true + }, + "chalk": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", + "dev": true + }, + "https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + } + } + }, + "webdriverio": { + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.19.1.tgz", + "integrity": "sha512-hpGgK6d9QNi3AaLFWIPQaEMqJhXF048XAIsV5i5mkL0kjghV1opcuhKgbbG+7pcn8JSpiq6mh7o3MDYtapw90w==", + "dev": true, + "requires": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.19.1", + "@wdio/logger": "9.18.0", + "@wdio/protocols": "9.16.2", + "@wdio/repl": "9.16.2", + "@wdio/types": "9.19.1", + "@wdio/utils": "9.19.1", + "archiver": "^7.0.1", + "aria-query": "^5.3.0", + "cheerio": "^1.0.0-rc.12", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.8.1", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^12.0.0", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.19.1" + }, + "dependencies": { + "@wdio/config": { + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.19.1.tgz", + "integrity": "sha512-BeTB2paSjaij3cf1NXQzX9CZmdj5jz2/xdUhkJlCeGmGn1KjWu5BjMO+exuiy+zln7dOJjev8f0jlg8e8f1EbQ==", + "dev": true, + "requires": { + "@wdio/logger": "9.18.0", + "@wdio/types": "9.19.1", + "@wdio/utils": "9.19.1", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0" + } + }, + "@wdio/logger": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.18.0.tgz", + "integrity": "sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==", + "dev": true, + "requires": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "safe-regex2": "^5.0.0", + "strip-ansi": "^7.1.0" + } + }, + "@wdio/protocols": { + "version": "9.16.2", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.16.2.tgz", + "integrity": "sha512-h3k97/lzmyw5MowqceAuY3HX/wGJojXHkiPXA3WlhGPCaa2h4+GovV2nJtRvknCKsE7UHA1xB5SWeI8MzloBew==", + "dev": true + }, + "@wdio/repl": { + "version": "9.16.2", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.16.2.tgz", + "integrity": "sha512-FLTF0VL6+o5BSTCO7yLSXocm3kUnu31zYwzdsz4n9s5YWt83sCtzGZlZpt7TaTzb3jVUfxuHNQDTb8UMkCu0lQ==", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/types": { + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.19.1.tgz", + "integrity": "sha512-Q1HVcXiWMHp3ze2NN1BvpsfEh/j6GtAeMHhHW4p2IWUfRZlZqTfiJ+95LmkwXOG2gw9yndT8NkJigAz8v7WVYQ==", + "dev": true, + "requires": { + "@types/node": "^20.1.0" + } + }, + "@wdio/utils": { + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.19.1.tgz", + "integrity": "sha512-wWx5uPCgdZQxFIemAFVk/aa3JLwqrTsvEJsPlV3lCRpLeQ67V8aUPvvNAzE+RhX67qvelwwsvX8RrPdLDfnnYw==", + "dev": true, + "requires": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.18.0", + "@wdio/types": "9.19.1", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^6.1.2", + "geckodriver": "^5.0.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "mitt": "^3.0.1", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + } + }, + "agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true + }, + "chalk": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", + "dev": true + }, + "https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + }, + "webdriver": { + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.19.1.tgz", + "integrity": "sha512-cvccIZ3QaUZxxrA81a3rqqgxKt6VzVrZupMc+eX9J40qfGrV3NtdLb/m4AA1PmeTPGN5O3/4KrzDpnVZM4WUnA==", + "dev": true, + "requires": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.19.1", + "@wdio/logger": "9.18.0", + "@wdio/protocols": "9.16.2", + "@wdio/types": "9.19.1", + "@wdio/utils": "9.19.1", + "deepmerge-ts": "^7.0.3", + "https-proxy-agent": "^7.0.6", + "undici": "^6.21.3", + "ws": "^8.8.0" + } + } + } + }, + "webpack": { + "version": "5.102.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", + "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "dependencies": { + "json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true + } + } + }, + "webpack-bundle-analyzer": { + "version": "4.10.2", + "dev": true, + "requires": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "dev": true + }, + "ws": { + "version": "7.5.10", + "dev": true, + "requires": {} + } + } + }, + "webpack-manifest-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-5.0.1.tgz", + "integrity": "sha512-xTlX7dC3hrASixA2inuWFMz6qHsNi6MT3Uiqw621sJjRTShtpMjbDYhPPZBwWUKdIYKIjSq9em6+uzWayf38aQ==", + "dev": true, + "requires": { + "tapable": "^2.0.0", + "webpack-sources": "^2.2.0" + }, + "dependencies": { + "webpack-sources": { + "version": "2.3.1", + "dev": true, + "requires": { + "source-list-map": "^2.0.1", + "source-map": "^0.6.1" + } + } + } + }, + "webpack-merge": { + "version": "4.2.2", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true + }, + "webpack-stream": { + "version": "7.0.0", + "dev": true, + "requires": { + "fancy-log": "^1.3.3", + "lodash.clone": "^4.3.2", + "lodash.some": "^4.2.2", + "memory-fs": "^0.5.0", + "plugin-error": "^1.0.1", + "supports-color": "^8.1.1", + "through": "^2.3.8", + "vinyl": "^2.2.1" + }, + "dependencies": { + "ansi-colors": { + "version": "1.1.0", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + }, + "arr-diff": { + "version": "4.0.0", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "fancy-log": { + "version": "1.3.3", + "dev": true, + "requires": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "dev": true + }, + "plugin-error": { + "version": "1.0.1", + "dev": true, + "requires": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + } + }, + "supports-color": { + "version": "8.1.1", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "websocket-driver": { + "version": "0.7.4", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "dev": true + }, + "whatwg-encoding": { + "version": "3.1.1", + "dev": true, + "requires": { + "iconv-lite": "0.6.3" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "whatwg-mimetype": { + "version": "4.0.0", + "dev": true + }, + "which": { + "version": "5.0.0", + "dev": true, + "requires": { + "isexe": "^3.1.1" + } + }, + "which-boxed-primitive": { + "version": "1.1.1", + "dev": true, + "requires": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + } + }, + "which-builtin-type": { + "version": "1.2.1", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + } + }, + "which-collection": { + "version": "1.0.2", + "dev": true, + "requires": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + } + }, + "which-typed-array": { + "version": "1.1.19", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + } + }, + "winston-transport": { + "version": "4.7.0", + "dev": true, + "requires": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "word-wrap": { + "version": "1.2.5", + "dev": true + }, + "wordwrap": { + "version": "1.0.0", + "dev": true + }, + "workerpool": { + "version": "6.5.1", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2" + }, + "ws": { + "version": "8.17.1", + "requires": {} + }, + "xtend": { + "version": "4.0.2" + }, + "y18n": { + "version": "5.0.8" + }, + "yallist": { + "version": "3.1.1" + }, + "yargs": { + "version": "1.3.3", + "dev": true + }, + "yargs-parser": { + "version": "21.1.1", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "dev": true + }, + "decamelize": { + "version": "4.0.0", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "dev": true + } + } + }, + "yauzl": { + "version": "3.1.3", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + } + }, + "yocto-queue": { + "version": "1.0.0", + "dev": true + }, + "yoctocolors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", + "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "dev": true + }, + "yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true + }, + "zip-stream": { + "version": "6.0.1", + "dev": true, + "requires": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.5.2", + "dev": true, + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "dev": true + } } } diff --git a/package.json b/package.json index f3b32c0b305..ebc7e305559 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "10.16.0", + "version": "10.21.0", "description": "Header Bidding Management Library", "main": "dist/src/prebid.public.ts", "exports": { @@ -56,6 +56,7 @@ "@babel/eslint-parser": "^7.16.5", "@babel/plugin-transform-runtime": "^7.27.4", "@babel/register": "^7.28.3", + "@chiragrupani/karma-chromium-edge-launcher": "^2.4.1", "@eslint/compat": "^1.3.1", "@types/google-publisher-tag": "^1.20250210.0", "@wdio/browserstack-service": "^9.19.1", @@ -156,12 +157,19 @@ "iab-adcom": "^1.0.6", "iab-native": "^1.0.0", "iab-openrtb": "^1.0.1", + "karma-safarinative-launcher": "^1.1.0", "klona": "^2.0.6", "live-connect-js": "^7.2.0" }, "optionalDependencies": { "fsevents": "^2.3.2" }, + "overrides": { + "gulp-connect": { + "send": "0.19.0" + }, + "send": "0.19.0" + }, "peerDependencies": { "schema-utils": "^4.3.2" } diff --git a/src/adapterManager.ts b/src/adapterManager.ts index 17e845a765b..15d63d97dbc 100644 --- a/src/adapterManager.ts +++ b/src/adapterManager.ts @@ -67,6 +67,7 @@ import type { AnalyticsConfig, AnalyticsProvider, AnalyticsProviderConfig, } from "../libraries/analyticsAdapter/AnalyticsAdapter.ts"; +import {getGlobal} from "./prebidGlobal.ts"; export {gdprDataHandler, gppDataHandler, uspDataHandler, coppaDataHandler} from './consentHandler.js'; @@ -168,6 +169,7 @@ export interface BaseBidderRequest { */ bidderRequestId: Identifier; auctionId: Identifier; + pageViewId: Identifier; /** * The bidder associated with this request, or null in the case of stored impressions. */ @@ -193,6 +195,7 @@ export interface BaseBidderRequest { gdprConsent?: ReturnType; uspConsent?: ReturnType; gppConsent?: ReturnType; + alwaysHasCapacity?: boolean; } export interface S2SBidderRequest extends BaseBidderRequest { @@ -503,18 +506,27 @@ const adapterManager = { .filter(uniques) .forEach(incrementAuctionsCounter); + let {[PARTITIONS.CLIENT]: clientBidders, [PARTITIONS.SERVER]: serverBidders} = partitionBidders(adUnits, _s2sConfigs); + const allowedBidders = new Set(); + adUnits.forEach(au => { if (!isPlainObject(au.mediaTypes)) { au.mediaTypes = {}; } // filter out bidders that cannot participate in the auction - au.bids = au.bids.filter((bid) => !bid.bidder || dep.isAllowed(ACTIVITY_FETCH_BIDS, activityParams(MODULE_TYPE_BIDDER, bid.bidder))) + au.bids = au.bids.filter((bid) => !bid.bidder || dep.isAllowed(ACTIVITY_FETCH_BIDS, activityParams(MODULE_TYPE_BIDDER, bid.bidder, { + isS2S: serverBidders.includes(bid.bidder) && !clientBidders.includes(bid.bidder) + }))) + au.bids.forEach(bid => { + allowedBidders.add(bid.bidder); + }); incrementRequestsCounter(au.code); }); - adUnits = setupAdUnitMediaTypes(adUnits, labels); + clientBidders = clientBidders.filter(bidder => allowedBidders.has(bidder)); + serverBidders = serverBidders.filter(bidder => allowedBidders.has(bidder)); - let {[PARTITIONS.CLIENT]: clientBidders, [PARTITIONS.SERVER]: serverBidders} = partitionBidders(adUnits, _s2sConfigs); + adUnits = setupAdUnitMediaTypes(adUnits, labels); if (config.getConfig('bidderSequence') === RANDOM) { clientBidders = shuffle(clientBidders); @@ -554,6 +566,15 @@ const adapterManager = { return bidderRequest as T; } + const pbjsInstance = getGlobal(); + + function getPageViewIdForBidder(bidderCode: string | null): string { + if (!pbjsInstance.pageViewIdPerBidder.has(bidderCode)) { + pbjsInstance.pageViewIdPerBidder.set(bidderCode, generateUUID()); + } + return pbjsInstance.pageViewIdPerBidder.get(bidderCode); + } + _s2sConfigs.forEach(s2sConfig => { const s2sParams = s2sActivityParams(s2sConfig); if (s2sConfig && s2sConfig.enabled && dep.isAllowed(ACTIVITY_FETCH_BIDS, s2sParams)) { @@ -564,11 +585,13 @@ const adapterManager = { (serverBidders.length === 0 && hasModuleBids ? [null] : serverBidders).forEach(bidderCode => { const bidderRequestId = generateUUID(); + const pageViewId = getPageViewIdForBidder(bidderCode); const metrics = auctionMetrics.fork(); const bidderRequest = addOrtb2({ bidderCode, auctionId, bidderRequestId, + pageViewId, uniquePbsTid, bids: getBids({ bidderCode, @@ -584,6 +607,7 @@ const adapterManager = { src: S2S.SRC, refererInfo, metrics, + alwaysHasCapacity: s2sConfig.alwaysHasCapacity, }, s2sParams); if (bidderRequest.bids.length !== 0) { bidRequests.push(bidderRequest); @@ -611,10 +635,13 @@ const adapterManager = { const adUnitsClientCopy = getAdUnitCopyForClientAdapters(adUnits); clientBidders.forEach(bidderCode => { const bidderRequestId = generateUUID(); + const pageViewId = getPageViewIdForBidder(bidderCode); const metrics = auctionMetrics.fork(); + const adapter = _bidderRegistry[bidderCode]; const bidderRequest = addOrtb2({ bidderCode, auctionId, + pageViewId, bidderRequestId, bids: getBids({ bidderCode, @@ -629,8 +656,8 @@ const adapterManager = { timeout: cbTimeout, refererInfo, metrics, + alwaysHasCapacity: adapter?.getSpec?.().alwaysHasCapacity, }); - const adapter = _bidderRegistry[bidderCode]; if (!adapter) { logError(`Trying to make a request for bidder that does not exist: ${bidderCode}`); } diff --git a/src/adapters/bidderFactory.ts b/src/adapters/bidderFactory.ts index bbd42934057..60f21964f88 100644 --- a/src/adapters/bidderFactory.ts +++ b/src/adapters/bidderFactory.ts @@ -157,6 +157,7 @@ export interface BidderSpec extends StorageDisclosure uspConsent: null | ConsentData[typeof CONSENT_USP], gppConsent: null | ConsentData[typeof CONSENT_GPP] ) => ({ type: SyncType, url: string })[]; + alwaysHasCapacity?: boolean; } export type BidAdapter = { diff --git a/src/auction.ts b/src/auction.ts index 4fe57c74223..7acd7b8cec9 100644 --- a/src/auction.ts +++ b/src/auction.ts @@ -366,6 +366,12 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a let requests = 1; const source = (typeof bidRequest.src !== 'undefined' && bidRequest.src === S2S.SRC) ? 's2s' : bidRequest.bidderCode; + + // if the bidder has alwaysHasCapacity flag set and forceMaxRequestsPerOrigin is false, don't check capacity + if (bidRequest.alwaysHasCapacity && !config.getConfig('forceMaxRequestsPerOrigin')) { + return false; + } + // if we have no previous info on this source just let them through if (sourceInfo[source]) { if (sourceInfo[source].SRA === false) { @@ -548,6 +554,7 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM bidderRequest.bids.forEach(bid => { if (!bidResponseMap[bid.bidId]) { + addBidTimingProperties(bid); auctionInstance.addNoBid(bid); events.emit(EVENTS.NO_BID, bid); } @@ -704,17 +711,30 @@ declare module './bidfactory' { adserverTargeting: BaseBidResponse['adserverTargeting']; } } + /** - * Augment `bidResponse` with properties that are common across all bids - including rejected bids. + * Add timing properties to a bid response */ -function addCommonResponseProperties(bidResponse: Partial, adUnitCode: string, {index = auctionManager.index} = {}) { +function addBidTimingProperties(bidResponse: Partial, {index = auctionManager.index} = {}) { const bidderRequest = index.getBidderRequest(bidResponse); - const adUnit = index.getAdUnit(bidResponse); const start = (bidderRequest && bidderRequest.start) || bidResponse.requestTimestamp; Object.assign(bidResponse, { responseTimestamp: bidResponse.responseTimestamp || timestamp(), requestTimestamp: bidResponse.requestTimestamp || start, + }); + bidResponse.timeToRespond = bidResponse.responseTimestamp - bidResponse.requestTimestamp; +} + +/** + * Augment `bidResponse` with properties that are common across all bids - including rejected bids. + */ +function addCommonResponseProperties(bidResponse: Partial, adUnitCode: string, {index = auctionManager.index} = {}) { + const adUnit = index.getAdUnit(bidResponse); + + addBidTimingProperties(bidResponse, {index}) + + Object.assign(bidResponse, { cpm: parseFloat(bidResponse.cpm) || 0, bidder: bidResponse.bidder || bidResponse.bidderCode, adUnitCode @@ -723,8 +743,6 @@ function addCommonResponseProperties(bidResponse: Partial, adUnitCode: stri if (adUnit?.ttlBuffer != null) { bidResponse.ttlBuffer = adUnit.ttlBuffer; } - - bidResponse.timeToRespond = bidResponse.responseTimestamp - bidResponse.requestTimestamp; } /** diff --git a/src/creativeRenderers.js b/src/creativeRenderers.js index 1297b2da4b6..dcbfd2b40ba 100644 --- a/src/creativeRenderers.js +++ b/src/creativeRenderers.js @@ -17,9 +17,22 @@ export const getCreativeRenderer = (function() { const src = getCreativeRendererSource(bidResponse); if (!renderers.hasOwnProperty(src)) { renderers[src] = new PbPromise((resolve) => { - const iframe = createInvisibleIframe(); - iframe.srcdoc = ``; - iframe.onload = () => resolve(iframe.contentWindow.render); + const iframe = createInvisibleIframe() + iframe.srcdoc = ` + + `; + const listenerForRendererReady = (event) => { + if (event.source !== iframe.contentWindow) return; + if (event.data?.type === `RENDERER_READY_${bidResponse.adId}`) { + window.removeEventListener('message', listenerForRendererReady); + resolve(iframe.contentWindow.render); + } + } + window.addEventListener('message', listenerForRendererReady); document.body.appendChild(iframe); }) } diff --git a/src/fpd/enrichment.ts b/src/fpd/enrichment.ts index b8102caa6ac..ac481ab389b 100644 --- a/src/fpd/enrichment.ts +++ b/src/fpd/enrichment.ts @@ -1,7 +1,17 @@ import {hook} from '../hook.js'; import {getRefererInfo, parseDomain} from '../refererDetection.js'; import {findRootDomain} from './rootDomain.js'; -import {deepSetValue, deepAccess, getDefinedParams, getWinDimensions, getDocument, getWindowSelf, getWindowTop, mergeDeep} from '../utils.js'; +import { + deepSetValue, + deepAccess, + getDefinedParams, + getWinDimensions, + getDocument, + getWindowSelf, + getWindowTop, + mergeDeep, + memoize +} from '../utils.js'; import { getDNT } from '../../libraries/dnt/index.js'; import {config} from '../config.js'; import {getHighEntropySUA, getLowEntropySUA} from './sua.js'; @@ -31,6 +41,19 @@ export interface FirstPartyDataConfig { * https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData#returning_high_entropy_values */ uaHints?: string[] + /** + * Control keyword enrichment - `site.keywords`, `dooh.keywords` and/or `app.keywords`. + */ + keywords?: { + /** + * If true (the default), look for keywords in a keyword meta tag () and add them to first party data + */ + meta?: boolean, + /** + * If true (the default), look for keywords in a JSON-LD tag ("'; + +describe('greenbidsBidAdapter', () => { + const bidderRequestDefault = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000 + }; + + const bidRequests = [ + { + 'bidder': 'greenbids', + 'params': { + 'placementId': 4242 + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ee', + 'deviceWidth': 1680 + } + ]; + + function checkMediaTypesSizes(mediaTypes, expectedSizes) { + const bidRequestWithBannerSizes = Object.assign(bidRequests[0], mediaTypes); + const requestWithBannerSizes = spec.buildRequests([bidRequestWithBannerSizes], bidderRequestDefault); + const payloadWithBannerSizes = JSON.parse(requestWithBannerSizes.data); + + return payloadWithBannerSizes.data.forEach(bid => { + if (Array.isArray(expectedSizes)) { + expect(JSON.stringify(bid.sizes)).to.equal(JSON.stringify(expectedSizes)); + } else { + expect(bid.sizes[0]).to.equal(expectedSizes); + } + }); + } + + const adapter = newBidder(spec); + let sandbox; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + 'bidder': 'greenbids', + 'params': { + 'placementId': 4242 + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ee', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not found', function () { + const bidNonGbCompatible = { + 'bidder': 'greenbids', + }; + expect(spec.isBidRequestValid(bidNonGbCompatible)).to.equal(false); + }); + + it('should return false when the placement is not a number', function () { + const bidNonGbCompatible = { + 'bidder': 'greenbids', + 'params': { + 'placementId': 'toto' + }, + }; + expect(spec.isBidRequestValid(bidNonGbCompatible)).to.equal(false); + }); + }) + describe('buildRequests', function () { + it('should send bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + + expect(request.url).to.equal(ENDPOINT_URL); + expect(request.method).to.equal('POST'); + }); + + it('should not send auctionId in bid request ', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.data[0].auctionId).to.not.exist + }); + + it('should send US Privacy to endpoint', function () { + const usPrivacy = 'OHHHFCP1' + const bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'uspConsent': usPrivacy + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.us_privacy).to.exist; + expect(payload.us_privacy).to.equal(usPrivacy); + }); + + it('should send GPP values to endpoint when available and valid', function () { + const consentString = 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'; + const applicableSectionIds = [7, 8]; + const bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gppConsent': { + 'gppString': consentString, + 'applicableSections': applicableSectionIds + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gpp).to.exist; + expect(payload.gpp.consentString).to.equal(consentString); + expect(payload.gpp.applicableSectionIds).to.have.members(applicableSectionIds); + }); + + it('should send default GPP values to endpoint when available but invalid', function () { + const bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gppConsent': { + 'gppString': undefined, + 'applicableSections': ['a'] + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gpp).to.exist; + expect(payload.gpp.consentString).to.equal(''); + expect(payload.gpp.applicableSectionIds).to.have.members([]); + }); + + it('should not set the GPP object in the request sent to the endpoint when not present', function () { + const bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000 + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gpp).to.not.exist; + }); + + it('should send GDPR to endpoint', function () { + const consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + const bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': true + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(12); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should add referer info to payload', function () { + const bidRequest = Object.assign({}, bidRequests[0]) + const bidderRequest = { + refererInfo: { + page: 'https://example.com/page.html', + reachedTop: true, + numIframes: 2 + } + } + const request = spec.buildRequests([bidRequest], bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.referrer).to.exist; + expect(payload.referrer).to.deep.equal('https://example.com/page.html') + }); + + const originalConnection = window.navigator.connection; + const mockConnection = { downlink: 10 }; + + const setNavigatorConnection = (connection) => { + Object.defineProperty(window.navigator, 'connection', { + value: connection, + configurable: true, + }); + }; + + try { + setNavigatorConnection(mockConnection); + + const requestWithConnection = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithConnection = JSON.parse(requestWithConnection.data); + + expect(payloadWithConnection.networkBandwidth).to.exist; + expect(payloadWithConnection.networkBandwidth).to.deep.equal(mockConnection.downlink.toString()); + + setNavigatorConnection(undefined); + + const requestWithoutConnection = spec.buildRequests(bidRequests, bidderRequestDefault); + const payloadWithoutConnection = JSON.parse(requestWithoutConnection.data); + + expect(payloadWithoutConnection.networkBandwidth).to.exist; + expect(payloadWithoutConnection.networkBandwidth).to.deep.equal(''); + } finally { + setNavigatorConnection(originalConnection); + } + + it('should add pageReferrer info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageReferrer).to.exist; + expect(payload.pageReferrer).to.deep.equal(document.referrer); + }); + + it('should add width info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const deviceWidth = screen.width + + expect(payload.deviceWidth).to.exist; + expect(payload.deviceWidth).to.deep.equal(deviceWidth); + }); + + it('should add height info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const deviceHeight = screen.height + + expect(payload.deviceHeight).to.exist; + expect(payload.deviceHeight).to.deep.equal(deviceHeight); + }); + + it('should add pixelRatio info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const pixelRatio = getDevicePixelRatio() + + expect(payload.devicePixelRatio).to.exist; + expect(payload.devicePixelRatio).to.deep.equal(pixelRatio); + }); + + it('should add screenOrientation info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const orientation = getScreenOrientation(window.top); + + if (orientation) { + expect(payload.screenOrientation).to.exist; + expect(payload.screenOrientation).to.deep.equal(orientation); + } else { + expect(payload.screenOrientation).to.not.exist; + } + }); + + it('should add historyLength info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.historyLength).to.exist; + expect(payload.historyLength).to.deep.equal(window.top.history.length); + }); + + it('should add viewportHeight info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportHeight).to.exist; + expect(payload.viewportHeight).to.deep.equal(window.top.visualViewport.height); + }); + + it('should add viewportWidth info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportWidth).to.exist; + expect(payload.viewportWidth).to.deep.equal(window.top.visualViewport.width); + }); + + it('should add viewportHeight info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportHeight).to.exist; + expect(payload.viewportHeight).to.deep.equal(window.top.visualViewport.height); + }); + + it('should add ortb2 device data to payload', function () { + const ortb2DeviceBidderRequest = { + ...bidderRequestDefault, + ...{ + ortb2: { + device: { + w: 980, + h: 1720, + dnt: 0, + ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/125.0.6422.80 Mobile/15E148 Safari/604.1', + language: 'en', + devicetype: 1, + make: 'Apple', + model: 'iPhone 12 Pro Max', + os: 'iOS', + osv: '17.4', + ext: { fiftyonedegrees_deviceId: '17595-133085-133468-18092' }, + }, + }, + }, + }; + const request = spec.buildRequests(bidRequests, ortb2DeviceBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.device).to.deep.equal(ortb2DeviceBidderRequest.ortb2.device); + }); + }); + + describe('pageTitle', function () { + it('should add pageTitle info to payload based on document title', function () { + const testText = 'This is a title'; + sandbox.stub(window.top.document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + + it('should add pageTitle info to payload based on open-graph title', function () { + const testText = 'This is a title from open-graph'; + sandbox.stub(window.top.document, 'title').value(''); + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + + it('should add pageTitle info to payload sliced on 300 first characters', function () { + const testText = Array(500).join('a'); + sandbox.stub(window.top.document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.have.length(300); + }); + + it('should add pageTitle info to payload when fallbacking from window.top', function () { + const testText = 'This is a fallback title'; + sandbox.stub(window.top.document, 'querySelector').throws(); + sandbox.stub(document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + }); + + describe('pageDescription', function () { + it('should add pageDescription info to payload based on open-graph description', function () { + const testText = 'This is a description'; + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add pageDescription info to payload based on open-graph description', function () { + const testText = 'This is a description from open-graph'; + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[property="og:description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add pageDescription info to payload sliced on 300 first characters', function () { + const testText = Array(500).join('a'); + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.have.length(300); + }); + + it('should add pageDescription info to payload when fallbacking from window.top', function () { + const testText = 'This is a fallback description'; + sandbox.stub(window.top.document, 'querySelector').throws(); + sandbox.stub(document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add timeToFirstByte info to payload for Navigation Timing V2', function () { + // Mock `performance` object with Navigation Timing V2 data + const mockPerformance = { + getEntriesByType: () => [ + { requestStart: 100, responseStart: 150 }, + ], + }; + + // Override the global performance object + const originalPerformance = window.performance; + window.performance = mockPerformance; + + // Execute the code + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + // Calculate expected TTFB for V2 + const ttfbExpected = Math.round( + mockPerformance.getEntriesByType('navigation')[0].responseStart - + mockPerformance.getEntriesByType('navigation')[0].requestStart + ).toString(); + + // Assertions + expect(payload.timeToFirstByte).to.exist; + expect(payload.timeToFirstByte).to.deep.equal(ttfbExpected); + + // Restore the original performance object + window.performance = originalPerformance; + }); + + it('should add timeToFirstByte info to payload for Navigation Timing V1', function () { + // Mock `performance` object with Navigation Timing V1 data + const mockPerformance = { + timing: { + requestStart: 100, + responseStart: 150, + }, + getEntriesByType: () => [], + }; + + // Override the global performance object + const originalPerformance = window.performance; + window.performance = mockPerformance; + + // Execute the code + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + // Calculate expected TTFB for V1 + const ttfbExpected = ( + mockPerformance.timing.responseStart - mockPerformance.timing.requestStart + ).toString(); + + // Assertions + expect(payload.timeToFirstByte).to.exist; + expect(payload.timeToFirstByte).to.deep.equal(ttfbExpected); + + // Restore the original performance object + window.performance = originalPerformance; + }); + + it('should send GDPR to endpoint with 11 status', function () { + const consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + const bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': false, + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(11); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR TCF2 to endpoint with 12 status', function () { + const consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + const bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': true + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(12); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR to endpoint with 22 status', function () { + const bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': undefined, + 'gdprApplies': undefined, + 'vendorData': undefined, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(''); + expect(payload.gdpr_iab.status).to.equal(22); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR to endpoint with 0 status', function () { + const consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + const bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': false, + 'vendorData': { + 'hasGlobalScope': false + }, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(0); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR to endpoint with 0 status when gdprApplies = false (vendorData = undefined)', function () { + const bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': undefined, + 'gdprApplies': false, + 'vendorData': undefined, + 'apiVersion': 2 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(''); + expect(payload.gdpr_iab.status).to.equal(0); + expect(payload.gdpr_iab.apiVersion).to.equal(2); + }); + + it('should send GDPR to endpoint with 12 status when apiVersion = 0', function () { + const consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A=='; + const bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'isServiceSpecific': true + }, + 'apiVersion': 0 + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_iab).to.exist; + expect(payload.gdpr_iab.consent).to.equal(consentString); + expect(payload.gdpr_iab.status).to.equal(12); + expect(payload.gdpr_iab.apiVersion).to.equal(0); + }); + + it('should add schain info to payload if available', function () { + const bidRequest = Object.assign({}, bidRequests[0], { + ortb2: { + source: { + ext: { + schain: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'example.com', + sid: '00001', + hp: 1 + }] + } + } + } + } + }); + + const request = spec.buildRequests([bidRequest], bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.schain).to.exist; + expect(payload.schain).to.deep.equal({ + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'example.com', + sid: '00001', + hp: 1 + }] + }); + }); + + it('should add userAgentClientHints info to payload if available', function () { + const sua = { + source: 2, + platform: { + brand: 'macOS', + version: ['12', '4', '0'] + }, + browsers: [ + { + brand: 'Chromium', + version: ['106', '0', '5249', '119'] + }, + { + brand: 'Google Chrome', + version: ['106', '0', '5249', '119'] + }, + { + brand: 'Not;A=Brand', + version: ['99', '0', '0', '0'] + } + ], + mobile: 0, + model: '', + bitness: '64', + architecture: 'x86' + } + + const bidRequest = Object.assign({}, bidRequests[0], { + ortb2: { + device: { + sua: sua + } + } + }); + + const requestWithUserAgentClientHints = spec.buildRequests([bidRequest], bidderRequestDefault); + const payload = JSON.parse(requestWithUserAgentClientHints.data); + + expect(payload.userAgentClientHints).to.exist; + expect(payload.userAgentClientHints).to.deep.equal(sua); + + const defaultRequest = spec.buildRequests(bidRequests, bidderRequestDefault); + expect(JSON.parse(defaultRequest.data).userAgentClientHints).to.not.exist; + }); + + it('should use good mediaTypes banner sizes', function () { + const mediaTypesBannerSize = { + 'mediaTypes': { + 'banner': { + 'sizes': [300, 250] + } + } + }; + checkMediaTypesSizes(mediaTypesBannerSize, '300x250'); + }); + }); + + describe('Global Placement Id', function () { + const bidRequests = [ + { + 'bidder': 'greenbids', + 'params': { + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ee', + 'deviceWidth': 1680 + }, + { + 'bidder': 'greenbids', + 'params': { + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1f', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ef', + 'deviceWidth': 1680 + } + ]; + + it('should add gpid if ortb2Imp.ext.gpid is present and is non empty', function () { + const updatedBidRequests = bidRequests.map(function (bidRequest, index) { + return { + ...bidRequest, + ortb2Imp: { + ext: { + gpid: '1111/home-left-' + index + } + } + }; + } + ); + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.data[0].gpid).to.equal('1111/home-left-0'); + expect(payload.data[1].gpid).to.equal('1111/home-left-1'); + }); + + it('should not add gpid if ortb2Imp.ext.gpid is present but empty', function () { + const updatedBidRequests = bidRequests.map(bidRequest => ({ + ...bidRequest, + ortb2Imp: { + ext: { + gpid: '' + } + } + })); + + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + payload.data.forEach(bid => { + expect(bid).not.to.have.property('gpid'); + }); + }); + + it('should not add gpid if ortb2Imp.ext.gpid is not present', function () { + const updatedBidRequests = bidRequests.map(bidRequest => ({ + ...bidRequest, + ortb2Imp: { + ext: { + } + } + })); + + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + payload.data.forEach(bid => { + expect(bid).not.to.have.property('gpid'); + }); + }); + + it('should add dsa info to payload if available', function () { + const bidRequestWithDsa = Object.assign({}, bidderRequestDefault, { + ortb2: { + regs: { + ext: { + dsa: { + dsarequired: '1', + pubrender: '2', + datatopub: '3', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }] + } + } + } + } + }); + + const requestWithDsa = spec.buildRequests(bidRequests, bidRequestWithDsa); + const payload = JSON.parse(requestWithDsa.data); + + expect(payload.dsa).to.exist; + expect(payload.dsa).to.deep.equal( + { + dsarequired: '1', + pubrender: '2', + datatopub: '3', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }] + } + ); + + const defaultRequest = spec.buildRequests(bidRequests, bidderRequestDefault); + expect(JSON.parse(defaultRequest.data).dsa).to.not.exist; + }); + }); + + describe('interpretResponse', function () { + it('should get correct bid responses', function () { + const bids = { + 'body': { + 'responses': [{ + 'ad': AD_SCRIPT, + 'cpm': 0.5, + 'currency': 'USD', + 'height': 250, + 'size': '200x100', + 'bidId': '3ede2a3fa0db94', + 'ttl': 360, + 'width': 300, + 'creativeId': 'er2ee', + 'placementId': 4242 + }, { + 'ad': AD_SCRIPT, + 'cpm': 0.5, + 'currency': 'USD', + 'height': 200, + 'size': '300x150', + 'bidId': '4fef3b4gb1ec15', + 'ttl': 360, + 'width': 350, + 'creativeId': 'fs3ff', + 'placementId': 4242, + 'dealId': 'ABC_123', + 'ext': { + 'dsa': { + 'behalf': 'some-behalf', + 'paid': 'some-paid', + 'transparency': [{ + 'domain': 'test.com', + 'dsaparams': [1, 2, 3] + }], + 'adrender': 1 + } + } + }] + } + }; + const expectedResponse = [ + { + 'cpm': 0.5, + 'width': 300, + 'height': 250, + 'size': '200x100', + 'currency': 'USD', + 'netRevenue': true, + 'meta': { + advertiserDomains: [] + }, + 'ttl': 360, + 'ad': AD_SCRIPT, + 'requestId': '3ede2a3fa0db94', + 'creativeId': 'er2ee', + 'placementId': 4242 + }, { + 'cpm': 0.5, + 'width': 350, + 'height': 200, + 'size': '300x150', + 'currency': 'USD', + 'netRevenue': true, + 'meta': { + advertiserDomains: [], + dsa: { + behalf: 'some-behalf', + paid: 'some-paid', + transparency: [{ + domain: 'test.com', + dsaparams: [1, 2, 3] + }], + adrender: 1 + } + }, + 'ttl': 360, + 'ad': AD_SCRIPT, + 'requestId': '4fef3b4gb1ec15', + 'creativeId': 'fs3ff', + 'placementId': 4242, + 'dealId': 'ABC_123' + } + ] + ; + + const result = spec.interpretResponse(bids); + expect(result).to.eql(expectedResponse); + }); + + it('handles nobid responses', function () { + const bids = { + 'body': { + 'responses': [] + } + }; + + const result = spec.interpretResponse(bids); + expect(result.length).to.equal(0); + }); + }); +}); diff --git a/test/spec/modules/hypelabBidAdapter_spec.js b/test/spec/modules/hypelabBidAdapter_spec.js index d6c79a957cc..ff98c33b136 100644 --- a/test/spec/modules/hypelabBidAdapter_spec.js +++ b/test/spec/modules/hypelabBidAdapter_spec.js @@ -13,6 +13,7 @@ import { } from 'modules/hypelabBidAdapter.js'; import { BANNER } from 'src/mediaTypes.js'; +import {getDevicePixelRatio} from '../../../libraries/devicePixelRatio/devicePixelRatio.js'; const mockValidBidRequest = { bidder: 'hypelab', @@ -177,7 +178,7 @@ describe('hypelabBidAdapter', function () { expect(data.dpr).to.be.a('number'); expect(data.location).to.be.a('string'); expect(data.floor).to.equal(null); - expect(data.dpr).to.equal(1); + expect(data.dpr).to.equal(getDevicePixelRatio()); expect(data.wp).to.deep.equal({ ada: false, bnb: false, diff --git a/test/spec/modules/id5AnalyticsAdapter_spec.js b/test/spec/modules/id5AnalyticsAdapter_spec.js index e213494ce78..1e1df900f81 100644 --- a/test/spec/modules/id5AnalyticsAdapter_spec.js +++ b/test/spec/modules/id5AnalyticsAdapter_spec.js @@ -1,12 +1,13 @@ import adapterManager from '../../../src/adapterManager.js'; import id5AnalyticsAdapter from '../../../modules/id5AnalyticsAdapter.js'; -import { expect } from 'chai'; +import {expect} from 'chai'; import * as events from '../../../src/events.js'; -import { EVENTS } from '../../../src/constants.js'; -import { generateUUID } from '../../../src/utils.js'; +import {EVENTS} from '../../../src/constants.js'; +import {generateUUID} from '../../../src/utils.js'; import {server} from '../../mocks/xhr.js'; import {getGlobal} from '../../../src/prebidGlobal.js'; import {enrichEidsRule} from "../../../modules/tcfControl.ts"; +import * as utils from '../../../src/utils.js'; const CONFIG_URL = 'https://api.id5-sync.com/analytics/12349/pbjs'; const INGEST_URL = 'https://test.me/ingest'; @@ -20,6 +21,7 @@ describe('ID5 analytics adapter', () => { config = { options: { partnerId: 12349, + compressionDisabled: true } }; }); @@ -129,6 +131,32 @@ describe('ID5 analytics adapter', () => { expect(body2.payload).to.eql(auction); }); + it('compresses large events with gzip when enabled', async function() { + // turn ON compression + config.options.compressionDisabled = false; + + const longCode = 'x'.repeat(2048); + auction.adUnits[0].code = longCode; + auction.adUnits[0].adUnitCodes = [longCode]; + + id5AnalyticsAdapter.enableAnalytics(config); + server.respond(); + events.emit(EVENTS.AUCTION_END, auction); + server.respond(); + + // Wait as gzip stream is async, we need to wait until it is processed. 3 requests: config, tcf2Enforcement, auctionEnd + await waitForRequests(3); + const eventReq = server.requests[2]; + if (utils.isGzipCompressionSupported()) { + expect(eventReq.requestHeaders['Content-Encoding']).to.equal('gzip'); + expect(eventReq.requestBody).to.be.instanceof(Uint8Array); + } else { // compression is not supported in some test browsers, so we expect the event to be uncompressed. + expect(eventReq.requestHeaders['Content-Encoding']).to.be.undefined; + const body = JSON.parse(eventReq.requestBody); + expect(body.event).to.equal(EVENTS.AUCTION_END); + } + }); + it('does not repeat already sent events on new events', () => { id5AnalyticsAdapter.enableAnalytics(config); server.respond(); @@ -160,7 +188,7 @@ describe('ID5 analytics adapter', () => { 'criteoId': '_h_y_19IMUhMZG1TOTRReHFNc29TekJ3TzQ3elhnRU81ayUyQjhiRkdJJTJGaTFXJTJCdDRnVmN4S0FETUhQbXdmQWg0M3g1NWtGbGolMkZXalclMkJvWjJDOXFDSk1HU3ZKaVElM0QlM0Q', 'id5id': { 'uid': 'ID5-ZHMOQ99ulpk687Fd9xVwzxMsYtkQIJnI-qm3iWdtww!ID5*FSycZQy7v7zWXiKbEpPEWoB3_UiWdPGzh554ncYDvOkAAA3rajiR0yNrFAU7oDTu', - 'ext': { 'linkType': 1 } + 'ext': {'linkType': 1} }, 'tdid': '888a6042-8f99-483b-aa26-23c44bc9166b' }, @@ -175,7 +203,7 @@ describe('ID5 analytics adapter', () => { 'uids': [{ 'id': 'ID5-ZHMOQ99ulpk687Fd9xVwzxMsYtkQIJnI-qm3iWdtww!ID5*FSycZQy7v7zWXiKbEpPEWoB3_UiWdPGzh554ncYDvOkAAA3rajiR0yNrFAU7oDTu', 'atype': 1, - 'ext': { 'linkType': 1 } + 'ext': {'linkType': 1} }] }] }]; @@ -489,7 +517,7 @@ describe('ID5 analytics adapter', () => { 'userId': { 'id5id': { 'uid': 'ID5-ZHMOQ99ulpk687Fd9xVwzxMsYtkQIJnI-qm3iWdtww!ID5*FSycZQy7v7zWXiKbEpPEWoB3_UiWdPGzh554ncYDvOkAAA3rajiR0yNrFAU7oDTu', - 'ext': { 'linkType': 1 } + 'ext': {'linkType': 1} } } }]; @@ -511,5 +539,21 @@ describe('ID5 analytics adapter', () => { expect(body.payload.bidsReceived[0].meta).to.equal(undefined); // new rule expect(body.payload.adUnits[0].bids[0].userId.id5id.uid).to.equal(auction.adUnits[0].bids[0].userId.id5id.uid); // old, overridden rule }); + + // helper to wait until server has received at least `expected` requests + async function waitForRequests(expected = 3, timeout = 2000, interval = 10) { + return new Promise((resolve, reject) => { + const start = Date.now(); + const timer = setInterval(() => { + if (server.requests.length >= expected) { + clearInterval(timer); + resolve(); + } else if (Date.now() - start > timeout) { + clearInterval(timer); + reject(new Error('Timed out waiting for requests: expected ' + expected)); + } + }, interval); + }); + } }); }); diff --git a/test/spec/modules/incrementxBidAdapter_spec.js b/test/spec/modules/incrementxBidAdapter_spec.js index 3fcf3bcc978..47008ac738e 100644 --- a/test/spec/modules/incrementxBidAdapter_spec.js +++ b/test/spec/modules/incrementxBidAdapter_spec.js @@ -1,51 +1,34 @@ import { expect } from 'chai'; import { spec } from 'modules/incrementxBidAdapter.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; -import { INSTREAM, OUTSTREAM } from 'src/video.js'; -describe('incrementx', function () { +describe('incrementxBidAdapter', function () { const bannerBidRequest = { bidder: 'incrementx', - params: { - placementId: 'IX-HB-12345' - }, - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - sizes: [ - [300, 250], - [300, 600] - ], + params: { placementId: 'IX-HB-12345' }, + mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } }, + sizes: [[300, 250], [300, 600]], adUnitCode: 'div-gpt-ad-1460505748561-0', bidId: '2faedf3e89d123', bidderRequestId: '1c78fb49cc71c6', auctionId: 'b4f81e8e36232', transactionId: '0d95b2c1-a834-4e50-a962-9b6aa0e1c8fb' }; - const videoBidRequest = { + + const outstreamVideoBidRequest = { bidder: 'incrementx', - params: { - placementId: 'IX-HB-12346' - }, - mediaTypes: { - video: { - context: 'outstream', - playerSize: ['640x480'] - } - }, + params: { placementId: 'IX-HB-12346' }, + mediaTypes: { video: { context: 'outstream', playerSize: [640, 480] } }, adUnitCode: 'div-gpt-ad-1460505748561-1', bidId: '2faedf3e89d124', bidderRequestId: '1c78fb49cc71c7', auctionId: 'b4f81e8e36233', transactionId: '0d95b2c1-a834-4e50-a962-9b6aa0e1c8fc' }; + const instreamVideoBidRequest = { bidder: 'incrementx', - params: { - placementId: 'IX-HB-12347' - }, + params: { placementId: 'IX-HB-12347' }, mediaTypes: { video: { context: 'instream', @@ -64,166 +47,622 @@ describe('incrementx', function () { transactionId: '0d95b2c1-a834-4e50-a962-9b6aa0e1c8fd' }; - describe('isBidRequestValid', function () { - it('should return true when required params are found', function () { + const bidderRequest = { + refererInfo: { page: 'https://example.com' } + }; + + // VALIDATION + + describe('isBidRequestValid', () => { + it('should return true when placementId exists', () => { expect(spec.isBidRequestValid(bannerBidRequest)).to.equal(true); - expect(spec.isBidRequestValid(videoBidRequest)).to.equal(true); + expect(spec.isBidRequestValid(outstreamVideoBidRequest)).to.equal(true); expect(spec.isBidRequestValid(instreamVideoBidRequest)).to.equal(true); }); + + it('should return false when placementId is missing', () => { + const invalidBid = { bidder: 'incrementx', params: {} }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return false when params is missing', () => { + const invalidBid = { bidder: 'incrementx' }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); }); - describe('buildRequests', function () { - const bidderRequest = { - refererInfo: { - page: 'https://someurl.com' - } - }; - it('should build banner request', function () { - const requests = spec.buildRequests([bannerBidRequest], bidderRequest); - expect(requests).to.have.lengthOf(1); - expect(requests[0].method).to.equal('POST'); - expect(requests[0].url).to.equal('https://hb.incrementxserv.com/vzhbidder/bid'); - const data = JSON.parse(decodeURI(requests[0].data.q)); - expect(data._vzPlacementId).to.equal('IX-HB-12345'); - expect(data.sizes).to.to.a('array'); - expect(data.mChannel).to.equal(1); - }); - it('should build outstream video request', function () { - const requests = spec.buildRequests([videoBidRequest], bidderRequest); - expect(requests).to.have.lengthOf(1); - expect(requests[0].method).to.equal('POST'); - expect(requests[0].url).to.equal('https://hb.incrementxserv.com/vzhbidder/bid'); - const data = JSON.parse(decodeURI(requests[0].data.q)); - expect(data._vzPlacementId).to.equal('IX-HB-12346'); - expect(data.sizes).to.be.a('array'); - expect(data.mChannel).to.equal(2); - // For video, bidderRequestData should be included - expect(requests[0].data.bidderRequestData).to.exist; - }); - it('should build instream video request', function () { - const requests = spec.buildRequests([instreamVideoBidRequest], bidderRequest); - expect(requests).to.have.lengthOf(1); - expect(requests[0].method).to.equal('POST'); - expect(requests[0].url).to.equal('https://hb.incrementxserv.com/vzhbidder/bid'); - const data = JSON.parse(decodeURI(requests[0].data.q)); - expect(data._vzPlacementId).to.equal('IX-HB-12347'); - expect(data.sizes).to.be.a('array'); - expect(data.mChannel).to.equal(2); - - // For video, bidderRequestData should be included - expect(requests[0].data.bidderRequestData).to.exist; - const decodedBidderRequestData = decodeURI(requests[0].data.bidderRequestData); - expect(decodedBidderRequestData).to.be.a('string'); - // Verify it can be parsed as JSON - expect(() => JSON.parse(decodedBidderRequestData)).to.not.throw(); + // BUILD REQUESTS TESTS (LEGACY FORMAT ONLY) + + describe('buildRequests', () => { + it('should build a valid banner request (LEGACY FORMAT: q only)', () => { + const reqs = spec.buildRequests([bannerBidRequest], bidderRequest); + const data = reqs[0].data; + + expect(reqs[0].method).to.equal('POST'); + expect(reqs[0].url).to.equal('https://hb.incrementxserv.com/vzhbidder/bid'); + + // Banner sends ONLY q + expect(data.q).to.exist; + expect(data.bidderRequestData).to.not.exist; + + const decodedQ = JSON.parse(decodeURIComponent(data.q)); + expect(decodedQ._vzPlacementId).to.equal('IX-HB-12345'); + expect(decodedQ.mChannel).to.equal(1); + expect(decodedQ._rqsrc).to.equal('https://example.com'); + expect(decodedQ._slotBidId).to.equal('2faedf3e89d123'); + expect(decodedQ.sizes).to.be.an('array'); + }); + + it('should build an outstream video request (LEGACY FORMAT: q + bidderRequestData)', () => { + const reqs = spec.buildRequests([outstreamVideoBidRequest], bidderRequest); + const data = reqs[0].data; + + // Video sends q + bidderRequestData ONLY + expect(data.q).to.exist; + expect(data.bidderRequestData).to.exist; + + const decodedQ = JSON.parse(decodeURIComponent(data.q)); + expect(decodedQ._vzPlacementId).to.equal('IX-HB-12346'); + expect(decodedQ.mChannel).to.equal(2); + + // bidderRequestData contains full bidderRequest + const decodedBidderRequest = JSON.parse(decodeURIComponent(data.bidderRequestData)); + expect(decodedBidderRequest.refererInfo).to.exist; + expect(decodedBidderRequest.refererInfo.page).to.equal('https://example.com'); + }); + + it('should build an instream video request (LEGACY FORMAT)', () => { + const reqs = spec.buildRequests([instreamVideoBidRequest], bidderRequest); + const data = reqs[0].data; + + expect(data.q).to.exist; + expect(data.bidderRequestData).to.exist; + + const decodedQ = JSON.parse(decodeURIComponent(data.q)); + expect(decodedQ.mChannel).to.equal(2); + expect(decodedQ._vzPlacementId).to.equal('IX-HB-12347'); + }); + + it('should handle multiple bid requests', () => { + const reqs = spec.buildRequests([bannerBidRequest, outstreamVideoBidRequest], bidderRequest); + expect(reqs).to.have.lengthOf(2); + expect(reqs[0].data.q).to.exist; + expect(reqs[1].data.q).to.exist; + }); + + it('should use params.size if available', () => { + const bidWithParamsSize = { + ...bannerBidRequest, + params: { placementId: 'IX-HB-12345', size: [[728, 90]] } + }; + const reqs = spec.buildRequests([bidWithParamsSize], bidderRequest); + const decodedQ = JSON.parse(decodeURIComponent(reqs[0].data.q)); + expect(decodedQ.sizes).to.be.an('array'); }); }); - describe('interpretResponse', function () { - const bannerServerResponse = { + // INTERPRET RESPONSE - BANNER + + describe('interpretResponse - banner', () => { + const bannerResponse = { body: { slotBidId: '2faedf3e89d123', - cpm: 0.5, + ad: '
BANNER
', + cpm: 1.5, + mediaType: BANNER, adWidth: 300, adHeight: 250, - ad: '
Banner Ad
', - mediaType: BANNER, - netRevenue: true, currency: 'USD', + netRevenue: true, + creativeId: 'CR123', + adType: '1', + settings: { test: 'value' }, advertiserDomains: ['example.com'] } }; - const videoServerResponse = { + + it('should parse banner response correctly', () => { + const req = { data: { q: 'dummy' } }; + const result = spec.interpretResponse(bannerResponse, req); + + expect(result).to.have.lengthOf(1); + const bid = result[0]; + expect(bid.requestId).to.equal('2faedf3e89d123'); + expect(bid.mediaType).to.equal(BANNER); + expect(bid.ad).to.equal('
BANNER
'); + expect(bid.cpm).to.equal(1.5); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.currency).to.equal('USD'); + expect(bid.netRevenue).to.equal(true); + expect(bid.creativeId).to.equal('CR123'); + expect(bid.ttl).to.equal(300); + expect(bid.meta.advertiserDomains).to.deep.equal(['example.com']); + }); + + it('should handle banner with missing ad content', () => { + const responseNoAd = { + body: { + slotBidId: '2faedf3e89d123', + cpm: 1.5, + mediaType: BANNER, + adWidth: 300, + adHeight: 250 + } + }; + const req = { data: { q: 'dummy' } }; + const result = spec.interpretResponse(responseNoAd, req); + expect(result[0].ad).to.equal(''); + }); + + it('should use default currency when not provided', () => { + const responseNoCurrency = { + body: { + slotBidId: '2faedf3e89d123', + ad: '
BANNER
', + cpm: 1.5, + mediaType: BANNER, + adWidth: 300, + adHeight: 250 + } + }; + const req = { data: { q: 'dummy' } }; + const result = spec.interpretResponse(responseNoCurrency, req); + expect(result[0].currency).to.equal('USD'); + }); + + it('should use default values for missing fields', () => { + const minimalResponse = { + body: { + slotBidId: '2faedf3e89d123', + cpm: 0, + mediaType: BANNER + } + }; + const req = { data: { q: 'dummy' } }; + const result = spec.interpretResponse(minimalResponse, req); + const bid = result[0]; + expect(bid.cpm).to.equal(0); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.adType).to.equal('1'); + expect(bid.creativeId).to.equal(0); + expect(bid.netRevenue).to.equal(false); + expect(bid.meta.advertiserDomains).to.deep.equal([]); + }); + }); + + // INTERPRET RESPONSE - VIDEO (with videoContext) + + describe('interpretResponse - video with videoContext', () => { + const outstreamResponse = { body: { slotBidId: '2faedf3e89d124', - cpm: 1.0, + ad: 'outstream', + cpm: 2, + mediaType: VIDEO, adWidth: 640, adHeight: 480, - ad: 'Test VAST', - mediaType: VIDEO, + rUrl: 'https://cdn/test.xml', netRevenue: true, currency: 'USD', - rUrl: 'https://example.com/vast.xml', advertiserDomains: ['example.com'] } }; - const instreamVideoServerResponse = { + + const instreamResponse = { body: { slotBidId: '2faedf3e89d125', - cpm: 1.5, + ad: 'instream', + cpm: 3, + mediaType: VIDEO, adWidth: 640, adHeight: 480, - ad: 'Test Instream VAST', - mediaType: VIDEO, + advertiserDomains: ['example.com'], netRevenue: true, currency: 'USD', ttl: 300, - advertiserDomains: ['example.com'] } }; - const bidderRequest = { - refererInfo: { - page: 'https://someurl.com' - }, - data: { - bidderRequestData: JSON.stringify({ - bids: [videoBidRequest] - }) + it('should parse outstream video using videoContext field', () => { + const req = { + data: { + videoContext: 'outstream', + adUnitCode: 'ad-unit-outstream' + } + }; + + const res = spec.interpretResponse(outstreamResponse, req); + expect(res).to.have.lengthOf(1); + expect(res[0].vastXml).to.equal('outstream'); + expect(res[0].renderer).to.exist; + expect(res[0].renderer.url).to.equal('https://cdn/test.xml'); + expect(res[0].renderer.id).to.equal('2faedf3e89d124'); + }); + + it('should parse instream video using videoContext field', () => { + const req = { + data: { + videoContext: 'instream', + adUnitCode: 'ad-unit-instream' + } + }; + + const res = spec.interpretResponse(instreamResponse, req); + expect(res).to.have.lengthOf(1); + expect(res[0].vastUrl).to.equal('instream'); + expect(res[0].renderer).to.not.exist; + }); + + it('should not create renderer for outstream without rUrl', () => { + const responseNoRUrl = { + body: { + slotBidId: '2faedf3e89d124', + ad: 'outstream', + cpm: 2, + mediaType: VIDEO, + adWidth: 640, + adHeight: 480 + } + }; + const req = { + data: { + videoContext: 'outstream', + adUnitCode: 'ad-unit-outstream' + } + }; + + const res = spec.interpretResponse(responseNoRUrl, req); + expect(res[0].renderer).to.not.exist; + }); + }); + + // INTERPRET RESPONSE - VIDEO (legacy bidderRequestData) + + describe('interpretResponse - video with legacy bidderRequestData', () => { + const outstreamResponse = { + body: { + slotBidId: '2faedf3e89d124', + ad: 'outstream', + cpm: 2, + mediaType: VIDEO, + adWidth: 640, + adHeight: 480, + rUrl: 'https://cdn/test.xml', + advertiserDomains: ['example.com'] } }; - const instreamBidderRequest = { - refererInfo: { - page: 'https://someurl.com' - }, - data: { - bidderRequestData: JSON.stringify({ - bids: [instreamVideoBidRequest] - }) + const instreamResponse = { + body: { + slotBidId: '2faedf3e89d125', + ad: 'instream', + cpm: 3, + mediaType: VIDEO, + adWidth: 640, + adHeight: 480, + advertiserDomains: ['example.com'] } }; - it('should handle banner response', function () { - const bidResponses = spec.interpretResponse(bannerServerResponse, bidderRequest); - expect(bidResponses).to.have.lengthOf(1); - const bid = bidResponses[0]; - expect(bid.requestId).to.equal('2faedf3e89d123'); - expect(bid.cpm).to.equal(0.5); - expect(bid.width).to.equal(300); - expect(bid.height).to.equal(250); - expect(bid.ad).to.equal('
Banner Ad
'); - expect(bid.mediaType).to.equal(BANNER); - expect(bid.meta.advertiserDomains).to.deep.equal(['example.com']); + it('should parse outstream video from bidderRequestData', () => { + const req = { + data: { + bidderRequestData: encodeURIComponent(JSON.stringify({ + bids: [{ + bidId: '2faedf3e89d124', + adUnitCode: 'ad-unit-outstream', + mediaTypes: { video: { context: 'outstream' } } + }] + })) + } + }; + + const res = spec.interpretResponse(outstreamResponse, req); + expect(res[0].vastXml).to.equal('outstream'); + expect(res[0].renderer).to.exist; }); - it('should handle outstream video response', function () { - const bidResponses = spec.interpretResponse(videoServerResponse, bidderRequest); - expect(bidResponses).to.have.lengthOf(1); - const bid = bidResponses[0]; - expect(bid.requestId).to.equal('2faedf3e89d124'); - expect(bid.cpm).to.equal(1.0); - expect(bid.width).to.equal(640); - expect(bid.height).to.equal(480); - expect(bid.vastXml).to.equal('Test VAST'); - expect(bid.renderer).to.exist; - expect(bid.mediaType).to.equal(VIDEO); - expect(bid.meta.advertiserDomains).to.deep.equal(['example.com']); + it('should parse instream video from bidderRequestData', () => { + const req = { + data: { + bidderRequestData: encodeURIComponent(JSON.stringify({ + bids: [{ + bidId: '2faedf3e89d125', + adUnitCode: 'ad-unit-instream', + mediaTypes: { video: { context: 'instream' } } + }] + })) + } + }; + + const res = spec.interpretResponse(instreamResponse, req); + expect(res[0].vastUrl).to.equal('instream'); + expect(res[0].renderer).to.not.exist; }); - it('should handle instream video response', function () { - const bidResponses = spec.interpretResponse(instreamVideoServerResponse, instreamBidderRequest); - expect(bidResponses).to.have.lengthOf(1); - const bid = bidResponses[0]; - expect(bid.requestId).to.equal('2faedf3e89d125'); - expect(bid.cpm).to.equal(1.5); - expect(bid.width).to.equal(640); - expect(bid.height).to.equal(480); - expect(bid.vastUrl).to.equal('Test Instream VAST'); - expect(bid.mediaType).to.equal(VIDEO); - expect(bid.ttl).to.equal(300); - expect(bid.renderer).to.not.exist; - expect(bid.meta.advertiserDomains).to.deep.equal(['example.com']); + it('should handle bidderRequestData as object (not string)', () => { + const req = { + data: { + bidderRequestData: { + bids: [{ + bidId: '2faedf3e89d125', + adUnitCode: 'ad-unit-instream', + mediaTypes: { video: { context: 'instream' } } + }] + } + } + }; + + const res = spec.interpretResponse(instreamResponse, req); + expect(res[0].vastUrl).to.equal('instream'); + }); + + it('should handle invalid JSON in bidderRequestData', () => { + const req = { + data: { + bidderRequestData: 'invalid-json' + } + }; + + const res = spec.interpretResponse(outstreamResponse, req); + expect(res).to.have.lengthOf(1); + // Should not crash, context will be undefined + }); + + it('should handle bidderRequestData without bids array', () => { + const req = { + data: { + bidderRequestData: encodeURIComponent(JSON.stringify({ refererInfo: {} })) + } + }; + + const res = spec.interpretResponse(outstreamResponse, req); + expect(res).to.have.lengthOf(1); + }); + + it('should handle empty bids array in bidderRequestData', () => { + const req = { + data: { + bidderRequestData: encodeURIComponent(JSON.stringify({ bids: [] })) + } + }; + + const res = spec.interpretResponse(outstreamResponse, req); + expect(res).to.have.lengthOf(1); + }); + + it('should find correct bid when multiple bids in bidderRequestData', () => { + const req = { + data: { + bidderRequestData: encodeURIComponent(JSON.stringify({ + bids: [ + { + bidId: 'OTHER_BID', + adUnitCode: 'other-unit', + mediaTypes: { video: { context: 'outstream' } } + }, + { + bidId: '2faedf3e89d124', + adUnitCode: 'ad-unit-outstream', + mediaTypes: { video: { context: 'outstream' } } + } + ] + })) + } + }; + + const res = spec.interpretResponse(outstreamResponse, req); + expect(res[0].vastXml).to.equal('outstream'); + expect(res[0].renderer).to.exist; + }); + + it('should handle missing mediaTypes in bid', () => { + const req = { + data: { + bidderRequestData: encodeURIComponent(JSON.stringify({ + bids: [{ + bidId: '2faedf3e89d124', + adUnitCode: 'ad-unit-outstream' + }] + })) + } + }; + + const res = spec.interpretResponse(outstreamResponse, req); + expect(res).to.have.lengthOf(1); + // Should not crash, context will be undefined + }); + }); + + // INTERPRET RESPONSE - EDGE CASES + + describe('interpretResponse - edge cases', () => { + it('should return empty array when serverResponse.body is empty object', () => { + const res = spec.interpretResponse({ body: {} }, { data: {} }); + expect(res).to.have.lengthOf(0); + }); + + it('should return empty array when serverResponse.body is null', () => { + const res = spec.interpretResponse({ body: null }, { data: {} }); + expect(res).to.have.lengthOf(0); + }); + + it('should return empty array when serverResponse.body is undefined', () => { + const res = spec.interpretResponse({ body: undefined }, { data: {} }); + expect(res).to.have.lengthOf(0); + }); + + it('should handle request without data object', () => { + const bannerResponse = { + body: { + slotBidId: '2faedf3e89d123', + ad: '
BANNER
', + cpm: 1, + mediaType: BANNER + } + }; + const res = spec.interpretResponse(bannerResponse, {}); + expect(res).to.have.lengthOf(1); + }); + + it('should handle video response without context (neither videoContext nor bidderRequestData)', () => { + const videoResponse = { + body: { + slotBidId: 'BID_VIDEO', + ad: 'video', + cpm: 2, + mediaType: VIDEO, + adWidth: 640, + adHeight: 480 + } + }; + const req = { data: {} }; + const res = spec.interpretResponse(videoResponse, req); + expect(res).to.have.lengthOf(1); + // Neither vastUrl nor vastXml should be set + expect(res[0].vastUrl).to.not.exist; + expect(res[0].vastXml).to.not.exist; + }); + + it('should handle negative cpm', () => { + const responseNegativeCpm = { + body: { + slotBidId: '2faedf3e89d123', + ad: '
BANNER
', + cpm: -1, + mediaType: BANNER + } + }; + const req = { data: { q: 'dummy' } }; + const result = spec.interpretResponse(responseNegativeCpm, req); + expect(result[0].cpm).to.equal(0); + }); + }); + + // RENDERER TESTS + + describe('renderer functionality', () => { + it('should create renderer with correct configuration', () => { + const outstreamResponse = { + body: { + slotBidId: '2faedf3e89d124', + ad: 'outstream', + cpm: 2, + mediaType: VIDEO, + adWidth: 640, + adHeight: 480, + rUrl: 'https://cdn/renderer.js', + advertiserDomains: ['example.com'] + } + }; + + const req = { + data: { + videoContext: 'outstream', + adUnitCode: 'ad-unit-outstream' + } + }; + + const res = spec.interpretResponse(outstreamResponse, req); + const renderer = res[0].renderer; + + expect(renderer).to.exist; + expect(renderer.url).to.equal('https://cdn/renderer.js'); + expect(renderer.id).to.equal('2faedf3e89d124'); + expect(typeof renderer.setRender).to.equal('function'); + }); + + it('should execute renderer callback when onetag is available', () => { + const outstreamResponse = { + body: { + slotBidId: '2faedf3e89d124', + ad: 'outstream', + cpm: 2, + mediaType: VIDEO, + adWidth: 640, + adHeight: 480, + rUrl: 'https://cdn/test.xml', + advertiserDomains: ['example.com'] + } + }; + + const req = { + data: { + videoContext: 'outstream', + adUnitCode: 'ad-unit-outstream' + } + }; + + const originalOnetag = window.onetag; + let playerInitCalled = false; + + window.onetag = { + Player: { + init: function (config) { + playerInitCalled = true; + expect(config).to.exist; + expect(config.width).to.exist; + expect(config.height).to.exist; + expect(config.vastXml).to.exist; + expect(config.nodeId).to.exist; + } + } + }; + + try { + const res = spec.interpretResponse(outstreamResponse, req); + const renderer = res[0].renderer; + + renderer.loaded = true; + const renderFn = renderer._render; + expect(renderFn).to.exist; + + renderFn.call(renderer, { + renderer: renderer, + width: 640, + height: 480, + vastXml: 'outstream', + adUnitCode: 'ad-unit-outstream' + }); + + expect(playerInitCalled).to.equal(true); + } finally { + if (originalOnetag) { + window.onetag = originalOnetag; + } else { + delete window.onetag; + } + } + }); + + it('should handle renderer setRender errors gracefully', () => { + // This tests the try-catch block in createRenderer + const outstreamResponse = { + body: { + slotBidId: '2faedf3e89d124', + ad: 'outstream', + cpm: 2, + mediaType: VIDEO, + adWidth: 640, + adHeight: 480, + rUrl: 'https://cdn/test.xml', + advertiserDomains: ['example.com'] + } + }; + + const req = { + data: { + videoContext: 'outstream', + adUnitCode: 'ad-unit-outstream' + } + }; + + // Should not throw even if setRender fails + expect(() => { + spec.interpretResponse(outstreamResponse, req); + }).to.not.throw(); }); }); }); diff --git a/test/spec/modules/intentIqAnalyticsAdapter_spec.js b/test/spec/modules/intentIqAnalyticsAdapter_spec.js index 664c6041ec8..e93b6ec1915 100644 --- a/test/spec/modules/intentIqAnalyticsAdapter_spec.js +++ b/test/spec/modules/intentIqAnalyticsAdapter_spec.js @@ -1,103 +1,127 @@ -import { expect } from 'chai'; -import iiqAnalyticsAnalyticsAdapter from 'modules/intentIqAnalyticsAdapter.js'; -import * as utils from 'src/utils.js'; -import { server } from 'test/mocks/xhr.js'; -import { config } from 'src/config.js'; -import { EVENTS } from 'src/constants.js'; -import * as events from 'src/events.js'; -import { getStorageManager } from 'src/storageManager.js'; -import sinon from 'sinon'; -import { REPORTER_ID, preparePayload, restoreReportList } from '../../../modules/intentIqAnalyticsAdapter.js'; -import { FIRST_PARTY_KEY, PREBID, VERSION } from '../../../libraries/intentIqConstants/intentIqConstants.js'; -import * as detectBrowserUtils from '../../../libraries/intentIqUtils/detectBrowserUtils.js'; -import { getReferrer, appendVrrefAndFui } from '../../../libraries/intentIqUtils/getRefferer.js'; -import { gppDataHandler, uspDataHandler, gdprDataHandler } from '../../../src/consentHandler.js'; +import { expect } from "chai"; +import iiqAnalyticsAnalyticsAdapter from "modules/intentIqAnalyticsAdapter.js"; +import * as utils from "src/utils.js"; +import { server } from "test/mocks/xhr.js"; +import { EVENTS } from "src/constants.js"; +import * as events from "src/events.js"; +import sinon from "sinon"; +import { + REPORTER_ID, + preparePayload, + restoreReportList, +} from "../../../modules/intentIqAnalyticsAdapter.js"; +import { + FIRST_PARTY_KEY, + PREBID, + VERSION, + WITHOUT_IIQ, + WITH_IIQ, + AB_CONFIG_SOURCE, +} from "../../../libraries/intentIqConstants/intentIqConstants.js"; +import * as detectBrowserUtils from "../../../libraries/intentIqUtils/detectBrowserUtils.js"; +import { + getReferrer, + appendVrrefAndFui, +} from "../../../libraries/intentIqUtils/getRefferer.js"; +import { + gppDataHandler, + uspDataHandler, + gdprDataHandler, +} from "../../../src/consentHandler.js"; const partner = 10; -const defaultData = '{"pcid":"f961ffb1-a0e1-4696-a9d2-a21d815bd344", "group": "A"}'; +const defaultIdentityObject = { + firstPartyData: { + pcid: "f961ffb1-a0e1-4696-a9d2-a21d815bd344", + pcidDate: 1762527405808, + uspString: "undefined", + gppString: "undefined", + gdprString: "", + date: Date.now(), + sCal: Date.now() - 36000, + isOptedOut: false, + pid: "profile", + dbsaved: "true", + spd: "spd", + }, + partnerData: { + abTestUuid: "abTestUuid", + adserverDeviceType: 1, + clientType: 2, + cttl: 43200000, + date: Date.now(), + profile: "profile", + wsrvcll: true, + }, + clientHints: { + 0: '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"', + 1: "?0", + 2: '"macOS"', + 3: '"arm"', + 4: '"64"', + 6: '"15.6.1"', + 7: "?0", + 8: '"Chromium";v="142.0.7444.60", "Google Chrome";v="142.0.7444.60", "Not_A Brand";v="99.0.0.0"', + }, +}; const version = VERSION; -const REPORT_ENDPOINT = 'https://reports.intentiq.com/report'; -const REPORT_ENDPOINT_GDPR = 'https://reports-gdpr.intentiq.com/report'; -const REPORT_SERVER_ADDRESS = 'https://test-reports.intentiq.com/report'; +const REPORT_ENDPOINT = "https://reports.intentiq.com/report"; +const REPORT_ENDPOINT_GDPR = "https://reports-gdpr.intentiq.com/report"; +const REPORT_SERVER_ADDRESS = "https://test-reports.intentiq.com/report"; -const storage = getStorageManager({ moduleType: 'analytics', moduleName: 'iiqAnalytics' }); +const randomVal = () => Math.floor(Math.random() * 100000) + 1; -const randomVal = () => Math.floor(Math.random() * 100000) + 1 - -const getUserConfig = () => [ - { - 'name': 'intentIqId', - 'params': { - 'partner': partner, - 'unpack': null, - 'manualWinReportEnabled': false, - }, - 'storage': { - 'type': 'html5', - 'name': 'intentIqId', - 'expires': 60, - 'refreshInSeconds': 14400 - } +const getDefaultConfig = () => { + return { + partner, + manualWinReportEnabled: false, } -]; - -const getUserConfigWithReportingServerAddress = () => [ - { - 'name': 'intentIqId', - 'params': { - 'partner': partner, - 'unpack': null, - }, - 'storage': { - 'type': 'html5', - 'name': 'intentIqId', - 'expires': 60, - 'refreshInSeconds': 14400 - } - } -]; +} const getWonRequest = () => ({ - 'bidderCode': 'pubmatic', - 'width': 728, - 'height': 90, - 'statusMessage': 'Bid available', - 'adId': '23caeb34c55da51', - 'requestId': '87615b45ca4973', - 'transactionId': '5e69fd76-8c86-496a-85ce-41ae55787a50', - 'auctionId': '0cbd3a43-ff45-47b8-b002-16d3946b23bf-' + randomVal(), - 'mediaType': 'banner', - 'source': 'client', - 'cpm': 5, - 'currency': 'USD', - 'ttl': 300, - 'referrer': '', - 'adapterCode': 'pubmatic', - 'originalCpm': 5, - 'originalCurrency': 'USD', - 'responseTimestamp': 1669644710345, - 'requestTimestamp': 1669644710109, - 'bidder': 'testbidder', - 'timeToRespond': 236, - 'pbLg': '5.00', - 'pbMg': '5.00', - 'pbHg': '5.00', - 'pbAg': '5.00', - 'pbDg': '5.00', - 'pbCg': '', - 'size': '728x90', - 'status': 'rendered' + bidderCode: "pubmatic", + width: 728, + height: 90, + statusMessage: "Bid available", + adId: "23caeb34c55da51", + requestId: "87615b45ca4973", + transactionId: "5e69fd76-8c86-496a-85ce-41ae55787a50", + auctionId: "0cbd3a43-ff45-47b8-b002-16d3946b23bf-" + randomVal(), + mediaType: "banner", + source: "client", + cpm: 5, + currency: "USD", + ttl: 300, + referrer: "", + adapterCode: "pubmatic", + originalCpm: 5, + originalCurrency: "USD", + responseTimestamp: 1669644710345, + requestTimestamp: 1669644710109, + bidder: "testbidder", + timeToRespond: 236, + pbLg: "5.00", + pbMg: "5.00", + pbHg: "5.00", + pbAg: "5.00", + pbDg: "5.00", + pbCg: "", + size: "728x90", + status: "rendered", }); -const enableAnalyticWithSpecialOptions = (options) => { - iiqAnalyticsAnalyticsAdapter.disableAnalytics() +const enableAnalyticWithSpecialOptions = (receivedOptions) => { + iiqAnalyticsAnalyticsAdapter.disableAnalytics(); iiqAnalyticsAnalyticsAdapter.enableAnalytics({ - provider: 'iiqAnalytics', - options - }) -} + provider: "iiqAnalytics", + options: { + ...getDefaultConfig(), + ...receivedOptions + }, + }); +}; -describe('IntentIQ tests all', function () { +describe("IntentIQ tests all", function () { let logErrorStub; let getWindowSelfStub; let getWindowTopStub; @@ -105,12 +129,8 @@ describe('IntentIQ tests all', function () { let detectBrowserStub; beforeEach(function () { - logErrorStub = sinon.stub(utils, 'logError'); - sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(getUserConfig()); - sinon.stub(events, 'getEvents').returns([]); - iiqAnalyticsAnalyticsAdapter.enableAnalytics({ - provider: 'iiqAnalytics', - }); + logErrorStub = sinon.stub(utils, "logError"); + sinon.stub(events, "getEvents").returns([]); iiqAnalyticsAnalyticsAdapter.initOptions = { lsValueInitialized: false, partner: null, @@ -121,12 +141,17 @@ describe('IntentIQ tests all', function () { eidl: null, lsIdsInitialized: false, manualWinReportEnabled: false, - domainName: null + domainName: null, }; + iiqAnalyticsAnalyticsAdapter.enableAnalytics({ + provider: "iiqAnalytics", + options: getDefaultConfig() + }); if (iiqAnalyticsAnalyticsAdapter.track.restore) { iiqAnalyticsAnalyticsAdapter.track.restore(); } - sinon.spy(iiqAnalyticsAnalyticsAdapter, 'track'); + sinon.spy(iiqAnalyticsAnalyticsAdapter, "track"); + window[`iiq_identity_${partner}`] = defaultIdentityObject; }); afterEach(function () { @@ -135,7 +160,6 @@ describe('IntentIQ tests all', function () { if (getWindowTopStub) getWindowTopStub.restore(); if (getWindowLocationStub) getWindowLocationStub.restore(); if (detectBrowserStub) detectBrowserStub.restore(); - config.getConfig.restore(); events.getEvents.restore(); iiqAnalyticsAnalyticsAdapter.disableAnalytics(); if (iiqAnalyticsAnalyticsAdapter.track.restore) { @@ -143,20 +167,15 @@ describe('IntentIQ tests all', function () { } localStorage.clear(); server.reset(); + delete window[`iiq_identity_${partner}`] }); - it('should send POST request with payload in request body if reportMethod is POST', function () { + it("should send POST request with payload in request body if reportMethod is POST", function () { enableAnalyticWithSpecialOptions({ - reportMethod: 'POST' - }) - const [userConfig] = getUserConfig(); + reportMethod: "POST", + }); const wonRequest = getWonRequest(); - config.getConfig.restore(); - sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); - - localStorage.setItem(FIRST_PARTY_KEY, defaultData); - events.emit(EVENTS.BID_WON, wonRequest); const request = server.requests[0]; @@ -165,25 +184,21 @@ describe('IntentIQ tests all', function () { const expectedData = preparePayload(wonRequest); const expectedPayload = `["${btoa(JSON.stringify(expectedData))}"]`; - expect(request.method).to.equal('POST'); + expect(request.method).to.equal("POST"); expect(request.requestBody).to.equal(expectedPayload); }); - it('should send GET request with payload in query string if reportMethod is NOT provided', function () { - const [userConfig] = getUserConfig(); + it("should send GET request with payload in query string if reportMethod is NOT provided", function () { const wonRequest = getWonRequest(); - config.getConfig.restore(); - sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); - localStorage.setItem(FIRST_PARTY_KEY, defaultData); events.emit(EVENTS.BID_WON, wonRequest); const request = server.requests[0]; - expect(request.method).to.equal('GET'); + expect(request.method).to.equal("GET"); const url = new URL(request.url); - const payloadEncoded = url.searchParams.get('payload'); + const payloadEncoded = url.searchParams.get("payload"); const decoded = JSON.parse(atob(JSON.parse(payloadEncoded)[0])); restoreReportList(); @@ -194,66 +209,79 @@ describe('IntentIQ tests all', function () { expect(decoded.prebidAuctionId).to.equal(expected.prebidAuctionId); }); - it('IIQ Analytical Adapter bid win report', function () { - localStorage.setItem(FIRST_PARTY_KEY, defaultData); - getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876' }); + it("IIQ Analytical Adapter bid win report", function () { + getWindowLocationStub = sinon + .stub(utils, "getWindowLocation") + .returns({ href: "http://localhost:9876" }); const expectedVrref = getWindowLocationStub().href; events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; const parsedUrl = new URL(request.url); - const vrref = parsedUrl.searchParams.get('vrref'); - expect(request.url).to.contain(REPORT_ENDPOINT + '?pid=' + partner + '&mct=1'); + const vrref = parsedUrl.searchParams.get("vrref"); + expect(request.url).to.contain( + REPORT_ENDPOINT + "?pid=" + partner + "&mct=1" + ); expect(request.url).to.contain(`&jsver=${version}`); - expect(`&vrref=${decodeURIComponent(vrref)}`).to.contain(`&vrref=${expectedVrref}`); - expect(request.url).to.contain('&payload='); - expect(request.url).to.contain('iiqid=f961ffb1-a0e1-4696-a9d2-a21d815bd344'); + expect(`&vrref=${decodeURIComponent(vrref)}`).to.contain( + `&vrref=${expectedVrref}` + ); + expect(request.url).to.contain("&payload="); + expect(request.url).to.contain( + "iiqid=f961ffb1-a0e1-4696-a9d2-a21d815bd344" + ); }); - it('should include adType in payload when present in BID_WON event', function () { - localStorage.setItem(FIRST_PARTY_KEY, defaultData); - getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); - const bidWonEvent = { ...getWonRequest(), mediaType: 'video' }; + it("should include adType in payload when present in BID_WON event", function () { + getWindowLocationStub = sinon + .stub(utils, "getWindowLocation") + .returns({ href: "http://localhost:9876/" }); + const bidWonEvent = { ...getWonRequest(), mediaType: "video" }; events.emit(EVENTS.BID_WON, bidWonEvent); const request = server.requests[0]; const urlParams = new URL(request.url); - const payloadEncoded = urlParams.searchParams.get('payload'); + const payloadEncoded = urlParams.searchParams.get("payload"); const payloadDecoded = JSON.parse(atob(JSON.parse(payloadEncoded)[0])); expect(server.requests.length).to.be.above(0); - expect(payloadDecoded).to.have.property('adType', bidWonEvent.mediaType); + expect(payloadDecoded).to.have.property("adType", bidWonEvent.mediaType); }); - it('should include adType in payload when present in reportExternalWin event', function () { - enableAnalyticWithSpecialOptions({ manualWinReportEnabled: true }) - getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); - const externalWinEvent = { cpm: 1, currency: 'USD', adType: 'banner' }; - const [userConfig] = getUserConfig(); - config.getConfig.restore(); - sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); - - const partnerId = userConfig.params.partner; + it("should include adType in payload when present in reportExternalWin event", function () { + enableAnalyticWithSpecialOptions({ manualWinReportEnabled: true }); + getWindowLocationStub = sinon + .stub(utils, "getWindowLocation") + .returns({ href: "http://localhost:9876/" }); + const externalWinEvent = { cpm: 1, currency: "USD", adType: "banner" }; events.emit(EVENTS.BID_REQUESTED); - window[`intentIqAnalyticsAdapter_${partnerId}`].reportExternalWin(externalWinEvent); + window[`intentIqAnalyticsAdapter_${partner}`].reportExternalWin( + externalWinEvent + ); const request = server.requests[0]; const urlParams = new URL(request.url); - const payloadEncoded = urlParams.searchParams.get('payload'); + const payloadEncoded = urlParams.searchParams.get("payload"); const payloadDecoded = JSON.parse(atob(JSON.parse(payloadEncoded)[0])); expect(server.requests.length).to.be.above(0); - expect(payloadDecoded).to.have.property('adType', externalWinEvent.adType); + expect(payloadDecoded).to.have.property("adType", externalWinEvent.adType); }); - it('should send report to report-gdpr address if gdpr is detected', function () { - const gppStub = sinon.stub(gppDataHandler, 'getConsentData').returns({ gppString: '{"key1":"value1","key2":"value2"}' }); - const uspStub = sinon.stub(uspDataHandler, 'getConsentData').returns('1NYN'); - const gdprStub = sinon.stub(gdprDataHandler, 'getConsentData').returns({ consentString: 'gdprConsent' }); + it("should send report to report-gdpr address if gdpr is detected", function () { + const gppStub = sinon + .stub(gppDataHandler, "getConsentData") + .returns({ gppString: '{"key1":"value1","key2":"value2"}' }); + const uspStub = sinon + .stub(uspDataHandler, "getConsentData") + .returns("1NYN"); + const gdprStub = sinon + .stub(gdprDataHandler, "getConsentData") + .returns({ consentString: "gdprConsent" }); events.emit(EVENTS.BID_WON, getWonRequest()); @@ -266,178 +294,208 @@ describe('IntentIQ tests all', function () { gdprStub.restore(); }); - it('should initialize with default configurations', function () { - expect(iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized).to.be.false; + it("should initialize with default configurations", function () { + expect(iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized).to.be + .false; }); - it('should handle BID_WON event with group configuration from local storage', function () { - localStorage.setItem(FIRST_PARTY_KEY, '{"pcid":"testpcid", "group": "B"}'); - const expectedVrref = encodeURIComponent('http://localhost:9876/'); + it("should handle BID_WON event with group configuration from local storage", function () { + window[`iiq_identity_${partner}`].firstPartyData = { + ...window[`iiq_identity_${partner}`].firstPartyData, + group: "B", + }; + + const expectedVrref = encodeURIComponent("http://localhost:9876/"); events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; - expect(request.url).to.contain('https://reports.intentiq.com/report?pid=' + partner + '&mct=1'); + expect(request.url).to.contain( + "https://reports.intentiq.com/report?pid=" + partner + "&mct=1" + ); expect(request.url).to.contain(`&jsver=${version}`); expect(request.url).to.contain(`&vrref=${expectedVrref}`); - expect(request.url).to.contain('iiqid=testpcid'); }); - it('should handle BID_WON event with default group configuration', function () { - localStorage.setItem(FIRST_PARTY_KEY, defaultData); - const defaultDataObj = JSON.parse(defaultData) + it("should handle BID_WON event with default group configuration", function () { const wonRequest = getWonRequest(); events.emit(EVENTS.BID_WON, wonRequest); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; - restoreReportList() + restoreReportList(); const dataToSend = preparePayload(wonRequest); const base64String = btoa(JSON.stringify(dataToSend)); const payload = encodeURIComponent(JSON.stringify([base64String])); - const expectedUrl = appendVrrefAndFui(REPORT_ENDPOINT + - `?pid=${partner}&mct=1&iiqid=${defaultDataObj.pcid}&agid=${REPORTER_ID}&jsver=${version}&source=pbjs&uh=&gdpr=0`, iiqAnalyticsAnalyticsAdapter.initOptions.domainName + const expectedUrl = appendVrrefAndFui( + REPORT_ENDPOINT + + `?pid=${partner}&mct=1&iiqid=${defaultIdentityObject.firstPartyData.pcid}&agid=${REPORTER_ID}&jsver=${version}&source=pbjs&uh=&gdpr=0&spd=spd`, + iiqAnalyticsAnalyticsAdapter.initOptions.domainName ); const urlWithPayload = expectedUrl + `&payload=${payload}`; expect(request.url).to.equal(urlWithPayload); - expect(dataToSend.pcid).to.equal(defaultDataObj.pcid) + expect(dataToSend.pcid).to.equal(defaultIdentityObject.firstPartyData.pcid); }); - it('should send CMP data in report if available', function () { - const uspData = '1NYN'; + it("should send CMP data in report if available", function () { + const uspData = "1NYN"; const gppData = { gppString: '{"key1":"value1","key2":"value2"}' }; - const gdprData = { consentString: 'gdprConsent' }; - - const gppStub = sinon.stub(gppDataHandler, 'getConsentData').returns(gppData); - const uspStub = sinon.stub(uspDataHandler, 'getConsentData').returns(uspData); - const gdprStub = sinon.stub(gdprDataHandler, 'getConsentData').returns(gdprData); - - getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); + const gdprData = { consentString: "gdprConsent" }; + + const gppStub = sinon + .stub(gppDataHandler, "getConsentData") + .returns(gppData); + const uspStub = sinon + .stub(uspDataHandler, "getConsentData") + .returns(uspData); + const gdprStub = sinon + .stub(gdprDataHandler, "getConsentData") + .returns(gdprData); + + getWindowLocationStub = sinon + .stub(utils, "getWindowLocation") + .returns({ href: "http://localhost:9876/" }); events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; - expect(request.url).to.contain(`&us_privacy=${encodeURIComponent(uspData)}`); - expect(request.url).to.contain(`&gpp=${encodeURIComponent(gppData.gppString)}`); - expect(request.url).to.contain(`&gdpr_consent=${encodeURIComponent(gdprData.consentString)}`); + expect(request.url).to.contain( + `&us_privacy=${encodeURIComponent(uspData)}` + ); + expect(request.url).to.contain( + `&gpp=${encodeURIComponent(gppData.gppString)}` + ); + expect(request.url).to.contain( + `&gdpr_consent=${encodeURIComponent(gdprData.consentString)}` + ); expect(request.url).to.contain(`&gdpr=1`); gppStub.restore(); uspStub.restore(); gdprStub.restore(); }); - it('should not send request if manualWinReportEnabled is true', function () { + it("should not send request if manualWinReportEnabled is true", function () { iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = true; events.emit(EVENTS.BID_WON, getWonRequest()); - expect(server.requests.length).to.equal(1); + expect(server.requests.length).to.equal(0); }); - it('should read data from local storage', function () { - localStorage.setItem(FIRST_PARTY_KEY, '{"group": "A"}'); - localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, '{"data":"testpcid", "eidl": 10}'); - events.emit(EVENTS.BID_WON, getWonRequest()); - expect(iiqAnalyticsAnalyticsAdapter.initOptions.dataInLs).to.equal('testpcid'); - expect(iiqAnalyticsAnalyticsAdapter.initOptions.eidl).to.equal(10); - expect(iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup).to.equal('A'); - }); + it("should handle initialization values from local storage", function () { + window[`iiq_identity_${partner}`].actualABGroup = WITHOUT_IIQ; - it('should handle initialization values from local storage', function () { - localStorage.setItem(FIRST_PARTY_KEY, '{"pcid":"testpcid", "group": "B"}'); - localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, '{"data":"testpcid"}'); events.emit(EVENTS.BID_WON, getWonRequest()); - expect(iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup).to.equal('B'); + expect(iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup).to.equal( + WITHOUT_IIQ + ); expect(iiqAnalyticsAnalyticsAdapter.initOptions.fpid).to.be.not.null; }); - it('should handle reportExternalWin', function () { + it("should handle reportExternalWin", function () { events.emit(EVENTS.BID_REQUESTED); iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = false; - localStorage.setItem(FIRST_PARTY_KEY, '{"pcid":"testpcid", "group": "B"}'); - localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, '{"data":"testpcid"}'); - expect(window[`intentIqAnalyticsAdapter_${partner}`].reportExternalWin).to.be.a('function'); - expect(window[`intentIqAnalyticsAdapter_${partner}`].reportExternalWin({ cpm: 1, currency: 'USD' })).to.equal(false); + expect( + window[`intentIqAnalyticsAdapter_${partner}`].reportExternalWin + ).to.be.a("function"); + expect( + window[`intentIqAnalyticsAdapter_${partner}`].reportExternalWin({ + cpm: 1, + currency: "USD", + }) + ).to.equal(false); }); - it('should return window.location.href when window.self === window.top', function () { + it("should return window.location.href when window.self === window.top", function () { // Stub helper functions - getWindowSelfStub = sinon.stub(utils, 'getWindowSelf').returns(window); - getWindowTopStub = sinon.stub(utils, 'getWindowTop').returns(window); - getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); + getWindowSelfStub = sinon.stub(utils, "getWindowSelf").returns(window); + getWindowTopStub = sinon.stub(utils, "getWindowTop").returns(window); + getWindowLocationStub = sinon + .stub(utils, "getWindowLocation") + .returns({ href: "http://localhost:9876/" }); const referrer = getReferrer(); - expect(referrer).to.equal('http://localhost:9876/'); + expect(referrer).to.equal("http://localhost:9876/"); }); - it('should return window.top.location.href when window.self !== window.top and access is successful', function () { + it("should return window.top.location.href when window.self !== window.top and access is successful", function () { // Stub helper functions to simulate iframe - getWindowSelfStub = sinon.stub(utils, 'getWindowSelf').returns({}); - getWindowTopStub = sinon.stub(utils, 'getWindowTop').returns({ location: { href: 'http://example.com/' } }); + getWindowSelfStub = sinon.stub(utils, "getWindowSelf").returns({}); + getWindowTopStub = sinon + .stub(utils, "getWindowTop") + .returns({ location: { href: "http://example.com/" } }); const referrer = getReferrer(); - expect(referrer).to.equal('http://example.com/'); + expect(referrer).to.equal("http://example.com/"); }); - it('should return an empty string and log an error when accessing window.top.location.href throws an error', function () { + it("should return an empty string and log an error when accessing window.top.location.href throws an error", function () { // Stub helper functions to simulate error - getWindowSelfStub = sinon.stub(utils, 'getWindowSelf').returns({}); - getWindowTopStub = sinon.stub(utils, 'getWindowTop').throws(new Error('Access denied')); + getWindowSelfStub = sinon.stub(utils, "getWindowSelf").returns({}); + getWindowTopStub = sinon + .stub(utils, "getWindowTop") + .throws(new Error("Access denied")); const referrer = getReferrer(); - expect(referrer).to.equal(''); + expect(referrer).to.equal(""); expect(logErrorStub.calledOnce).to.be.true; - expect(logErrorStub.firstCall.args[0]).to.contain('Error accessing location: Error: Access denied'); + expect(logErrorStub.firstCall.args[0]).to.contain( + "Error accessing location: Error: Access denied" + ); }); - it('should not send request if the browser is in blacklist (chrome)', function () { - const USERID_CONFIG_BROWSER = [...getUserConfig()]; - USERID_CONFIG_BROWSER[0].params.browserBlackList = 'ChrOmE'; - - config.getConfig.restore(); - sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(USERID_CONFIG_BROWSER); - detectBrowserStub = sinon.stub(detectBrowserUtils, 'detectBrowser').returns('chrome'); + it("should not send request if the browser is in blacklist (chrome)", function () { + enableAnalyticWithSpecialOptions({ + browserBlackList: "ChrOmE" + }) + detectBrowserStub = sinon + .stub(detectBrowserUtils, "detectBrowser") + .returns("chrome"); - localStorage.setItem(FIRST_PARTY_KEY, defaultData); events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.equal(0); }); - it('should send request if the browser is not in blacklist (safari)', function () { - const USERID_CONFIG_BROWSER = [...getUserConfig()]; - USERID_CONFIG_BROWSER[0].params.browserBlackList = 'chrome,firefox'; + it("should send request if the browser is not in blacklist (safari)", function () { + enableAnalyticWithSpecialOptions({ + browserBlackList: "chrome,firefox" + }) - config.getConfig.restore(); - sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(USERID_CONFIG_BROWSER); - detectBrowserStub = sinon.stub(detectBrowserUtils, 'detectBrowser').returns('safari'); + detectBrowserStub = sinon + .stub(detectBrowserUtils, "detectBrowser") + .returns("safari"); - localStorage.setItem(FIRST_PARTY_KEY, defaultData); events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; - expect(request.url).to.contain(`https://reports.intentiq.com/report?pid=${partner}&mct=1`); + expect(request.url).to.contain( + `https://reports.intentiq.com/report?pid=${partner}&mct=1` + ); expect(request.url).to.contain(`&jsver=${version}`); - expect(request.url).to.contain(`&vrref=${encodeURIComponent('http://localhost:9876/')}`); - expect(request.url).to.contain('&payload='); - expect(request.url).to.contain('iiqid=f961ffb1-a0e1-4696-a9d2-a21d815bd344'); + expect(request.url).to.contain( + `&vrref=${encodeURIComponent("http://localhost:9876/")}` + ); + expect(request.url).to.contain("&payload="); + expect(request.url).to.contain( + "iiqid=f961ffb1-a0e1-4696-a9d2-a21d815bd344" + ); }); - it('should send request in reportingServerAddress no gdpr', function () { - const USERID_CONFIG_BROWSER = [...getUserConfigWithReportingServerAddress()]; - USERID_CONFIG_BROWSER[0].params.browserBlackList = 'chrome,firefox'; - - config.getConfig.restore(); - sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(USERID_CONFIG_BROWSER); - detectBrowserStub = sinon.stub(detectBrowserUtils, 'detectBrowser').returns('safari'); - enableAnalyticWithSpecialOptions({ reportingServerAddress: REPORT_SERVER_ADDRESS }) + it("should send request in reportingServerAddress no gdpr", function () { + detectBrowserStub = sinon + .stub(detectBrowserUtils, "detectBrowser") + .returns("safari"); + enableAnalyticWithSpecialOptions({ + reportingServerAddress: REPORT_SERVER_ADDRESS, + browserBlackList: "chrome,firefox" + }); - localStorage.setItem(FIRST_PARTY_KEY, defaultData); events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); @@ -445,9 +503,7 @@ describe('IntentIQ tests all', function () { expect(request.url).to.contain(REPORT_SERVER_ADDRESS); }); - it('should include source parameter in report URL', function () { - localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify(defaultData)); - + it("should include source parameter in report URL", function () { events.emit(EVENTS.BID_WON, getWonRequest()); const request = server.requests[0]; @@ -455,64 +511,51 @@ describe('IntentIQ tests all', function () { expect(request.url).to.include(`&source=${PREBID}`); }); - it('should use correct key if siloEnabled is true', function () { - const siloEnabled = true; - const USERID_CONFIG = [...getUserConfig()]; - USERID_CONFIG[0].params.siloEnabled = siloEnabled; - - config.getConfig.restore(); - sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(USERID_CONFIG); + it("should send additionalParams in report if valid and small enough", function () { + enableAnalyticWithSpecialOptions({ + additionalParams: [ + { + parameterName: "general", + parameterValue: "Lee", + destination: [0, 0, 1], + }, + ] + }) - localStorage.setItem(FIRST_PARTY_KEY, `${FIRST_PARTY_KEY}${siloEnabled ? '_p_' + partner : ''}`); events.emit(EVENTS.BID_WON, getWonRequest()); - expect(server.requests.length).to.be.above(0); const request = server.requests[0]; - expect(request.url).to.contain(REPORT_ENDPOINT + '?pid=' + partner + '&mct=1'); + expect(request.url).to.include("general=Lee"); }); - it('should send additionalParams in report if valid and small enough', function () { - const userConfig = getUserConfig(); - userConfig[0].params.additionalParams = [{ - parameterName: 'general', - parameterValue: 'Lee', - destination: [0, 0, 1] - }]; + it("should not send additionalParams in report if value is too large", function () { + const longVal = "x".repeat(5000000); - config.getConfig.restore(); - sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(userConfig); + enableAnalyticWithSpecialOptions({ + additionalParams: [ + { + parameterName: "general", + parameterValue: longVal, + destination: [0, 0, 1], + }, + ] + }) - localStorage.setItem(FIRST_PARTY_KEY, defaultData); events.emit(EVENTS.BID_WON, getWonRequest()); const request = server.requests[0]; - expect(request.url).to.include('general=Lee'); + expect(request.url).not.to.include("general"); }); - it('should not send additionalParams in report if value is too large', function () { - const longVal = 'x'.repeat(5000000); - const userConfig = getUserConfig(); - userConfig[0].params.additionalParams = [{ - parameterName: 'general', - parameterValue: longVal, - destination: [0, 0, 1] - }]; - - config.getConfig.restore(); - sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(userConfig); - - localStorage.setItem(FIRST_PARTY_KEY, defaultData); - events.emit(EVENTS.BID_WON, getWonRequest()); - - const request = server.requests[0]; - expect(request.url).not.to.include('general'); - }); - it('should include spd parameter from LS in report URL', function () { - const spdObject = { foo: 'bar', value: 42 }; + it("should include spd parameter from LS in report URL", function () { + const spdObject = { foo: "bar", value: 42 }; const expectedSpdEncoded = encodeURIComponent(JSON.stringify(spdObject)); + window[`iiq_identity_${partner}`].firstPartyData.spd = + JSON.stringify(spdObject); - localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({ ...defaultData, spd: spdObject })); - getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); + getWindowLocationStub = sinon + .stub(utils, "getWindowLocation") + .returns({ href: "http://localhost:9876/" }); events.emit(EVENTS.BID_WON, getWonRequest()); @@ -522,12 +565,14 @@ describe('IntentIQ tests all', function () { expect(request.url).to.include(`&spd=${expectedSpdEncoded}`); }); - it('should include spd parameter string from LS in report URL', function () { - const spdObject = 'server provided data'; - const expectedSpdEncoded = encodeURIComponent(spdObject); + it("should include spd parameter string from LS in report URL", function () { + const spdData = "server provided data"; + const expectedSpdEncoded = encodeURIComponent(spdData); + window[`iiq_identity_${partner}`].firstPartyData.spd = spdData; - localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({ ...defaultData, spd: spdObject })); - getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); + getWindowLocationStub = sinon + .stub(utils, "getWindowLocation") + .returns({ href: "http://localhost:9876/" }); events.emit(EVENTS.BID_WON, getWonRequest()); @@ -537,7 +582,7 @@ describe('IntentIQ tests all', function () { expect(request.url).to.include(`&spd=${expectedSpdEncoded}`); }); - describe('GAM prediction reporting', function () { + describe("GAM prediction reporting", function () { function createMockGAM() { const listeners = {}; return { @@ -545,86 +590,96 @@ describe('IntentIQ tests all', function () { pubads: () => ({ addEventListener: (name, cb) => { listeners[name] = cb; - } + }, }), - _listeners: listeners + _listeners: listeners, }; } - function withConfigGamPredict(gamObj) { - const [userConfig] = getUserConfig(); - userConfig.params.gamObjectReference = gamObj; - userConfig.params.gamPredictReporting = true; - config.getConfig.restore(); - sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); - } - - it('should subscribe to GAM and send report on slotRenderEnded without prior bidWon', function () { + it("should subscribe to GAM and send report on slotRenderEnded without prior bidWon", function () { const gam = createMockGAM(); - withConfigGamPredict(gam); + + enableAnalyticWithSpecialOptions({ + gamObjectReference: gam + }) // enable subscription by LS flag - localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, JSON.stringify({ gpr: true })); - localStorage.setItem(FIRST_PARTY_KEY, defaultData); + window[`iiq_identity_${partner}`].partnerData.gpr = true; // provide recent auctionEnd with matching bid to enrich payload events.getEvents.restore(); - sinon.stub(events, 'getEvents').returns([ + sinon.stub(events, "getEvents").returns([ { - eventType: 'auctionEnd', args: { - auctionId: 'auc-1', - adUnitCodes: ['ad-unit-1'], - bidsReceived: [{ bidder: 'pubmatic', adUnitCode: 'ad-unit-1', cpm: 1, currency: 'USD', originalCpm: 1, originalCurrency: 'USD', status: 'rendered' }] - } - } + eventType: "auctionEnd", + args: { + auctionId: "auc-1", + adUnitCodes: ["ad-unit-1"], + bidsReceived: [ + { + bidder: "pubmatic", + adUnitCode: "ad-unit-1", + cpm: 1, + currency: "USD", + originalCpm: 1, + originalCurrency: "USD", + status: "rendered", + }, + ], + }, + }, ]); // trigger adapter to subscribe events.emit(EVENTS.BID_REQUESTED); // execute GAM cmd to register listener - gam.cmd.forEach(fn => fn()); + gam.cmd.forEach((fn) => fn()); // simulate slotRenderEnded const slot = { - getSlotElementId: () => 'ad-unit-1', - getAdUnitPath: () => '/123/foo', - getTargetingKeys: () => ['hb_bidder', 'hb_adid'], - getTargeting: (k) => k === 'hb_bidder' ? ['pubmatic'] : k === 'hb_adid' ? ['ad123'] : [] + getSlotElementId: () => "ad-unit-1", + getAdUnitPath: () => "/123/foo", + getTargetingKeys: () => ["hb_bidder", "hb_adid"], + getTargeting: (k) => + k === "hb_bidder" ? ["pubmatic"] : k === "hb_adid" ? ["ad123"] : [], }; - if (gam._listeners['slotRenderEnded']) { - gam._listeners['slotRenderEnded']({ isEmpty: false, slot }); + if (gam._listeners["slotRenderEnded"]) { + gam._listeners["slotRenderEnded"]({ isEmpty: false, slot }); } expect(server.requests.length).to.be.above(0); }); - it('should NOT send report if a matching bidWon already exists', function () { + it("should NOT send report if a matching bidWon already exists", function () { const gam = createMockGAM(); - withConfigGamPredict(gam); - localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, JSON.stringify({ gpr: true })); - localStorage.setItem(FIRST_PARTY_KEY, defaultData); + localStorage.setItem( + FIRST_PARTY_KEY + "_" + partner, + JSON.stringify({ gpr: true }) + ); // provide prior bidWon matching placementId and hb_adid events.getEvents.restore(); - sinon.stub(events, 'getEvents').returns([ - { eventType: 'bidWon', args: { adId: 'ad123' }, id: 'ad-unit-1' } - ]); + sinon + .stub(events, "getEvents") + .returns([ + { eventType: "bidWon", args: { adId: "ad123" }, id: "ad-unit-1" }, + ]); events.emit(EVENTS.BID_REQUESTED); - gam.cmd.forEach(fn => fn()); + gam.cmd.forEach((fn) => fn()); const slot = { - getSlotElementId: () => 'ad-unit-1', - getAdUnitPath: () => '/123/foo', - getTargetingKeys: () => ['hb_bidder', 'hb_adid'], - getTargeting: (k) => k === 'hb_bidder' ? ['pubmatic'] : k === 'hb_adid' ? ['ad123'] : [] + getSlotElementId: () => "ad-unit-1", + getAdUnitPath: () => "/123/foo", + getTargetingKeys: () => ["hb_bidder", "hb_adid"], + getTargeting: (k) => + k === "hb_bidder" ? ["pubmatic"] : k === "hb_adid" ? ["ad123"] : [], }; const initialRequests = server.requests.length; - if (gam._listeners['slotRenderEnded']) { - gam._listeners['slotRenderEnded']({ isEmpty: false, slot }); + if (gam._listeners["slotRenderEnded"]) { + gam._listeners["slotRenderEnded"]({ isEmpty: false, slot }); } expect(server.requests.length).to.equal(initialRequests); }); @@ -632,136 +687,213 @@ describe('IntentIQ tests all', function () { const testCasesVrref = [ { - description: 'domainName matches window.top.location.href', + description: "domainName matches window.top.location.href", getWindowSelf: {}, - getWindowTop: { location: { href: 'http://example.com/page' } }, - getWindowLocation: { href: 'http://example.com/page' }, - domainName: 'example.com', - expectedVrref: encodeURIComponent('http://example.com/page'), - shouldContainFui: false + getWindowTop: { location: { href: "http://example.com/page" } }, + getWindowLocation: { href: "http://example.com/page" }, + domainName: "example.com", + expectedVrref: encodeURIComponent("http://example.com/page"), + shouldContainFui: false, }, { - description: 'domainName does not match window.top.location.href', + description: "domainName does not match window.top.location.href", getWindowSelf: {}, - getWindowTop: { location: { href: 'http://anotherdomain.com/page' } }, - getWindowLocation: { href: 'http://anotherdomain.com/page' }, - domainName: 'example.com', - expectedVrref: encodeURIComponent('example.com'), - shouldContainFui: false + getWindowTop: { location: { href: "http://anotherdomain.com/page" } }, + getWindowLocation: { href: "http://anotherdomain.com/page" }, + domainName: "example.com", + expectedVrref: encodeURIComponent("example.com"), + shouldContainFui: false, }, { - description: 'domainName is missing, only fui=1 is returned', + description: "domainName is missing, only fui=1 is returned", getWindowSelf: {}, - getWindowTop: { location: { href: '' } }, - getWindowLocation: { href: '' }, + getWindowTop: { location: { href: "" } }, + getWindowLocation: { href: "" }, domainName: null, - expectedVrref: '', - shouldContainFui: true + expectedVrref: "", + shouldContainFui: true, }, { - description: 'domainName is missing', + description: "domainName is missing", getWindowSelf: {}, - getWindowTop: { location: { href: 'http://example.com/page' } }, - getWindowLocation: { href: 'http://example.com/page' }, + getWindowTop: { location: { href: "http://example.com/page" } }, + getWindowLocation: { href: "http://example.com/page" }, domainName: null, - expectedVrref: encodeURIComponent('http://example.com/page'), - shouldContainFui: false + expectedVrref: encodeURIComponent("http://example.com/page"), + shouldContainFui: false, }, ]; - testCasesVrref.forEach(({ description, getWindowSelf, getWindowTop, getWindowLocation, domainName, expectedVrref, shouldContainFui }) => { - it(`should append correct vrref when ${description}`, function () { - getWindowSelfStub = sinon.stub(utils, 'getWindowSelf').returns(getWindowSelf); - getWindowTopStub = sinon.stub(utils, 'getWindowTop').returns(getWindowTop); - getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns(getWindowLocation); - - const url = 'https://reports.intentiq.com/report?pid=10'; - const modifiedUrl = appendVrrefAndFui(url, domainName); - const urlObj = new URL(modifiedUrl); - - const vrref = encodeURIComponent(urlObj.searchParams.get('vrref') || ''); - const fui = urlObj.searchParams.get('fui'); - - expect(vrref).to.equal(expectedVrref); - expect(urlObj.searchParams.has('fui')).to.equal(shouldContainFui); - if (shouldContainFui) { - expect(fui).to.equal('1'); - } - }); - }); + testCasesVrref.forEach( + ({ + description, + getWindowSelf, + getWindowTop, + getWindowLocation, + domainName, + expectedVrref, + shouldContainFui, + }) => { + it(`should append correct vrref when ${description}`, function () { + getWindowSelfStub = sinon + .stub(utils, "getWindowSelf") + .returns(getWindowSelf); + getWindowTopStub = sinon + .stub(utils, "getWindowTop") + .returns(getWindowTop); + getWindowLocationStub = sinon + .stub(utils, "getWindowLocation") + .returns(getWindowLocation); + + const url = "https://reports.intentiq.com/report?pid=10"; + const modifiedUrl = appendVrrefAndFui(url, domainName); + const urlObj = new URL(modifiedUrl); + + const vrref = encodeURIComponent( + urlObj.searchParams.get("vrref") || "" + ); + const fui = urlObj.searchParams.get("fui"); + + expect(vrref).to.equal(expectedVrref); + expect(urlObj.searchParams.has("fui")).to.equal(shouldContainFui); + if (shouldContainFui) { + expect(fui).to.equal("1"); + } + }); + } + ); const adUnitConfigTests = [ { adUnitConfig: 1, - description: 'should extract adUnitCode first (adUnitConfig = 1)', - event: { adUnitCode: 'adUnitCode-123', placementId: 'placementId-456' }, - expectedPlacementId: 'adUnitCode-123' + description: "should extract adUnitCode first (adUnitConfig = 1)", + event: { adUnitCode: "adUnitCode-123", placementId: "placementId-456" }, + expectedPlacementId: "adUnitCode-123", }, { adUnitConfig: 1, - description: 'should extract placementId if there is no adUnitCode (adUnitConfig = 1)', - event: { placementId: 'placementId-456' }, - expectedPlacementId: 'placementId-456' + description: + "should extract placementId if there is no adUnitCode (adUnitConfig = 1)", + event: { placementId: "placementId-456" }, + expectedPlacementId: "placementId-456", }, { adUnitConfig: 2, - description: 'should extract placementId first (adUnitConfig = 2)', - event: { adUnitCode: 'adUnitCode-123', placementId: 'placementId-456' }, - expectedPlacementId: 'placementId-456' + description: "should extract placementId first (adUnitConfig = 2)", + event: { adUnitCode: "adUnitCode-123", placementId: "placementId-456" }, + expectedPlacementId: "placementId-456", }, { adUnitConfig: 2, - description: 'should extract adUnitCode if there is no placementId (adUnitConfig = 2)', - event: { adUnitCode: 'adUnitCode-123', }, - expectedPlacementId: 'adUnitCode-123' + description: + "should extract adUnitCode if there is no placementId (adUnitConfig = 2)", + event: { adUnitCode: "adUnitCode-123" }, + expectedPlacementId: "adUnitCode-123", }, { adUnitConfig: 3, - description: 'should extract only adUnitCode (adUnitConfig = 3)', - event: { adUnitCode: 'adUnitCode-123', placementId: 'placementId-456' }, - expectedPlacementId: 'adUnitCode-123' + description: "should extract only adUnitCode (adUnitConfig = 3)", + event: { adUnitCode: "adUnitCode-123", placementId: "placementId-456" }, + expectedPlacementId: "adUnitCode-123", }, { adUnitConfig: 4, - description: 'should extract only placementId (adUnitConfig = 4)', - event: { adUnitCode: 'adUnitCode-123', placementId: 'placementId-456' }, - expectedPlacementId: 'placementId-456' + description: "should extract only placementId (adUnitConfig = 4)", + event: { adUnitCode: "adUnitCode-123", placementId: "placementId-456" }, + expectedPlacementId: "placementId-456", }, { adUnitConfig: 1, - description: 'should return empty placementId if neither adUnitCode or placementId exist', + description: + "should return empty placementId if neither adUnitCode or placementId exist", event: {}, - expectedPlacementId: '' + expectedPlacementId: "", }, { adUnitConfig: 1, - description: 'should extract placementId from params array if no top-level adUnitCode or placementId exist (adUnitConfig = 1)', + description: + "should extract placementId from params array if no top-level adUnitCode or placementId exist (adUnitConfig = 1)", event: { - params: [{ someKey: 'value' }, { placementId: 'nested-placementId' }] + params: [{ someKey: "value" }, { placementId: "nested-placementId" }], }, - expectedPlacementId: 'nested-placementId' - } + expectedPlacementId: "nested-placementId", + }, ]; - adUnitConfigTests.forEach(({ adUnitConfig, description, event, expectedPlacementId }) => { - it(description, function () { - const [userConfig] = getUserConfig(); - enableAnalyticWithSpecialOptions({ adUnitConfig }) + adUnitConfigTests.forEach( + ({ adUnitConfig, description, event, expectedPlacementId }) => { + it(description, function () { + enableAnalyticWithSpecialOptions({ adUnitConfig }); + + const testEvent = { ...getWonRequest(), ...event }; + events.emit(EVENTS.BID_WON, testEvent); + + const request = server.requests[0]; + const urlParams = new URL(request.url); + const encodedPayload = urlParams.searchParams.get("payload"); + const decodedPayload = JSON.parse(atob(JSON.parse(encodedPayload)[0])); + + expect(server.requests.length).to.be.above(0); + expect(encodedPayload).to.exist; + expect(decodedPayload).to.have.property( + "placementId", + expectedPlacementId + ); + }); + } + ); - config.getConfig.restore(); - sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); + it("should include ABTestingConfigurationSource in payload when provided", function () { + const ABTestingConfigurationSource = "percentage"; + enableAnalyticWithSpecialOptions({ ABTestingConfigurationSource }); - const testEvent = { ...getWonRequest(), ...event }; - events.emit(EVENTS.BID_WON, testEvent); + events.emit(EVENTS.BID_WON, getWonRequest()); - const request = server.requests[0]; - const urlParams = new URL(request.url); - const encodedPayload = urlParams.searchParams.get('payload'); - const decodedPayload = JSON.parse(atob(JSON.parse(encodedPayload)[0])); + const request = server.requests[0]; + const urlParams = new URL(request.url); + const encodedPayload = urlParams.searchParams.get("payload"); + const decodedPayload = JSON.parse(atob(JSON.parse(encodedPayload)[0])); - expect(server.requests.length).to.be.above(0); - expect(encodedPayload).to.exist; - expect(decodedPayload).to.have.property('placementId', expectedPlacementId); + expect(server.requests.length).to.be.above(0); + expect(decodedPayload).to.have.property( + "ABTestingConfigurationSource", + ABTestingConfigurationSource + ); + }); + + it("should not include ABTestingConfigurationSource in payload when not provided", function () { + enableAnalyticWithSpecialOptions({}); + + events.emit(EVENTS.BID_WON, getWonRequest()); + + const request = server.requests[0]; + const urlParams = new URL(request.url); + const encodedPayload = urlParams.searchParams.get("payload"); + const decodedPayload = JSON.parse(atob(JSON.parse(encodedPayload)[0])); + + expect(server.requests.length).to.be.above(0); + expect(decodedPayload).to.not.have.property("ABTestingConfigurationSource"); + }); + + it("should use group from provided options when ABTestingConfigurationSource is 'group'", function () { + const providedGroup = WITHOUT_IIQ; + // Ensure actualABGroup is not set so group from options is used + delete window[`iiq_identity_${partner}`].actualABGroup; + + enableAnalyticWithSpecialOptions({ + group: providedGroup, + ABTestingConfigurationSource: AB_CONFIG_SOURCE.GROUP, }); + + events.emit(EVENTS.BID_WON, getWonRequest()); + + const request = server.requests[0]; + const urlParams = new URL(request.url); + const encodedPayload = urlParams.searchParams.get("payload"); + const decodedPayload = JSON.parse(atob(JSON.parse(encodedPayload)[0])); + + expect(server.requests.length).to.be.above(0); + // Verify that the group from options is used in the payload + expect(decodedPayload).to.have.property("abGroup", providedGroup); }); }); diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index 42cff5c2582..4b9afc38146 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -6,13 +6,14 @@ import { intentIqIdSubmodule, handleClientHints, firstPartyData as moduleFPD, - isCMPStringTheSame, createPixelUrl, translateMetadata + isCMPStringTheSame, createPixelUrl, translateMetadata, + initializeGlobalIIQ } from '../../../modules/intentIqIdSystem.js'; import { storage, readData, storeData } from '../../../libraries/intentIqUtils/storageUtils.js'; import { gppDataHandler, uspDataHandler, gdprDataHandler } from '../../../src/consentHandler.js'; import { clearAllCookies } from '../../helpers/cookies.js'; import { detectBrowser, detectBrowserFromUserAgent, detectBrowserFromUserAgentData } from '../../../libraries/intentIqUtils/detectBrowserUtils.js'; -import {CLIENT_HINTS_KEY, FIRST_PARTY_KEY, NOT_YET_DEFINED, PREBID, WITH_IIQ, WITHOUT_IIQ} from '../../../libraries/intentIqConstants/intentIqConstants.js'; +import {CLIENT_HINTS_KEY, FIRST_PARTY_KEY, PREBID, WITH_IIQ, WITHOUT_IIQ} from '../../../libraries/intentIqConstants/intentIqConstants.js'; import { decryptData } from '../../../libraries/intentIqUtils/cryptionUtils.js'; import { isCHSupported } from '../../../libraries/intentIqUtils/chUtils.js'; @@ -81,34 +82,25 @@ function ensureUAData() { } async function waitForClientHints() { - if (!isCHSupported()) return; - const clock = globalThis.__iiqClock; if (clock && typeof clock.runAllAsync === 'function') { await clock.runAllAsync(); - return; - } - if (clock && typeof clock.runAll === 'function') { + } else if (clock && typeof clock.runAll === 'function') { clock.runAll(); await Promise.resolve(); await Promise.resolve(); - return; - } - if (clock && typeof clock.runToLast === 'function') { + } else if (clock && typeof clock.runToLast === 'function') { clock.runToLast(); await Promise.resolve(); - return; - } - if (clock && typeof clock.tick === 'function') { + } else if (clock && typeof clock.tick === 'function') { clock.tick(0); await Promise.resolve(); - return; + } else { + await Promise.resolve(); + await Promise.resolve(); + await new Promise(r => setTimeout(r, 0)); } - - await Promise.resolve(); - await Promise.resolve(); - await new Promise(r => setTimeout(r, 0)); } const testAPILink = 'https://new-test-api.intentiq.com' @@ -132,6 +124,7 @@ const mockGAM = () => { }; describe('IntentIQ tests', function () { + this.timeout(10000); let sandbox; let logErrorStub; let clock; @@ -178,6 +171,23 @@ describe('IntentIQ tests', function () { localStorage.clear(); }); + it('should create global IIQ identity object', async () => { + const globalName = `iiq_identity_${partner}` + const callBackSpy = sinon.spy(); + const submoduleCallback = intentIqIdSubmodule.getId({ params: { partner }}).callback; + submoduleCallback(callBackSpy); + await waitForClientHints() + expect(window[globalName]).to.be.not.undefined + expect(window[globalName].partnerData).to.be.not.undefined + expect(window[globalName].firstPartyData).to.be.not.undefined + }) + + it('should not create a global IIQ identity object in case it was already created', () => { + intentIqIdSubmodule.getId({ params: { partner }}) + const secondTimeCalling = initializeGlobalIIQ(partner) + expect(secondTimeCalling).to.be.false + }) + it('should log an error if no configParams were passed when getId', function () { const submodule = intentIqIdSubmodule.getId({ params: {} }); expect(logErrorStub.calledOnce).to.be.true; @@ -330,10 +340,11 @@ describe('IntentIQ tests', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should set GAM targeting to U initially and update to A after server response', async function () { + it('should set GAM targeting to B initially and update to A after server response', async function () { const callBackSpy = sinon.spy(); const mockGamObject = mockGAM(); const expectedGamParameterName = 'intent_iq_group'; + defaultConfigParams.params.abPercentage = 0; // "B" provided percentage by user const originalPubads = mockGamObject.pubads; const setTargetingSpy = sinon.spy(); @@ -350,33 +361,69 @@ describe('IntentIQ tests', function () { defaultConfigParams.params.gamObjectReference = mockGamObject; const submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; - submoduleCallback(callBackSpy); await waitForClientHints(); const request = server.requests[0]; mockGamObject.cmd.forEach(cb => cb()); - mockGamObject.cmd = [] + mockGamObject.cmd = []; const groupBeforeResponse = mockGamObject.pubads().getTargeting(expectedGamParameterName); - request.respond( - 200, - responseHeader, - JSON.stringify({ group: 'A', tc: 20 }) - ); + request.respond(200, responseHeader, JSON.stringify({ tc: 20 })); - mockGamObject.cmd.forEach(item => item()); + mockGamObject.cmd.forEach(cb => cb()); + mockGamObject.cmd = []; const groupAfterResponse = mockGamObject.pubads().getTargeting(expectedGamParameterName); expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39'); - expect(groupBeforeResponse).to.deep.equal([NOT_YET_DEFINED]); + expect(groupBeforeResponse).to.deep.equal([WITHOUT_IIQ]); expect(groupAfterResponse).to.deep.equal([WITH_IIQ]); - expect(setTargetingSpy.calledTwice).to.be.true; }); + it('should set GAM targeting to B when server tc=41', async () => { + window.localStorage.clear(); + const mockGam = mockGAM(); + defaultConfigParams.params.gamObjectReference = mockGam; + defaultConfigParams.params.abPercentage = 100; + + const cb = intentIqIdSubmodule.getId(defaultConfigParams).callback; + cb(() => {}); + await waitForClientHints(); + + const req = server.requests[0]; + mockGam.cmd.forEach(fn => fn()); + const before = mockGam.pubads().getTargeting('intent_iq_group'); + + req.respond(200, responseHeader, JSON.stringify({ tc: 41 })); + mockGam.cmd.forEach(fn => fn()); + const after = mockGam.pubads().getTargeting('intent_iq_group'); + + expect(before).to.deep.equal([WITH_IIQ]); + expect(after).to.deep.equal([WITHOUT_IIQ]); + }); + + it('should read tc from LS and set relevant GAM group', async () => { + window.localStorage.clear(); + const storageKey = `${FIRST_PARTY_KEY}_${defaultConfigParams.params.partner}`; + localStorage.setItem(storageKey, JSON.stringify({ terminationCause: 41 })); + + const mockGam = mockGAM(); + defaultConfigParams.params.gamObjectReference = mockGam; + defaultConfigParams.params.abPercentage = 100; + + const cb = intentIqIdSubmodule.getId(defaultConfigParams).callback; + cb(() => {}); + await waitForClientHints(); + + mockGam.cmd.forEach(fn => fn()); + const group = mockGam.pubads().getTargeting('intent_iq_group'); + + expect(group).to.deep.equal([WITHOUT_IIQ]); + }); + it('should use the provided gamParameterName from configParams', function () { const callBackSpy = sinon.spy(); const mockGamObject = mockGAM(); @@ -409,7 +456,6 @@ describe('IntentIQ tests', function () { localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({ pcid: 'pcid-1', pcidDate: Date.now(), - group: 'A', isOptedOut: false, date: Date.now(), sCal: Date.now() @@ -698,7 +744,6 @@ describe('IntentIQ tests', function () { const FPD = { pcid: 'c869aa1f-fe40-47cb-810f-4381fec28fc9', pcidDate: 1747720820757, - group: 'A', sCal: Date.now(), gdprString: null, gppString: null, @@ -713,13 +758,13 @@ describe('IntentIQ tests', function () { const request = server.requests[0]; expect(request.url).contain("ProfilesEngineServlet?at=39") // server was called }) + it("Should NOT call the server if FPD has been updated user Opted Out, and 24 hours have not yet passed.", async () => { const allowedStorage = ['html5'] const newPartnerId = 12345 const FPD = { pcid: 'c869aa1f-fe40-47cb-810f-4381fec28fc9', pcidDate: 1747720820757, - group: 'A', isOptedOut: true, sCal: Date.now(), gdprString: null, @@ -1547,4 +1592,35 @@ describe('IntentIQ tests', function () { expect(callBackSpy.calledOnce).to.be.true; expect(groupChangedSpy.calledWith(WITH_IIQ)).to.be.true; }); + + it('should use group provided by partner', async function () { + const groupChangedSpy = sinon.spy(); + const callBackSpy = sinon.spy(); + const usedGroup = 'B' + const ABTestingConfigurationSource = 'group' + const configParams = { + params: { + ...defaultConfigParams.params, + ABTestingConfigurationSource, + group: usedGroup, + groupChanged: groupChangedSpy + } + }; + + const submoduleCallback = intentIqIdSubmodule.getId(configParams).callback; + submoduleCallback(callBackSpy); + await waitForClientHints() + const request = server.requests[0]; + request.respond( + 200, + responseHeader, + JSON.stringify({ pid: 'test_pid', data: 'test_personid', ls: true }) + ); + + expect(request.url).to.contain(`abtg=${usedGroup}`); + expect(request.url).to.contain(`ABTestingConfigurationSource=${ABTestingConfigurationSource}`); + expect(request.url).to.contain(`testGroup=${usedGroup}`); + expect(callBackSpy.calledOnce).to.be.true; + expect(groupChangedSpy.calledWith(usedGroup)).to.be.true; + }); }); diff --git a/test/spec/modules/koblerBidAdapter_spec.js b/test/spec/modules/koblerBidAdapter_spec.js index 187ccf9459f..4e70ac6f9f3 100644 --- a/test/spec/modules/koblerBidAdapter_spec.js +++ b/test/spec/modules/koblerBidAdapter_spec.js @@ -1,5 +1,5 @@ import {expect} from 'chai'; -import {pageViewId, spec} from 'modules/koblerBidAdapter.js'; +import {spec} from 'modules/koblerBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; @@ -7,7 +7,7 @@ import {getRefererInfo} from 'src/refererDetection.js'; import { setConfig as setCurrencyConfig } from '../../../modules/currency.js'; import { addFPDToBidderRequest } from '../../helpers/fpd.js'; -function createBidderRequest(auctionId, timeout, pageUrl, gdprVendorData = {}) { +function createBidderRequest(auctionId, timeout, pageUrl, gdprVendorData = {}, pageViewId) { const gdprConsent = { consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', apiVersion: 2, @@ -21,7 +21,8 @@ function createBidderRequest(auctionId, timeout, pageUrl, gdprVendorData = {}) { refererInfo: { page: pageUrl || 'example.com' }, - gdprConsent: gdprConsent + gdprConsent: gdprConsent, + pageViewId }; } @@ -259,15 +260,17 @@ describe('KoblerAdapter', function () { const testUrl = 'kobler.no'; const auctionId1 = '8319af54-9795-4642-ba3a-6f57d6ff9100'; const auctionId2 = 'e19f2d0c-602d-4969-96a1-69a22d483f47'; + const pageViewId1 = '2949ce3c-2c4d-4b96-9ce0-8bf5aa0bb416'; + const pageViewId2 = '6c449b7d-c9b0-461d-8cc7-ce0a8da58349'; const timeout = 5000; const validBidRequests = [createValidBidRequest()]; - const bidderRequest1 = createBidderRequest(auctionId1, timeout, testUrl); - const bidderRequest2 = createBidderRequest(auctionId2, timeout, testUrl); + const bidderRequest1 = createBidderRequest(auctionId1, timeout, testUrl, {}, pageViewId1); + const bidderRequest2 = createBidderRequest(auctionId2, timeout, testUrl, {}, pageViewId2); const openRtbRequest1 = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest1).data); - expect(openRtbRequest1.ext.kobler.page_view_id).to.be.equal(pageViewId); + expect(openRtbRequest1.ext.kobler.page_view_id).to.be.equal(pageViewId1); const openRtbRequest2 = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest2).data); - expect(openRtbRequest2.ext.kobler.page_view_id).to.be.equal(pageViewId); + expect(openRtbRequest2.ext.kobler.page_view_id).to.be.equal(pageViewId2); }); it('should read data from valid bid requests', function () { @@ -440,6 +443,7 @@ describe('KoblerAdapter', function () { }); it('should create whole OpenRTB request', function () { + const pageViewId = 'aa9f0b20-a642-4d0e-acb5-e35805253ef7'; const validBidRequests = [ createValidBidRequest( { @@ -483,7 +487,8 @@ describe('KoblerAdapter', function () { } } } - } + }, + pageViewId ); const result = spec.buildRequests(validBidRequests, bidderRequest); diff --git a/test/spec/modules/limelightDigitalBidAdapter_spec.js b/test/spec/modules/limelightDigitalBidAdapter_spec.js index a4b161b7026..31b8530c7eb 100644 --- a/test/spec/modules/limelightDigitalBidAdapter_spec.js +++ b/test/spec/modules/limelightDigitalBidAdapter_spec.js @@ -736,6 +736,101 @@ describe('limelightDigitalAdapter', function () { ]); }); }); + describe('getFloor support', function() { + const bidderRequest = { + ortb2: { + device: { + sua: { + browsers: [], + platform: [], + mobile: 1, + architecture: 'arm' + } + } + }, + refererInfo: { + page: 'testPage' + } + }; + it('should include floorInfo when getFloor is available', function() { + const bidWithFloor = { + ...bid1, + getFloor: function(params) { + if (params.size[0] === 300 && params.size[1] === 250) { + return { currency: 'USD', floor: 2.0 }; + } + return { currency: 'USD', floor: 0 }; + } + }; + + const serverRequests = spec.buildRequests([bidWithFloor], bidderRequest); + expect(serverRequests).to.have.lengthOf(1); + const adUnit = serverRequests[0].data.adUnits[0]; + expect(adUnit.sizes).to.have.lengthOf(1); + expect(adUnit.sizes[0].floorInfo).to.exist; + expect(adUnit.sizes[0].floorInfo.currency).to.equal('USD'); + expect(adUnit.sizes[0].floorInfo.floor).to.equal(2.0); + }); + it('should set floorInfo to null when getFloor is not available', function() { + const bidWithoutFloor = { ...bid1 }; + delete bidWithoutFloor.getFloor; + + const serverRequests = spec.buildRequests([bidWithoutFloor], bidderRequest); + expect(serverRequests).to.have.lengthOf(1); + expect(serverRequests[0].data.adUnits[0].sizes[0].floorInfo).to.be.null; + }); + it('should handle multiple sizes with different floors', function() { + const bidWithMultipleSizes = { + ...bid1, + mediaTypes: { + banner: { + sizes: [[300, 250], [728, 90]] + } + }, + getFloor: function(params) { + if (params.size[0] === 300 && params.size[1] === 250) { + return { currency: 'USD', floor: 1.5 }; + } + if (params.size[0] === 728 && params.size[1] === 90) { + return { currency: 'USD', floor: 2.0 }; + } + return { currency: 'USD', floor: 0 }; + } + }; + + const serverRequests = spec.buildRequests([bidWithMultipleSizes], bidderRequest); + expect(serverRequests).to.have.lengthOf(1); + const adUnit = serverRequests[0].data.adUnits[0]; + expect(adUnit.sizes).to.have.lengthOf(2); + expect(adUnit.sizes[0].floorInfo.floor).to.equal(1.5); + expect(adUnit.sizes[1].floorInfo.floor).to.equal(2.0); + }); + it('should set floorInfo to null when getFloor returns empty object', function() { + const bidWithEmptyFloor = { + ...bid1, + getFloor: function() { + return {}; + } + }; + + const serverRequests = spec.buildRequests([bidWithEmptyFloor], bidderRequest); + expect(serverRequests).to.have.lengthOf(1); + expect(serverRequests[0].data.adUnits[0].sizes[0].floorInfo).to.deep.equal({}); + }); + it('should handle getFloor errors and set floorInfo to null', function() { + const bidWithErrorFloor = { + ...bid1, + getFloor: function() { + throw new Error('Floor module error'); + } + }; + + const serverRequests = spec.buildRequests([bidWithErrorFloor], bidderRequest); + expect(serverRequests).to.have.lengthOf(1); + const adUnit = serverRequests[0].data.adUnits[0]; + expect(adUnit.sizes[0].floorInfo).to.be.null; + }); + }); }); function validateAdUnit(adUnit, bid) { @@ -758,7 +853,8 @@ function validateAdUnit(adUnit, bid) { expect(adUnit.sizes).to.deep.equal(bidSizes.map(size => { return { width: size[0], - height: size[1] + height: size[1], + floorInfo: null } })); expect(adUnit.publisherId).to.equal(bid.params.publisherId); diff --git a/test/spec/modules/liveIntentAnalyticsAdapter_spec.js b/test/spec/modules/liveIntentAnalyticsAdapter_spec.js index 51ada80b825..869e9eb789c 100644 --- a/test/spec/modules/liveIntentAnalyticsAdapter_spec.js +++ b/test/spec/modules/liveIntentAnalyticsAdapter_spec.js @@ -77,11 +77,11 @@ describe('LiveIntent Analytics Adapter ', () => { events.emit(EVENTS.AUCTION_INIT, AUCTION_INIT_EVENT); expect(server.requests.length).to.equal(1); - expect(server.requests[0].url).to.equal('https://wba.liadm.com/analytic-events/auction-init?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&liip=y&aun=2') + expect(server.requests[0].url).to.equal('https://wba.liadm.com/analytic-events/auction-init?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&liip=y&aun=2&asz=300x250%2C728x90%2C300x250'); events.emit(EVENTS.BID_WON, BID_WON_EVENT); expect(server.requests.length).to.equal(2); - expect(server.requests[1].url).to.equal('https://wba.liadm.com/analytic-events/bid-won?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&auc=test-div2&auid=afc6bc6a-3082-4940-b37f-d22e1b026e48&cpm=1.5&c=USD&b=appnexus&bc=appnexus&pid=a123&iid=pbjs&sts=1739971147744&rts=1739971147806&liip=y'); + expect(server.requests[1].url).to.equal('https://wba.liadm.com/analytic-events/bid-won?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&auc=test-div2&auid=afc6bc6a-3082-4940-b37f-d22e1b026e48&cpm=1.5&c=USD&b=appnexus&bc=appnexus&pid=a123&iid=pbjs&sts=1739971147744&rts=1739971147806&liip=y&asz=728x90'); }); it('request is computed and sent correctly when sampling is 1 and liModule is enabled', () => { @@ -90,11 +90,11 @@ describe('LiveIntent Analytics Adapter ', () => { events.emit(EVENTS.AUCTION_INIT, AUCTION_INIT_EVENT); expect(server.requests.length).to.equal(1); - expect(server.requests[0].url).to.equal('https://wba.liadm.com/analytic-events/auction-init?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&me=y&liip=y&aun=2') + expect(server.requests[0].url).to.equal('https://wba.liadm.com/analytic-events/auction-init?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&me=y&liip=y&aun=2&asz=300x250%2C728x90%2C300x250') events.emit(EVENTS.BID_WON, BID_WON_EVENT); expect(server.requests.length).to.equal(2); - expect(server.requests[1].url).to.equal('https://wba.liadm.com/analytic-events/bid-won?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&auc=test-div2&auid=afc6bc6a-3082-4940-b37f-d22e1b026e48&cpm=1.5&c=USD&b=appnexus&bc=appnexus&pid=a123&iid=pbjs&sts=1739971147744&rts=1739971147806&me=y&liip=y'); + expect(server.requests[1].url).to.equal('https://wba.liadm.com/analytic-events/bid-won?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&auc=test-div2&auid=afc6bc6a-3082-4940-b37f-d22e1b026e48&cpm=1.5&c=USD&b=appnexus&bc=appnexus&pid=a123&iid=pbjs&sts=1739971147744&rts=1739971147806&me=y&liip=y&asz=728x90'); }); it('request is computed and sent correctly when sampling is 1 and liModule is disabled', () => { @@ -103,11 +103,11 @@ describe('LiveIntent Analytics Adapter ', () => { events.emit(EVENTS.AUCTION_INIT, AUCTION_INIT_EVENT); expect(server.requests.length).to.equal(1); - expect(server.requests[0].url).to.equal('https://wba.liadm.com/analytic-events/auction-init?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&me=n&liip=y&aun=2') + expect(server.requests[0].url).to.equal('https://wba.liadm.com/analytic-events/auction-init?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&me=n&liip=y&aun=2&asz=300x250%2C728x90%2C300x250') events.emit(EVENTS.BID_WON, BID_WON_EVENT); expect(server.requests.length).to.equal(2); - expect(server.requests[1].url).to.equal('https://wba.liadm.com/analytic-events/bid-won?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&auc=test-div2&auid=afc6bc6a-3082-4940-b37f-d22e1b026e48&cpm=1.5&c=USD&b=appnexus&bc=appnexus&pid=a123&iid=pbjs&sts=1739971147744&rts=1739971147806&me=n&liip=y'); + expect(server.requests[1].url).to.equal('https://wba.liadm.com/analytic-events/bid-won?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&auc=test-div2&auid=afc6bc6a-3082-4940-b37f-d22e1b026e48&cpm=1.5&c=USD&b=appnexus&bc=appnexus&pid=a123&iid=pbjs&sts=1739971147744&rts=1739971147806&me=n&liip=y&asz=728x90'); }); it('request is computed and sent correctly when sampling is 1 and should forward the correct liTreatmentRate', () => { @@ -116,11 +116,11 @@ describe('LiveIntent Analytics Adapter ', () => { events.emit(EVENTS.AUCTION_INIT, AUCTION_INIT_EVENT); expect(server.requests.length).to.equal(1); - expect(server.requests[0].url).to.equal('https://wba.liadm.com/analytic-events/auction-init?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&tr=0.95&liip=y&aun=2') + expect(server.requests[0].url).to.equal('https://wba.liadm.com/analytic-events/auction-init?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&tr=0.95&liip=y&aun=2&asz=300x250%2C728x90%2C300x250') events.emit(EVENTS.BID_WON, BID_WON_EVENT); expect(server.requests.length).to.equal(2); - expect(server.requests[1].url).to.equal('https://wba.liadm.com/analytic-events/bid-won?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&auc=test-div2&auid=afc6bc6a-3082-4940-b37f-d22e1b026e48&cpm=1.5&c=USD&b=appnexus&bc=appnexus&pid=a123&iid=pbjs&sts=1739971147744&rts=1739971147806&tr=0.95&liip=y'); + expect(server.requests[1].url).to.equal('https://wba.liadm.com/analytic-events/bid-won?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&auc=test-div2&auid=afc6bc6a-3082-4940-b37f-d22e1b026e48&cpm=1.5&c=USD&b=appnexus&bc=appnexus&pid=a123&iid=pbjs&sts=1739971147744&rts=1739971147806&tr=0.95&liip=y&asz=728x90'); }); it('not send any events on auction init if disabled in settings', () => { @@ -137,7 +137,7 @@ describe('LiveIntent Analytics Adapter ', () => { events.emit(EVENTS.BID_WON, BID_WON_EVENT_UNDEFINED); expect(server.requests.length).to.equal(2); - expect(server.requests[1].url).to.equal('https://wba.liadm.com/analytic-events/bid-won?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&liip=y'); + expect(server.requests[1].url).to.equal('https://wba.liadm.com/analytic-events/bid-won?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&liip=y&asz=728x90'); }); it('liip should be n if there is no source or provider in userIdAsEids have the value liveintent.com', () => { @@ -145,7 +145,7 @@ describe('LiveIntent Analytics Adapter ', () => { events.emit(EVENTS.AUCTION_INIT, AUCTION_INIT_EVENT_NOT_LI); expect(server.requests.length).to.equal(1); - expect(server.requests[0].url).to.equal('https://wba.liadm.com/analytic-events/auction-init?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&liip=n&aun=2'); + expect(server.requests[0].url).to.equal('https://wba.liadm.com/analytic-events/auction-init?id=77abbc81-c1f1-41cd-8f25-f7149244c800&aid=87b4a93d-19ae-432a-96f0-8c2d4cc1c539&u=https%3A%2F%2Fwww.test.com&ats=1739969798557&pid=a123&iid=pbjs&liip=n&aun=2&asz=300x250%2C728x90'); }); it('no request is computed when sampling is 0', () => { diff --git a/test/spec/modules/liveIntentExternalIdSystem_spec.js b/test/spec/modules/liveIntentExternalIdSystem_spec.js index c4fbe85bd79..c4bd7eec960 100644 --- a/test/spec/modules/liveIntentExternalIdSystem_spec.js +++ b/test/spec/modules/liveIntentExternalIdSystem_spec.js @@ -435,11 +435,6 @@ describe('LiveIntentExternalId', function() { expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'vidazoo': 'bar'}, 'vidazoo': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); - it('should decode a nexxen id to a sTeparate object when present', function() { - const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', nexxen: 'bar' }, defaultConfigParams); - expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'nexxen': 'bar'}, 'nexxen': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); - }); - it('should decode the segments as part of lipb', function() { const result = liveIntentExternalIdSubmodule.decode({ nonId: 'foo', 'segments': ['bar'] }, defaultConfigParams); expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'segments': ['bar']}}); diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index eec6a515d72..ecf7dc9a634 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -527,11 +527,6 @@ describe('LiveIntentId', function() { expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'vidazoo': 'bar'}, 'vidazoo': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); - it('should decode a nexxen id to a separate object when present', function() { - const result = liveIntentIdSubmodule.decode({ nonId: 'foo', nexxen: 'bar' }, defaultConfigParams); - expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'nexxen': 'bar'}, 'nexxen': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); - }); - it('getId does not set the global variables when liModuleEnabled, liTreatmentRate and activatePartialTreatment are undefined', function() { window.liModuleEnabled = undefined; window.liTreatmentRate = undefined; @@ -1183,39 +1178,6 @@ describe('LiveIntentId', function() { }); }); - it('nexxen', function () { - const userId = { - nexxen: { 'id': 'sample_id' } - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'liveintent.unrulymedia.com', - uids: [{ - id: 'sample_id', - atype: 3 - }] - }); - }); - - it('nexxen with ext', function () { - const userId = { - nexxen: { 'id': 'sample_id', 'ext': { 'provider': 'some.provider.com' } } - }; - const newEids = createEidsArray(userId); - expect(newEids.length).to.equal(1); - expect(newEids[0]).to.deep.equal({ - source: 'liveintent.unrulymedia.com', - uids: [{ - id: 'sample_id', - atype: 3, - ext: { - provider: 'some.provider.com' - } - }] - }); - }); - it('tdid sets matcher for liveintent', function() { const userId = { tdid: 'some-tdid' diff --git a/test/spec/modules/mediagoBidAdapter_spec.js b/test/spec/modules/mediagoBidAdapter_spec.js index 6a1e588e886..8c4c6bc0f3a 100644 --- a/test/spec/modules/mediagoBidAdapter_spec.js +++ b/test/spec/modules/mediagoBidAdapter_spec.js @@ -200,7 +200,7 @@ describe('mediago:BidAdapterTests', function () { bid: [ { id: '6e28cfaf115a354ea1ad8e1304d6d7b8', - impid: '1', + impid: '54d73f19c9d47a', price: 0.087581, adm: adm, cid: '1339145', diff --git a/test/spec/modules/mycodemediaBidAdapter_spec.js b/test/spec/modules/mycodemediaBidAdapter_spec.js new file mode 100644 index 00000000000..7cc9b412ea0 --- /dev/null +++ b/test/spec/modules/mycodemediaBidAdapter_spec.js @@ -0,0 +1,513 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/mycodemediaBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'mycodemedia'; + +describe('MyCodeMediaBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + }, + userIdAsEids + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + }, + userIdAsEids + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative' + }, + userIdAsEids + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, + refererInfo: { + referer: 'https://test.com', + page: 'https://test.com' + }, + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK', + } + }, + timeout: 500 + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns general data valid', function () { + const data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys( + 'deviceWidth', + 'deviceHeight', + 'device', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('object'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + expect(placement.eids).to.exist.and.to.be.deep.equal(userIdAsEids); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns valid endpoints', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + endpointId: 'testBanner', + }, + userIdAsEids + } + ]; + + const serverRequest = spec.buildRequests(bids, bidderRequest); + + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.endpointId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('network'); + expect(placement.eids).to.exist.and.to.be.deep.equal(userIdAsEids); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + const data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('object'); + expect(data.gdpr).to.have.property('consentString'); + expect(data.gdpr).to.not.have.property('vendorData'); + expect(data.gdpr.consentString).to.equal(bidderRequest.gdprConsent.consentString); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + const data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + }); + + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + const serverRequest = spec.buildRequests(bids, bidderRequest); + const data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + delete bidderRequest.gppConsent; + }) + + it('bidderRequest.ortb2.regs.gpp', () => { + bidderRequest.ortb2 = bidderRequest.ortb2 || {}; + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + const serverRequest = spec.buildRequests(bids, bidderRequest); + const data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + }) + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + const bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + const dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + const videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + const dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + const nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + const dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + const serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + const serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + const serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + const serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://usersync.mycodemedia.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://usersync.mycodemedia.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + it('Should return array of objects with proper sync config , include GPP', function() { + const syncData = spec.getUserSyncs({}, {}, {}, {}, { + gppString: 'abc123', + applicableSections: [8] + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://usersync.mycodemedia.com/image?pbjs=1&gpp=abc123&gpp_sid=8&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/neuwoRtdProvider_spec.js b/test/spec/modules/neuwoRtdProvider_spec.js index 1f75b1441e8..4b2951afe33 100644 --- a/test/spec/modules/neuwoRtdProvider_spec.js +++ b/test/spec/modules/neuwoRtdProvider_spec.js @@ -82,6 +82,11 @@ function bidsConfiglike() { } describe("neuwoRtdModule", function () { + beforeEach(function () { + // Clear the global cache before each test to ensure test isolation + neuwo.clearCache(); + }); + describe("init", function () { it("should return true when all required parameters are provided", function () { expect( @@ -513,7 +518,440 @@ describe("neuwoRtdModule", function () { }); }); - // NEW TESTS START HERE + describe("cleanUrl", function () { + describe("when no stripping options are provided", function () { + it("should return the URL unchanged", function () { + const url = "https://example.com/page?foo=bar&baz=qux"; + const result = neuwo.cleanUrl(url, {}); + expect(result, "should return the original URL with all query params intact").to.equal(url); + }); + + it("should return the URL unchanged when options object is empty", function () { + const url = "https://example.com/page?foo=bar"; + const result = neuwo.cleanUrl(url); + expect(result, "should handle missing options parameter").to.equal(url); + }); + }); + + describe("with query parameters edge cases", function () { + it("should strip all query parameters from the URL for `stripAllQueryParams` (edge cases)", function () { + const stripAll = (url) => neuwo.cleanUrl(url, { stripAllQueryParams: true }); + const expected = "https://example.com/page"; + const expectedWithFragment = "https://example.com/page#anchor"; + + // Basic formats + expect(stripAll("https://example.com/page?key=value"), "should remove basic key=value params").to.equal(expected); + expect(stripAll("https://example.com/page?key="), "should remove params with empty value").to.equal(expected); + expect(stripAll("https://example.com/page?key"), "should remove params without equals sign").to.equal(expected); + expect(stripAll("https://example.com/page?=value"), "should remove params with empty key").to.equal(expected); + + // Multiple parameters + expect(stripAll("https://example.com/page?key1=value1&key2=value2"), "should remove multiple different params").to.equal(expected); + expect(stripAll("https://example.com/page?key=value1&key=value2"), "should remove multiple params with same key").to.equal(expected); + + // Special characters and encoding + expect(stripAll("https://example.com/page?key=value%20with%20spaces"), "should remove URL encoded spaces").to.equal(expected); + expect(stripAll("https://example.com/page?key=value+with+plus"), "should remove plus as space").to.equal(expected); + expect(stripAll("https://example.com/page?key=value%3D%26%3F"), "should remove encoded special chars").to.equal(expected); + expect(stripAll("https://example.com/page?key=%"), "should remove incomplete encoding").to.equal(expected); + expect(stripAll("https://example.com/page?key=value%2"), "should remove malformed encoding").to.equal(expected); + + // Delimiters and syntax edge cases + expect(stripAll("https://example.com/page?&key=value"), "should remove params with leading ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value&"), "should remove params with trailing ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value&&key2=value2"), "should remove params with double ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value?key2=value2"), "should remove params with question mark delimiter").to.equal(expected); + expect(stripAll("https://example.com/page?key=value;key2=value2"), "should remove params with semicolon delimiter").to.equal(expected); + + // Empty and missing cases + expect(stripAll("https://example.com/page?"), "should remove question mark alone").to.equal(expected); + expect(stripAll("https://example.com/page??"), "should remove double question mark").to.equal(expected); + expect(stripAll("https://example.com/page"), "should handle URL without query string").to.equal(expected); + + // Unicode and special values + expect(stripAll("https://example.com/page?key=值"), "should remove unicode characters").to.equal(expected); + expect(stripAll("https://example.com/page?key=null"), "should remove string 'null'").to.equal(expected); + expect(stripAll("https://example.com/page?key=undefined"), "should remove string 'undefined'").to.equal(expected); + + // Fragment positioning (fragments are preserved by default) + expect(stripAll("https://example.com/page?key=value#anchor"), "should remove query params and preserve fragment").to.equal(expectedWithFragment); + expect(stripAll("https://example.com/page#anchor?key=value"), "should preserve fragment before params").to.equal("https://example.com/page#anchor?key=value"); + }); + + it("should strip all query parameters from the URL for `stripQueryParamsForDomains` (edge cases)", function () { + const stripAll = (url) => neuwo.cleanUrl(url, { stripQueryParamsForDomains: ["example.com"] }); + const expected = "https://example.com/page"; + const expectedWithFragment = "https://example.com/page#anchor"; + + // Basic formats + expect(stripAll("https://example.com/page?key=value"), "should remove basic key=value params").to.equal(expected); + expect(stripAll("https://example.com/page?key="), "should remove params with empty value").to.equal(expected); + expect(stripAll("https://example.com/page?key"), "should remove params without equals sign").to.equal(expected); + expect(stripAll("https://example.com/page?=value"), "should remove params with empty key").to.equal(expected); + + // Multiple parameters + expect(stripAll("https://example.com/page?key1=value1&key2=value2"), "should remove multiple different params").to.equal(expected); + expect(stripAll("https://example.com/page?key=value1&key=value2"), "should remove multiple params with same key").to.equal(expected); + + // Special characters and encoding + expect(stripAll("https://example.com/page?key=value%20with%20spaces"), "should remove URL encoded spaces").to.equal(expected); + expect(stripAll("https://example.com/page?key=value+with+plus"), "should remove plus as space").to.equal(expected); + expect(stripAll("https://example.com/page?key=value%3D%26%3F"), "should remove encoded special chars").to.equal(expected); + expect(stripAll("https://example.com/page?key=%"), "should remove incomplete encoding").to.equal(expected); + expect(stripAll("https://example.com/page?key=value%2"), "should remove malformed encoding").to.equal(expected); + + // Delimiters and syntax edge cases + expect(stripAll("https://example.com/page?&key=value"), "should remove params with leading ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value&"), "should remove params with trailing ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value&&key2=value2"), "should remove params with double ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value?key2=value2"), "should remove params with question mark delimiter").to.equal(expected); + expect(stripAll("https://example.com/page?key=value;key2=value2"), "should remove params with semicolon delimiter").to.equal(expected); + + // Empty and missing cases + expect(stripAll("https://example.com/page?"), "should remove question mark alone").to.equal(expected); + expect(stripAll("https://example.com/page??"), "should remove double question mark").to.equal(expected); + expect(stripAll("https://example.com/page"), "should handle URL without query string").to.equal(expected); + + // Unicode and special values + expect(stripAll("https://example.com/page?key=值"), "should remove unicode characters").to.equal(expected); + expect(stripAll("https://example.com/page?key=null"), "should remove string 'null'").to.equal(expected); + expect(stripAll("https://example.com/page?key=undefined"), "should remove string 'undefined'").to.equal(expected); + + // Fragment positioning (fragments are preserved by default) + expect(stripAll("https://example.com/page?key=value#anchor"), "should remove query params and preserve fragment").to.equal(expectedWithFragment); + expect(stripAll("https://example.com/page#anchor?key=value"), "should preserve fragment before params").to.equal("https://example.com/page#anchor?key=value"); + }); + + it("should strip all query parameters from the URL for `stripQueryParams` (edge cases)", function () { + const stripAll = (url) => neuwo.cleanUrl(url, { stripQueryParams: ["key", "key1", "key2", "", "?"] }); + const expected = "https://example.com/page"; + const expectedWithFragment = "https://example.com/page#anchor"; + + // Basic formats + expect(stripAll("https://example.com/page?key=value"), "should remove basic key=value params").to.equal(expected); + expect(stripAll("https://example.com/page?key="), "should remove params with empty value").to.equal(expected); + expect(stripAll("https://example.com/page?key"), "should remove params without equals sign").to.equal(expected); + expect(stripAll("https://example.com/page?=value"), "should remove params with empty key").to.equal(expected); + + // Multiple parameters + expect(stripAll("https://example.com/page?key1=value1&key2=value2"), "should remove multiple different params").to.equal(expected); + expect(stripAll("https://example.com/page?key=value1&key=value2"), "should remove multiple params with same key").to.equal(expected); + + // Special characters and encoding + expect(stripAll("https://example.com/page?key=value%20with%20spaces"), "should remove URL encoded spaces").to.equal(expected); + expect(stripAll("https://example.com/page?key=value+with+plus"), "should remove plus as space").to.equal(expected); + expect(stripAll("https://example.com/page?key=value%3D%26%3F"), "should remove encoded special chars").to.equal(expected); + expect(stripAll("https://example.com/page?key=%"), "should remove incomplete encoding").to.equal(expected); + expect(stripAll("https://example.com/page?key=value%2"), "should remove malformed encoding").to.equal(expected); + + // Delimiters and syntax edge cases + expect(stripAll("https://example.com/page?&key=value"), "should remove params with leading ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value&"), "should remove params with trailing ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value&&key2=value2"), "should remove params with double ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value?key2=value2"), "should remove params with question mark delimiter").to.equal(expected); + expect(stripAll("https://example.com/page?key=value;key2=value2"), "should remove params with semicolon delimiter").to.equal(expected); + + // Empty and missing cases + expect(stripAll("https://example.com/page?"), "should remove question mark alone").to.equal(expected); + expect(stripAll("https://example.com/page"), "should handle URL without query string").to.equal(expected); + + // Unicode and special values + expect(stripAll("https://example.com/page?key=值"), "should remove unicode characters").to.equal(expected); + expect(stripAll("https://example.com/page?key=null"), "should remove string 'null'").to.equal(expected); + expect(stripAll("https://example.com/page?key=undefined"), "should remove string 'undefined'").to.equal(expected); + + // Fragment positioning (fragments are preserved by default) + expect(stripAll("https://example.com/page?key=value#anchor"), "should remove query params and preserve fragment").to.equal(expectedWithFragment); + expect(stripAll("https://example.com/page#anchor?key=value"), "should preserve fragment before params").to.equal("https://example.com/page#anchor?key=value"); + }); + }); + + describe("when stripAllQueryParams is true", function () { + it("should strip all query parameters from the URL", function () { + const url = "https://example.com/page?foo=bar&baz=qux&test=123"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { stripAllQueryParams: true }); + expect(result, "should remove all query parameters").to.equal(expected); + }); + + it("should return the URL unchanged if there are no query parameters", function () { + const url = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { stripAllQueryParams: true }); + expect(result, "should handle URLs without query params").to.equal(url); + }); + + it("should preserve the hash fragment when stripping query params without stripFragments", function () { + const url = "https://example.com/page?foo=bar#section"; + const expected = "https://example.com/page#section"; + const result = neuwo.cleanUrl(url, { stripAllQueryParams: true }); + expect(result, "should preserve hash fragments by default").to.equal(expected); + }); + + it("should strip hash fragment when stripFragments is enabled", function () { + const url = "https://example.com/page?foo=bar#section"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { stripAllQueryParams: true, stripFragments: true }); + expect(result, "should strip both query params and fragments").to.equal(expected); + }); + + it("should strip query params but preserve path and protocol", function () { + const url = "https://subdomain.example.com:8080/path/to/page?param=value"; + const expected = "https://subdomain.example.com:8080/path/to/page"; + const result = neuwo.cleanUrl(url, { stripAllQueryParams: true }); + expect(result, "should preserve protocol, domain, port, and path").to.equal(expected); + }); + }); + + describe("when stripQueryParamsForDomains is provided", function () { + it("should strip all query params for exact domain match", function () { + const url = "https://example.com/page?foo=bar&baz=qux"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["example.com"] + }); + expect(result, "should strip params for exact domain match").to.equal(expected); + }); + + it("should strip all query params for subdomain match", function () { + const url = "https://sub.example.com/page?foo=bar"; + const expected = "https://sub.example.com/page"; + const result = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["example.com"] + }); + expect(result, "should strip params for subdomains").to.equal(expected); + }); + + it("should not strip query params if domain does not match", function () { + const url = "https://other.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["example.com"] + }); + expect(result, "should preserve params for non-matching domains").to.equal(url); + }); + + it("should not strip query params if subdomain is provided for domain", function () { + const url = "https://example.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["sub.example.com"] + }); + expect(result, "should preserve params for domain when subdomain is provided").to.equal(url); + }); + + it("should handle multiple domains in the list", function () { + const url1 = "https://example.com/page?foo=bar"; + const url2 = "https://test.com/page?foo=bar"; + const url3 = "https://other.com/page?foo=bar"; + const domains = ["example.com", "test.com"]; + + const result1 = neuwo.cleanUrl(url1, { stripQueryParamsForDomains: domains }); + const result2 = neuwo.cleanUrl(url2, { stripQueryParamsForDomains: domains }); + const result3 = neuwo.cleanUrl(url3, { stripQueryParamsForDomains: domains }); + + expect(result1, "should strip params for first domain").to.equal("https://example.com/page"); + expect(result2, "should strip params for second domain").to.equal("https://test.com/page"); + expect(result3, "should preserve params for non-listed domain").to.equal(url3); + }); + + it("should handle deep subdomains correctly", function () { + const url = "https://deep.sub.example.com/page?foo=bar"; + const expected = "https://deep.sub.example.com/page"; + const result1 = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["example.com"] + }); + const result2 = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["sub.example.com"] + }); + expect(result1, "should strip params for deep subdomains with domain matching").to.equal(expected); + expect(result2, "should strip params for deep subdomains with subdomain matching").to.equal(expected); + }); + + it("should not match partial domain names", function () { + const url = "https://notexample.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["example.com"] + }); + expect(result, "should not match partial domain strings").to.equal(url); + }); + + it("should handle empty domain list", function () { + const url = "https://example.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { stripQueryParamsForDomains: [] }); + expect(result, "should not strip params with empty domain list").to.equal(url); + }); + }); + + describe("when stripQueryParams is provided", function () { + it("should strip only specified query parameters", function () { + const url = "https://example.com/page?foo=bar&baz=qux&keep=this"; + const expected = "https://example.com/page?keep=this"; + const result = neuwo.cleanUrl(url, { + stripQueryParams: ["foo", "baz"] + }); + expect(result, "should remove only specified params").to.equal(expected); + }); + + it("should handle single parameter stripping", function () { + const url = "https://example.com/page?remove=this&keep=that"; + const expected = "https://example.com/page?keep=that"; + const result = neuwo.cleanUrl(url, { + stripQueryParams: ["remove"] + }); + expect(result, "should remove single specified param").to.equal(expected); + }); + + it("should return URL without query string if all params are stripped", function () { + const url = "https://example.com/page?foo=bar&baz=qux"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { + stripQueryParams: ["foo", "baz"] + }); + expect(result, "should remove query string when all params stripped").to.equal(expected); + }); + + it("should handle case where specified params do not exist", function () { + const url = "https://example.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { + stripQueryParams: ["nonexistent", "alsonothere"] + }); + expect(result, "should handle non-existent params gracefully").to.equal(url); + }); + + it("should handle empty param list", function () { + const url = "https://example.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { stripQueryParams: [] }); + expect(result, "should not strip params with empty list").to.equal(url); + }); + + it("should preserve param order for remaining params", function () { + const url = "https://example.com/page?a=1&b=2&c=3&d=4"; + const result = neuwo.cleanUrl(url, { + stripQueryParams: ["b", "d"] + }); + expect(result, "should preserve order of remaining params").to.include("a=1"); + expect(result, "should preserve order of remaining params").to.include("c=3"); + expect(result, "should not include stripped param b").to.not.include("b=2"); + expect(result, "should not include stripped param d").to.not.include("d=4"); + }); + }); + + describe("error handling", function () { + it("should return null or undefined input unchanged", function () { + expect(neuwo.cleanUrl(null, {}), "should handle null input").to.equal(null); + expect(neuwo.cleanUrl(undefined, {}), "should handle undefined input").to.equal(undefined); + expect(neuwo.cleanUrl("", {}), "should handle empty string").to.equal(""); + }); + + it("should return invalid URL unchanged and log error", function () { + const invalidUrl = "not-a-valid-url"; + const result = neuwo.cleanUrl(invalidUrl, { stripAllQueryParams: true }); + expect(result, "should return invalid URL unchanged").to.equal(invalidUrl); + }); + + it("should handle malformed URLs gracefully", function () { + const malformedUrl = "http://"; + const result = neuwo.cleanUrl(malformedUrl, { stripAllQueryParams: true }); + expect(result, "should return malformed URL unchanged").to.equal(malformedUrl); + }); + }); + + describe("when stripFragments is enabled", function () { + it("should strip URL fragments from URLs without query params", function () { + const url = "https://example.com/page#section"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { stripFragments: true }); + expect(result, "should remove hash fragment").to.equal(expected); + }); + + it("should strip URL fragments from URLs with query params", function () { + const url = "https://example.com/page?foo=bar#section"; + const expected = "https://example.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { stripFragments: true }); + expect(result, "should remove hash fragment and preserve query params").to.equal(expected); + }); + + it("should strip fragments when combined with stripAllQueryParams", function () { + const url = "https://example.com/page?foo=bar#section"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { stripAllQueryParams: true, stripFragments: true }); + expect(result, "should remove both query params and fragment").to.equal(expected); + }); + + it("should strip fragments when combined with stripQueryParamsForDomains", function () { + const url = "https://example.com/page?foo=bar#section"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["example.com"], + stripFragments: true + }); + expect(result, "should remove both query params and fragment for matching domain").to.equal(expected); + }); + + it("should strip fragments when combined with stripQueryParams", function () { + const url = "https://example.com/page?foo=bar&keep=this#section"; + const expected = "https://example.com/page?keep=this"; + const result = neuwo.cleanUrl(url, { + stripQueryParams: ["foo"], + stripFragments: true + }); + expect(result, "should remove specified query params and fragment").to.equal(expected); + }); + + it("should handle URLs without fragments gracefully", function () { + const url = "https://example.com/page?foo=bar"; + const expected = "https://example.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { stripFragments: true }); + expect(result, "should handle URLs without fragments").to.equal(expected); + }); + + it("should handle empty fragments", function () { + const url = "https://example.com/page#"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { stripFragments: true }); + expect(result, "should remove empty fragment").to.equal(expected); + }); + + it("should handle complex fragments with special characters", function () { + const url = "https://example.com/page?foo=bar#section-1/subsection?query"; + const expected = "https://example.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { stripFragments: true }); + expect(result, "should remove complex fragments").to.equal(expected); + }); + }); + + describe("option priority", function () { + it("should apply stripAllQueryParams first when multiple options are set", function () { + const url = "https://example.com/page?foo=bar&baz=qux"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { + stripAllQueryParams: true, + stripQueryParams: ["foo"] + }); + expect(result, "stripAllQueryParams should take precedence").to.equal(expected); + }); + + it("should apply stripQueryParamsForDomains before stripQueryParams", function () { + const url = "https://example.com/page?foo=bar&baz=qux"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["example.com"], + stripQueryParams: ["foo"] + }); + expect(result, "domain-specific stripping should take precedence").to.equal(expected); + }); + + it("should not strip for non-matching domain even with stripQueryParams set", function () { + const url = "https://other.com/page?foo=bar&baz=qux"; + const expected = "https://other.com/page?baz=qux"; + const result = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["example.com"], + stripQueryParams: ["foo"] + }); + expect(result, "should fall through to stripQueryParams for non-matching domain").to.equal(expected); + }); + }); + }); + + // Integration Tests describe("injectIabCategories edge cases and merging", function () { it("should not inject data if 'marketing_categories' is missing from the successful API response", function () { const apiResponse = { brand_safety: { BS_score: "1.0" } }; // Missing marketing_categories @@ -584,4 +1022,281 @@ describe("neuwoRtdModule", function () { ); }); }); + + describe("getBidRequestData with caching", function () { + describe("when enableCache is true (default)", function () { + it("should cache the API response and reuse it on subsequent calls", function () { + const apiResponse = getNeuwoApiResponse(); + const bidsConfig1 = bidsConfiglike(); + const bidsConfig2 = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://publisher.works/article.php?id=1"; + + // First call should make an API request + neuwo.getBidRequestData(bidsConfig1, () => {}, conf, "consent data"); + expect(server.requests.length, "First call should make an API request").to.equal(1); + + const request1 = server.requests[0]; + request1.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + + // Second call should use cached response (no new API request) + neuwo.getBidRequestData(bidsConfig2, () => {}, conf, "consent data"); + expect(server.requests.length, "Second call should not make a new API request").to.equal(1); + + // Both configs should have the same data + const contentData1 = bidsConfig1.ortb2Fragments.global.site.content.data[0]; + const contentData2 = bidsConfig2.ortb2Fragments.global.site.content.data[0]; + expect(contentData1, "First config should have Neuwo data").to.exist; + expect(contentData2, "Second config should have Neuwo data from cache").to.exist; + expect(contentData1.name, "First config should have correct provider").to.equal(neuwo.DATA_PROVIDER); + expect(contentData2.name, "Second config should have correct provider").to.equal(neuwo.DATA_PROVIDER); + }); + + it("should cache when enableCache is explicitly set to true", function () { + const apiResponse = getNeuwoApiResponse(); + const bidsConfig1 = bidsConfiglike(); + const bidsConfig2 = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://publisher.works/article.php?id=2"; + conf.params.enableCache = true; + + // First call + neuwo.getBidRequestData(bidsConfig1, () => {}, conf, "consent data"); + expect(server.requests.length, "First call should make an API request").to.equal(1); + + const request1 = server.requests[0]; + request1.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + + // Second call should use cache + neuwo.getBidRequestData(bidsConfig2, () => {}, conf, "consent data"); + expect(server.requests.length, "Second call should use cached response").to.equal(1); + }); + }); + + describe("when enableCache is false", function () { + it("should not cache the API response and make a new request each time", function () { + const apiResponse = getNeuwoApiResponse(); + const bidsConfig1 = bidsConfiglike(); + const bidsConfig2 = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://publisher.works/article.php?id=3"; + conf.params.enableCache = false; + + // First call should make an API request + neuwo.getBidRequestData(bidsConfig1, () => {}, conf, "consent data"); + expect(server.requests.length, "First call should make an API request").to.equal(1); + + const request1 = server.requests[0]; + request1.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + + // Second call should make a new API request (not use cache) + neuwo.getBidRequestData(bidsConfig2, () => {}, conf, "consent data"); + expect(server.requests.length, "Second call should make a new API request").to.equal(2); + + const request2 = server.requests[1]; + request2.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + + // Both configs should have the same data structure + const contentData1 = bidsConfig1.ortb2Fragments.global.site.content.data[0]; + const contentData2 = bidsConfig2.ortb2Fragments.global.site.content.data[0]; + expect(contentData1, "First config should have Neuwo data").to.exist; + expect(contentData2, "Second config should have Neuwo data from new request").to.exist; + expect(contentData1.name, "First config should have correct provider").to.equal(neuwo.DATA_PROVIDER); + expect(contentData2.name, "Second config should have correct provider").to.equal(neuwo.DATA_PROVIDER); + }); + + it("should bypass existing cache when enableCache is false", function () { + const apiResponse = getNeuwoApiResponse(); + const bidsConfig1 = bidsConfiglike(); + const bidsConfig2 = bidsConfiglike(); + const bidsConfig3 = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://publisher.works/article.php?id=4"; + + // First call with caching enabled (default) + neuwo.getBidRequestData(bidsConfig1, () => {}, conf, "consent data"); + expect(server.requests.length, "First call should make an API request").to.equal(1); + + const request1 = server.requests[0]; + request1.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + + // Second call with caching enabled should use cache + neuwo.getBidRequestData(bidsConfig2, () => {}, conf, "consent data"); + expect(server.requests.length, "Second call should use cache").to.equal(1); + + // Third call with caching disabled should bypass cache + conf.params.enableCache = false; + neuwo.getBidRequestData(bidsConfig3, () => {}, conf, "consent data"); + expect(server.requests.length, "Third call should bypass cache and make new request").to.equal(2); + + const request2 = server.requests[1]; + request2.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + }); + }); + }); + + describe("getBidRequestData with URL query param stripping", function () { + describe("when stripAllQueryParams is enabled", function () { + it("should strip all query parameters from the analyzed URL", function () { + const bidsConfig = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://publisher.works/article.php?utm_source=test&utm_campaign=example&id=5"; + conf.params.stripAllQueryParams = true; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should not contain encoded query params").to.include( + encodeURIComponent("https://publisher.works/article.php") + ); + expect(request.url, "The request URL should not contain utm_source").to.not.include( + encodeURIComponent("utm_source") + ); + }); + }); + + describe("when stripQueryParamsForDomains is enabled", function () { + it("should strip query params only for matching domains", function () { + const bidsConfig = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://publisher.works/article.php?foo=bar&id=5"; + conf.params.stripQueryParamsForDomains = ["publisher.works"]; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should contain the URL without query params").to.include( + encodeURIComponent("https://publisher.works/article.php") + ); + expect(request.url, "The request URL should not contain the id param").to.not.include( + encodeURIComponent("id=5") + ); + }); + + it("should not strip query params for non-matching domains", function () { + const bidsConfig = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://other-domain.com/page?foo=bar&id=5"; + conf.params.stripQueryParamsForDomains = ["publisher.works"]; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should contain the full URL with query params").to.include( + encodeURIComponent("https://other-domain.com/page?foo=bar&id=5") + ); + }); + + it("should handle subdomain matching correctly", function () { + const bidsConfig = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://sub.publisher.works/page?tracking=123"; + conf.params.stripQueryParamsForDomains = ["publisher.works"]; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should strip params for subdomain").to.include( + encodeURIComponent("https://sub.publisher.works/page") + ); + expect(request.url, "The request URL should not contain tracking param").to.not.include( + encodeURIComponent("tracking=123") + ); + }); + }); + + describe("when stripQueryParams is enabled", function () { + it("should strip only specified query parameters", function () { + const bidsConfig = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://publisher.works/article.php?utm_source=test&utm_campaign=example&id=5"; + conf.params.stripQueryParams = ["utm_source", "utm_campaign"]; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should contain the id param").to.include( + encodeURIComponent("id=5") + ); + expect(request.url, "The request URL should not contain utm_source").to.not.include( + encodeURIComponent("utm_source") + ); + expect(request.url, "The request URL should not contain utm_campaign").to.not.include( + encodeURIComponent("utm_campaign") + ); + }); + + it("should handle stripping params that result in no query string", function () { + const bidsConfig = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://publisher.works/article.php?utm_source=test"; + conf.params.stripQueryParams = ["utm_source"]; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should not contain a query string").to.include( + encodeURIComponent("https://publisher.works/article.php") + ); + expect(request.url, "The request URL should not contain utm_source").to.not.include( + encodeURIComponent("utm_source") + ); + }); + + it("should leave URL unchanged if specified params do not exist", function () { + const bidsConfig = bidsConfiglike(); + const conf = config(); + const originalUrl = "https://publisher.works/article.php?id=5"; + conf.params.websiteToAnalyseUrl = originalUrl; + conf.params.stripQueryParams = ["utm_source", "nonexistent"]; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should contain the original URL").to.include( + encodeURIComponent(originalUrl) + ); + }); + }); + + describe("when no stripping options are provided", function () { + it("should send the URL with all query parameters intact", function () { + const bidsConfig = bidsConfiglike(); + const conf = config(); + const originalUrl = "https://publisher.works/article.php?get=horrible_url_for_testing&id=5"; + conf.params.websiteToAnalyseUrl = originalUrl; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should contain the full original URL").to.include( + encodeURIComponent(originalUrl) + ); + }); + }); + }); }); diff --git a/test/spec/modules/nextMillenniumBidAdapter_spec.js b/test/spec/modules/nextMillenniumBidAdapter_spec.js index 9834f27f132..e05d797526a 100644 --- a/test/spec/modules/nextMillenniumBidAdapter_spec.js +++ b/test/spec/modules/nextMillenniumBidAdapter_spec.js @@ -18,8 +18,8 @@ describe('nextMillenniumBidAdapterTests', () => { { title: 'imp - banner', data: { + impId: '5', id: '123', - postBody: {ext: {nextMillennium: {refresh_counts: {}, elemOffsets: {}}}}, bid: { mediaTypes: {banner: {sizes: [[300, 250], [320, 250]]}}, adUnitCode: 'test-banner-1', @@ -37,7 +37,7 @@ describe('nextMillenniumBidAdapterTests', () => { }, expected: { - id: 'e36ea395f67f', + id: '5', bidfloorcur: 'EUR', bidfloor: 1.11, ext: {prebid: {storedrequest: {id: '123'}}}, @@ -53,8 +53,8 @@ describe('nextMillenniumBidAdapterTests', () => { { title: 'imp - video', data: { + impId: '3', id: '234', - postBody: {ext: {nextMillennium: {refresh_counts: {}, elemOffsets: {}}}}, bid: { mediaTypes: {video: {playerSize: [400, 300], api: [2], placement: 1, plcmt: 1}}, adUnitCode: 'test-video-1', @@ -71,7 +71,7 @@ describe('nextMillenniumBidAdapterTests', () => { }, expected: { - id: 'e36ea395f67f', + id: '3', bidfloorcur: 'USD', ext: {prebid: {storedrequest: {id: '234'}}}, video: { @@ -89,8 +89,8 @@ describe('nextMillenniumBidAdapterTests', () => { { title: 'imp - mediaTypes.video is empty', data: { + impId: '4', id: '234', - postBody: {ext: {nextMillennium: {refresh_counts: {}, elemOffsets: {}}}}, bid: { mediaTypes: {video: {w: 640, h: 480}}, bidId: 'e36ea395f67f', @@ -105,7 +105,7 @@ describe('nextMillenniumBidAdapterTests', () => { }, expected: { - id: 'e36ea395f67f', + id: '4', bidfloorcur: 'USD', ext: {prebid: {storedrequest: {id: '234'}}}, video: {w: 640, h: 480, mimes: ['video/mp4', 'video/x-ms-wmv', 'application/javascript']}, @@ -115,8 +115,8 @@ describe('nextMillenniumBidAdapterTests', () => { { title: 'imp with gpid', data: { + impId: '2', id: '123', - postBody: {ext: {nextMillennium: {refresh_counts: {}, elemOffsets: {}}}}, bid: { mediaTypes: {banner: {sizes: [[300, 250], [320, 250]]}}, adUnitCode: 'test-gpid-1', @@ -132,7 +132,7 @@ describe('nextMillenniumBidAdapterTests', () => { }, expected: { - id: 'e36ea395f67a', + id: '2', ext: { prebid: {storedrequest: {id: '123'}}, gpid: 'imp-gpid-123' @@ -144,8 +144,8 @@ describe('nextMillenniumBidAdapterTests', () => { { title: 'imp with pbadslot', data: { + impId: '1', id: '123', - postBody: {ext: {nextMillennium: {refresh_counts: {}, elemOffsets: {}}}}, bid: { mediaTypes: {banner: {sizes: [[300, 250], [320, 250]]}}, adUnitCode: 'test-gpid-1', @@ -167,7 +167,7 @@ describe('nextMillenniumBidAdapterTests', () => { }, expected: { - id: 'e36ea395f67a', + id: '1', ext: { prebid: {storedrequest: {id: '123'}}, }, @@ -178,8 +178,8 @@ describe('nextMillenniumBidAdapterTests', () => { for (const {title, data, expected} of dataTests) { it(title, () => { - const {bid, id, mediaTypes, postBody} = data; - const imp = getImp(bid, id, mediaTypes, postBody); + const {impId, bid, id, mediaTypes} = data; + const imp = getImp(impId, bid, id, mediaTypes); expect(imp).to.deep.equal(expected); }); } @@ -900,17 +900,17 @@ describe('nextMillenniumBidAdapterTests', () => { describe('Check ext.next_mil_imps', function() { const expectedNextMilImps = [ { - impId: 'bid1234', + impId: '1', nextMillennium: {refresh_count: 1}, }, { - impId: 'bid1235', + impId: '2', nextMillennium: {refresh_count: 1}, }, { - impId: 'bid1236', + impId: '3', nextMillennium: {refresh_count: 1}, }, ]; @@ -1183,6 +1183,12 @@ describe('nextMillenniumBidAdapterTests', () => { expect(requestData.id).to.equal(expected.id); expect(requestData.tmax).to.equal(expected.tmax); expect(requestData?.imp?.length).to.equal(expected.impSize); + + for (let i = 0; i < bidRequests.length; i++) { + const impId = String(i + 1); + expect(impId).to.equal(requestData.imp[i].id); + expect(bidRequests[i].bidId).to.equal(request[0].bidIds.get(impId)); + }; }); }; }); @@ -1199,7 +1205,7 @@ describe('nextMillenniumBidAdapterTests', () => { bid: [ { id: '7457329903666272789-0', - impid: '700ce0a43f72', + impid: '1', price: 0.5, adm: 'Hello! It\'s a test ad!', adid: '96846035-0', @@ -1210,7 +1216,7 @@ describe('nextMillenniumBidAdapterTests', () => { { id: '7457329903666272789-1', - impid: '700ce0a43f73', + impid: '2', price: 0.7, adm: 'https://some_vast_host.com/vast.xml', adid: '96846035-1', @@ -1222,7 +1228,7 @@ describe('nextMillenniumBidAdapterTests', () => { { id: '7457329903666272789-2', - impid: '700ce0a43f74', + impid: '3', price: 1.0, adm: '', adid: '96846035-3', @@ -1238,6 +1244,14 @@ describe('nextMillenniumBidAdapterTests', () => { }, }, + bidRequest: { + bidIds: new Map([ + ['1', '700ce0a43f72'], + ['2', '700ce0a43f73'], + ['3', '700ce0a43f74'], + ]), + }, + expected: [ { title: 'banner', @@ -1308,15 +1322,19 @@ describe('nextMillenniumBidAdapterTests', () => { const tests = [ { title: 'parameters adSlots and allowedAds are empty', + impId: '1', bid: { params: {}, }, - expected: {}, + expected: { + impId: '1', + }, }, { title: 'parameters adSlots and allowedAds', + impId: '2', bid: { params: { adSlots: ['test1'], @@ -1325,15 +1343,17 @@ describe('nextMillenniumBidAdapterTests', () => { }, expected: { + impId: '2', adSlots: ['test1'], allowedAds: ['test2'], }, }, ]; - for (const {title, bid, expected} of tests) { + for (const {title, impId, bid, expected} of tests) { it(title, () => { - const extNextMilImp = getExtNextMilImp(bid); + const extNextMilImp = getExtNextMilImp(impId, bid); + expect(extNextMilImp.impId).to.deep.equal(expected.impId); expect(extNextMilImp.nextMillennium.adSlots).to.deep.equal(expected.adSlots); expect(extNextMilImp.nextMillennium.allowedAds).to.deep.equal(expected.allowedAds); }); diff --git a/test/spec/modules/nexx360BidAdapter_spec.js b/test/spec/modules/nexx360BidAdapter_spec.js index 7756e96bd99..d3ba872946f 100644 --- a/test/spec/modules/nexx360BidAdapter_spec.js +++ b/test/spec/modules/nexx360BidAdapter_spec.js @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { - spec, STORAGE, getNexx360LocalStorage, + spec, STORAGE, getNexx360LocalStorage, getGzipSetting, } from 'modules/nexx360BidAdapter.js'; import sinon from 'sinon'; import { getAmxId } from '../../../libraries/nexx360Utils/index.js'; @@ -33,6 +33,11 @@ describe('Nexx360 bid adapter tests', () => { }, }; + it('We test getGzipSettings', () => { + const output = getGzipSetting(); + expect(output).to.be.a('boolean'); + }); + describe('isBidRequestValid()', () => { let bannerBid; beforeEach(() => { @@ -95,12 +100,12 @@ describe('Nexx360 bid adapter tests', () => { }); it('We test if we get the nexx360Id', () => { const output = getNexx360LocalStorage(); - expect(output).to.be.eql(false); + expect(output).to.be.eql(null); }); after(() => { sandbox.restore() }); - }) + }); describe('getNexx360LocalStorage enabled but nothing', () => { before(() => { @@ -115,7 +120,7 @@ describe('Nexx360 bid adapter tests', () => { after(() => { sandbox.restore() }); - }) + }); describe('getNexx360LocalStorage enabled but wrong payload', () => { before(() => { @@ -125,7 +130,7 @@ describe('Nexx360 bid adapter tests', () => { }); it('We test if we get the nexx360Id', () => { const output = getNexx360LocalStorage(); - expect(output).to.be.eql(false); + expect(output).to.be.eql(null); }); after(() => { sandbox.restore() @@ -155,7 +160,7 @@ describe('Nexx360 bid adapter tests', () => { }); it('We test if we get the amxId', () => { const output = getAmxId(STORAGE, 'nexx360'); - expect(output).to.be.eql(false); + expect(output).to.be.eql(null); }); after(() => { sandbox.restore() @@ -303,6 +308,9 @@ describe('Nexx360 bid adapter tests', () => { }, nexx360: { tagId: 'luvxjvgn', + adUnitName: 'header-ad', + adUnitPath: '/12345/nexx360/Homepage/HP/Header-Ad', + divId: 'div-1', }, adUnitName: 'header-ad', adUnitPath: '/12345/nexx360/Homepage/HP/Header-Ad', @@ -324,6 +332,7 @@ describe('Nexx360 bid adapter tests', () => { divId: 'div-2-abcd', nexx360: { placement: 'testPlacement', + divId: 'div-2-abcd', allBids: true, }, }, @@ -335,8 +344,10 @@ describe('Nexx360 bid adapter tests', () => { version: requestContent.ext.version, source: 'prebid.js', pageViewId: requestContent.ext.pageViewId, - bidderVersion: '6.3', - localStorage: { amxId: 'abcdef'} + bidderVersion: '7.1', + localStorage: { amxId: 'abcdef'}, + sessionId: requestContent.ext.sessionId, + requestCounter: 0, }, cur: [ 'USD', @@ -564,6 +575,7 @@ describe('Nexx360 bid adapter tests', () => { mediaType: 'outstream', ssp: 'appnexus', adUnitCode: 'div-1', + divId: 'div-1', }, }, ], @@ -585,6 +597,7 @@ describe('Nexx360 bid adapter tests', () => { creativeId: '97517771', currency: 'USD', netRevenue: true, + divId: 'div-1', ttl: 120, mediaType: 'video', meta: { advertiserDomains: ['appnexus.com'], demandSource: 'appnexus' }, diff --git a/test/spec/modules/nodalsAiRtdProvider_spec.js b/test/spec/modules/nodalsAiRtdProvider_spec.js index 486822f3934..9155e07b2ff 100644 --- a/test/spec/modules/nodalsAiRtdProvider_spec.js +++ b/test/spec/modules/nodalsAiRtdProvider_spec.js @@ -965,4 +965,62 @@ describe('NodalsAI RTD Provider', () => { expect(server.requests.length).to.equal(0); }); }); + + describe('#getEngine()', () => { + it('should return undefined when $nodals object does not exist', () => { + // Setup data in storage to avoid triggering fetchData + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + + delete window.$nodals; + const result = nodalsAiRtdSubmodule.getTargetingData([], validConfig, permissiveUserConsent); + expect(result).to.deep.equal({}); + }); + + it('should return undefined when adTargetingEngine object does not exist', () => { + // Setup data in storage to avoid triggering fetchData + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + + window.$nodals = {}; + const result = nodalsAiRtdSubmodule.getTargetingData([], validConfig, permissiveUserConsent); + expect(result).to.deep.equal({}); + }); + + it('should return undefined when specific engine version does not exist', () => { + // Setup data in storage to avoid triggering fetchData + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + + window.$nodals = { + adTargetingEngine: {} + }; + const result = nodalsAiRtdSubmodule.getTargetingData([], validConfig, permissiveUserConsent); + expect(result).to.deep.equal({}); + }); + + it('should return undefined when property access throws an error', () => { + // Setup data in storage to avoid triggering fetchData + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + + Object.defineProperty(window, '$nodals', { + get() { + throw new Error('Access denied'); + }, + configurable: true + }); + const result = nodalsAiRtdSubmodule.getTargetingData([], validConfig, permissiveUserConsent); + expect(result).to.deep.equal({}); + delete window.$nodals; + }); + }); }); diff --git a/test/spec/modules/oguryBidAdapter_spec.js b/test/spec/modules/oguryBidAdapter_spec.js index f6922f70942..3cd7542f6c2 100644 --- a/test/spec/modules/oguryBidAdapter_spec.js +++ b/test/spec/modules/oguryBidAdapter_spec.js @@ -3,6 +3,7 @@ import sinon from 'sinon'; import { spec, ortbConverterProps } from 'modules/oguryBidAdapter'; import * as utils from 'src/utils.js'; import { server } from '../../mocks/xhr.js'; +import {getDevicePixelRatio} from '../../../libraries/devicePixelRatio/devicePixelRatio.js'; const BID_URL = 'https://mweb-hb.presage.io/api/header-bidding-request'; const TIMEOUT_URL = 'https://ms-ads-monitoring-events.presage.io/bid_timeout' @@ -121,34 +122,34 @@ describe('OguryBidAdapter', () => { describe('isBidRequestValid', () => { it('should validate correct bid', () => { - const validBid = utils.deepClone(bidRequests[0]); + let validBid = utils.deepClone(bidRequests[0]); - const isValid = spec.isBidRequestValid(validBid); + let isValid = spec.isBidRequestValid(validBid); expect(isValid).to.true; }); it('should not validate when sizes is not defined', () => { - const invalidBid = utils.deepClone(bidRequests[0]); + let invalidBid = utils.deepClone(bidRequests[0]); delete invalidBid.sizes; delete invalidBid.mediaTypes; - const isValid = spec.isBidRequestValid(invalidBid); + let isValid = spec.isBidRequestValid(invalidBid); expect(isValid).to.be.false; }); it('should not validate bid when adunit is not defined', () => { - const invalidBid = utils.deepClone(bidRequests[0]); + let invalidBid = utils.deepClone(bidRequests[0]); delete invalidBid.params.adUnitId; - const isValid = spec.isBidRequestValid(invalidBid); + let isValid = spec.isBidRequestValid(invalidBid); expect(isValid).to.to.be.false; }); it('should not validate bid when assetKey is not defined', () => { - const invalidBid = utils.deepClone(bidRequests[0]); + let invalidBid = utils.deepClone(bidRequests[0]); delete invalidBid.params.assetKey; - const isValid = spec.isBidRequestValid(invalidBid); + let isValid = spec.isBidRequestValid(invalidBid); expect(isValid).to.be.false; }); @@ -201,16 +202,12 @@ describe('OguryBidAdapter', () => { syncOptions = { pixelEnabled: true }; }); - it('should return syncs array with three elements of type image', () => { + it('should return syncs array with one element of type image', () => { const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); - expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.contain('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch'); - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.contain('https://ms-cookie-sync.presage.io/ttd/init-sync'); - expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.contain('https://ms-cookie-sync.presage.io/xandr/init-sync'); + expect(userSyncs[0].url).to.contain('https://ms-cookie-sync.presage.io/user-sync'); }); it('should set the source as query param', () => { @@ -220,23 +217,17 @@ describe('OguryBidAdapter', () => { it('should set the tcString as query param', () => { const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); - expect(new URL(userSyncs[0].url).searchParams.get('iab_string')).to.equal(gdprConsent.consentString) - expect(new URL(userSyncs[1].url).searchParams.get('iab_string')).to.equal(gdprConsent.consentString) - expect(new URL(userSyncs[2].url).searchParams.get('iab_string')).to.equal(gdprConsent.consentString) + expect(new URL(userSyncs[0].url).searchParams.get('gdpr_consent')).to.equal(gdprConsent.consentString) }); it('should set the gppString as query param', () => { const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); expect(new URL(userSyncs[0].url).searchParams.get('gpp')).to.equal(gppConsent.gppString) - expect(new URL(userSyncs[1].url).searchParams.get('gpp')).to.equal(gppConsent.gppString) - expect(new URL(userSyncs[2].url).searchParams.get('gpp')).to.equal(gppConsent.gppString) }); it('should set the gpp_sid as query param', () => { const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); expect(new URL(userSyncs[0].url).searchParams.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) - expect(new URL(userSyncs[1].url).searchParams.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) - expect(new URL(userSyncs[2].url).searchParams.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) }); it('should return an empty array when pixel is disable', () => { @@ -251,13 +242,9 @@ describe('OguryBidAdapter', () => { }; const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); - expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('image'); - expect(new URL(userSyncs[0].url).searchParams.get('iab_string')).to.equal('') - expect(userSyncs[1].type).to.equal('image'); - expect(new URL(userSyncs[1].url).searchParams.get('iab_string')).to.equal('') - expect(userSyncs[2].type).to.equal('image'); - expect(new URL(userSyncs[2].url).searchParams.get('iab_string')).to.equal('') + expect(new URL(userSyncs[0].url).searchParams.get('gdpr_consent')).to.equal('') }); it('should return syncs array with three elements of type image when consentString is null', () => { @@ -267,39 +254,27 @@ describe('OguryBidAdapter', () => { }; const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); - expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('image'); - expect(new URL(userSyncs[0].url).searchParams.get('iab_string')).to.equal('') - expect(userSyncs[1].type).to.equal('image'); - expect(new URL(userSyncs[1].url).searchParams.get('iab_string')).to.equal('') - expect(userSyncs[2].type).to.equal('image'); - expect(new URL(userSyncs[2].url).searchParams.get('iab_string')).to.equal('') + expect(new URL(userSyncs[0].url).searchParams.get('gdpr_consent')).to.equal('') }); it('should return syncs array with three elements of type image when gdprConsent is undefined', () => { gdprConsent = undefined; const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); - expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('image'); - expect(new URL(userSyncs[0].url).searchParams.get('iab_string')).to.equal('') - expect(userSyncs[1].type).to.equal('image'); - expect(new URL(userSyncs[1].url).searchParams.get('iab_string')).to.equal('') - expect(userSyncs[2].type).to.equal('image'); - expect(new URL(userSyncs[2].url).searchParams.get('iab_string')).to.equal('') + expect(new URL(userSyncs[0].url).searchParams.get('gdpr_consent')).to.equal('') }); it('should return syncs array with three elements of type image when gdprConsent is null', () => { gdprConsent = null; const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); - expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('image'); - expect(new URL(userSyncs[0].url).searchParams.get('iab_string')).to.equal('') - expect(userSyncs[1].type).to.equal('image'); - expect(new URL(userSyncs[1].url).searchParams.get('iab_string')).to.equal('') - expect(userSyncs[2].type).to.equal('image'); - expect(new URL(userSyncs[2].url).searchParams.get('iab_string')).to.equal('') + expect(new URL(userSyncs[0].url).searchParams.get('gdpr_consent')).to.equal('') }); it('should return syncs array with three elements of type image when gdprConsent is null and gdprApplies is false', () => { @@ -309,13 +284,9 @@ describe('OguryBidAdapter', () => { }; const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); - expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('image'); - expect(new URL(userSyncs[0].url).searchParams.get('iab_string')).to.equal('') - expect(userSyncs[1].type).to.equal('image'); - expect(new URL(userSyncs[1].url).searchParams.get('iab_string')).to.equal('') - expect(userSyncs[2].type).to.equal('image'); - expect(new URL(userSyncs[2].url).searchParams.get('iab_string')).to.equal('') + expect(new URL(userSyncs[0].url).searchParams.get('gdpr_consent')).to.equal('') }); it('should return syncs array with three elements of type image when gdprConsent is empty string and gdprApplies is false', () => { @@ -325,13 +296,9 @@ describe('OguryBidAdapter', () => { }; const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); - expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('image'); - expect(new URL(userSyncs[0].url).searchParams.get('iab_string')).to.equal('') - expect(userSyncs[1].type).to.equal('image'); - expect(new URL(userSyncs[1].url).searchParams.get('iab_string')).to.equal('') - expect(userSyncs[2].type).to.equal('image'); - expect(new URL(userSyncs[2].url).searchParams.get('iab_string')).to.equal('') + expect(new URL(userSyncs[0].url).searchParams.get('gdpr_consent')).to.equal('') }); it('should return syncs array with three elements of type image when gppString is undefined', () => { @@ -341,22 +308,12 @@ describe('OguryBidAdapter', () => { }; const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); - expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[2].type).to.equal('image'); const firstUrlSync = new URL(userSyncs[0].url).searchParams expect(firstUrlSync.get('gpp')).to.equal('') expect(firstUrlSync.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) - - const secondtUrlSync = new URL(userSyncs[1].url).searchParams - expect(secondtUrlSync.get('gpp')).to.equal('') - expect(secondtUrlSync.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) - - const thirdUrlSync = new URL(userSyncs[2].url).searchParams - expect(thirdUrlSync.get('gpp')).to.equal('') - expect(thirdUrlSync.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) }); it('should return syncs array with three elements of type image when gppString is null', () => { @@ -366,66 +323,36 @@ describe('OguryBidAdapter', () => { }; const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); - expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[2].type).to.equal('image'); const firstUrlSync = new URL(userSyncs[0].url).searchParams expect(firstUrlSync.get('gpp')).to.equal('') expect(firstUrlSync.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) - - const secondtUrlSync = new URL(userSyncs[1].url).searchParams - expect(secondtUrlSync.get('gpp')).to.equal('') - expect(secondtUrlSync.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) - - const thirdUrlSync = new URL(userSyncs[2].url).searchParams - expect(thirdUrlSync.get('gpp')).to.equal('') - expect(thirdUrlSync.get('gpp_sid')).to.equal(gppConsent.applicableSections.toString()) }); it('should return syncs array with three elements of type image when gppConsent is undefined', () => { gppConsent = undefined; const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); - expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[2].type).to.equal('image'); const firstUrlSync = new URL(userSyncs[0].url).searchParams expect(firstUrlSync.get('gpp')).to.equal('') expect(firstUrlSync.get('gpp_sid')).to.equal('') - - const secondtUrlSync = new URL(userSyncs[1].url).searchParams - expect(secondtUrlSync.get('gpp')).to.equal('') - expect(secondtUrlSync.get('gpp_sid')).to.equal('') - - const thirdUrlSync = new URL(userSyncs[2].url).searchParams - expect(thirdUrlSync.get('gpp')).to.equal('') - expect(thirdUrlSync.get('gpp_sid')).to.equal('') }); it('should return syncs array with three elements of type image when gppConsent is null', () => { gppConsent = null; const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); - expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[2].type).to.equal('image'); const firstUrlSync = new URL(userSyncs[0].url).searchParams expect(firstUrlSync.get('gpp')).to.equal('') expect(firstUrlSync.get('gpp_sid')).to.equal('') - - const secondtUrlSync = new URL(userSyncs[1].url).searchParams - expect(secondtUrlSync.get('gpp')).to.equal('') - expect(secondtUrlSync.get('gpp_sid')).to.equal('') - - const thirdUrlSync = new URL(userSyncs[2].url).searchParams - expect(thirdUrlSync.get('gpp')).to.equal('') - expect(thirdUrlSync.get('gpp_sid')).to.equal('') }); it('should return syncs array with three elements of type image when gppConsent is null and applicableSections is empty', () => { @@ -435,22 +362,12 @@ describe('OguryBidAdapter', () => { }; const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent, [], gppConsent); - expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[2].type).to.equal('image'); const firstUrlSync = new URL(userSyncs[0].url).searchParams expect(firstUrlSync.get('gpp')).to.equal('') expect(firstUrlSync.get('gpp_sid')).to.equal('') - - const secondtUrlSync = new URL(userSyncs[1].url).searchParams - expect(secondtUrlSync.get('gpp')).to.equal('') - expect(secondtUrlSync.get('gpp_sid')).to.equal('') - - const thirdUrlSync = new URL(userSyncs[2].url).searchParams - expect(thirdUrlSync.get('gpp')).to.equal('') - expect(thirdUrlSync.get('gpp_sid')).to.equal('') }); it('should return syncs array with three elements of type image when gppString is empty string and applicableSections is empty', () => { @@ -460,22 +377,12 @@ describe('OguryBidAdapter', () => { }; const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs).to.have.lengthOf(1); expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[2].type).to.equal('image'); const firstUrlSync = new URL(userSyncs[0].url).searchParams expect(firstUrlSync.get('gpp')).to.equal('') expect(firstUrlSync.get('gpp_sid')).to.equal('') - - const secondtUrlSync = new URL(userSyncs[1].url).searchParams - expect(secondtUrlSync.get('gpp')).to.equal('') - expect(secondtUrlSync.get('gpp_sid')).to.equal('') - - const thirdUrlSync = new URL(userSyncs[2].url).searchParams - expect(thirdUrlSync.get('gpp')).to.equal('') - expect(thirdUrlSync.get('gpp_sid')).to.equal('') }); }); @@ -671,10 +578,6 @@ describe('OguryBidAdapter', () => { return stubbedCurrentTime; }); - const stubbedDevicePixelMethod = sinon.stub(window, 'devicePixelRatio').get(function() { - return stubbedDevicePixelRatio; - }); - const defaultTimeout = 1000; function assertImpObject(ortbBidRequest, bidRequest) { @@ -719,7 +622,7 @@ describe('OguryBidAdapter', () => { expect(dataRequest.ext).to.deep.equal({ prebidversion: '$prebid.version$', - adapterversion: '2.0.4' + adapterversion: '2.0.5' }); expect(dataRequest.device).to.deep.equal({ @@ -733,7 +636,7 @@ describe('OguryBidAdapter', () => { beforeEach(() => { windowTopStub = sinon.stub(utils, 'getWindowTop'); - windowTopStub.returns({ location: { href: currentLocation } }); + windowTopStub.returns({ location: { href: currentLocation }, devicePixelRatio: stubbedDevicePixelRatio}); }); afterEach(() => { @@ -742,7 +645,6 @@ describe('OguryBidAdapter', () => { after(() => { stubbedCurrentTimeMethod.restore(); - stubbedDevicePixelMethod.restore(); }); it('sends bid request to ENDPOINT via POST', function () { @@ -851,7 +753,7 @@ describe('OguryBidAdapter', () => { }); describe('interpretResponse', function () { - const openRtbBidResponse = { + let openRtbBidResponse = { body: { id: 'id_of_bid_response', seatbid: [{ diff --git a/test/spec/modules/omsBidAdapter_spec.js b/test/spec/modules/omsBidAdapter_spec.js index 2c36465c66d..f1f7df46843 100644 --- a/test/spec/modules/omsBidAdapter_spec.js +++ b/test/spec/modules/omsBidAdapter_spec.js @@ -451,7 +451,7 @@ describe('omsBidAdapter', function () { 'currency': 'USD', 'netRevenue': true, 'mediaType': 'video', - 'ad': `
`, + 'vastXml': ``, 'ttl': 300, 'meta': { 'advertiserDomains': ['example.com'] diff --git a/test/spec/modules/onetagBidAdapter_spec.js b/test/spec/modules/onetagBidAdapter_spec.js index aa953e35be5..dd37a929b45 100644 --- a/test/spec/modules/onetagBidAdapter_spec.js +++ b/test/spec/modules/onetagBidAdapter_spec.js @@ -491,7 +491,7 @@ describe('onetag', function () { }); it('Should contain all keys', function () { expect(data).to.be.an('object'); - expect(data).to.include.all.keys('location', 'referrer', 'stack', 'numIframes', 'sHeight', 'sWidth', 'docHeight', 'wHeight', 'wWidth', 'sLeft', 'sTop', 'hLength', 'bids', 'docHidden', 'xOffset', 'yOffset', 'networkConnectionType', 'networkEffectiveConnectionType', 'timing', 'version', 'fledgeEnabled'); + expect(data).to.include.all.keys('location', 'referrer', 'stack', 'numIframes', 'sHeight', 'sWidth', 'docHeight', 'wHeight', 'wWidth', 'hLength', 'bids', 'docHidden', 'xOffset', 'yOffset', 'networkConnectionType', 'networkEffectiveConnectionType', 'timing', 'version', 'fledgeEnabled'); expect(data.location).to.satisfy(function (value) { return value === null || typeof value === 'string'; }); @@ -502,8 +502,6 @@ describe('onetag', function () { expect(data.sWidth).to.be.a('number'); expect(data.wWidth).to.be.a('number'); expect(data.wHeight).to.be.a('number'); - expect(data.sLeft).to.be.a('number'); - expect(data.sTop).to.be.a('number'); expect(data.hLength).to.be.a('number'); expect(data.networkConnectionType).to.satisfy(function (value) { return value === null || typeof value === 'string' @@ -1133,8 +1131,6 @@ function getBannerVideoRequest() { wHeight: 949, sWidth: 1920, sHeight: 1080, - sLeft: 1987, - sTop: 27, xOffset: 0, yOffset: 0, docHidden: false, diff --git a/test/spec/modules/operaadsBidAdapter_spec.js b/test/spec/modules/operaadsBidAdapter_spec.js index 15708c1bb42..0ff16d72293 100644 --- a/test/spec/modules/operaadsBidAdapter_spec.js +++ b/test/spec/modules/operaadsBidAdapter_spec.js @@ -217,7 +217,7 @@ describe('Opera Ads Bid Adapter', function () { const bidRequest = bidRequests[i]; expect(req.method).to.equal('POST'); - expect(req.url).to.equal('https://s.adx.opera.com/ortb/v2/' + + expect(req.url).to.equal('https://s.oa.opera.com/ortb/v2/' + bidRequest.params.publisherId + '?ep=' + bidRequest.params.endpointId); expect(req.options).to.be.an('object'); @@ -546,8 +546,8 @@ describe('Opera Ads Bid Adapter', function () { 'id': '003004d9c05c6bc7fec0', 'impid': '22c4871113f461', 'price': 1.04, - 'nurl': 'https://s.adx.opera.com/win', - 'lurl': 'https://s.adx.opera.com/loss', + 'nurl': 'https://s.oa.opera.com/win', + 'lurl': 'https://s.oa.opera.com/loss', 'adm': '', 'adomain': [ 'opera.com', @@ -628,8 +628,8 @@ describe('Opera Ads Bid Adapter', function () { 'id': '003004d9c05c6bc7fec0', 'impid': '22c4871113f461', 'price': 1.04, - 'nurl': 'https://s.adx.opera.com/win', - 'lurl': 'https://s.adx.opera.com/loss', + 'nurl': 'https://s.oa.opera.com/win', + 'lurl': 'https://s.oa.opera.com/loss', 'adm': 'Static VAST TemplateStatic VAST Taghttp://example.com/pixel.gif?asi=[ADSERVINGID]00:00:08http://example.com/pixel.gifhttp://example.com/pixel.gifhttp://example.com/pixel.gifhttp://example.com/pixel.gifhttp://example.com/pixel.gifhttp://example.com/pixel.gifhttp://example.com/pixel.gifhttp://example.com/pixel.gifhttp://www.jwplayer.com/http://example.com/pixel.gif?r=[REGULATIONS]&gdpr=[GDPRCONSENT]&pu=[PAGEURL]&da=[DEVICEUA] http://example.com/uploads/myPrerollVideo.mp4 https://example.com/adchoices-sm.pnghttps://sample-url.com', 'adomain': [ 'opera.com', @@ -698,8 +698,8 @@ describe('Opera Ads Bid Adapter', function () { 'id': '003004d9c05c6bc7fec0', 'impid': '22c4871113f461', 'price': 1.04, - 'nurl': 'https://s.adx.opera.com/win', - 'lurl': 'https://s.adx.opera.com/loss', + 'nurl': 'https://s.oa.opera.com/win', + 'lurl': 'https://s.oa.opera.com/loss', 'adm': '{"native":{"ver":"1.1","assets":[{"id":1,"required":1,"title":{"text":"The first personal browser"}},{"id":2,"required":1,"img":{"url":"https://res.adx.opera.com/xxx.png","w":720,"h":1280}},{"id":3,"required":1,"img":{"url":"https://res.adx.opera.com/xxx.png","w":60,"h":60}},{"id":4,"required":1,"data":{"value":"Download Opera","len":14}},{"id":5,"required":1,"data":{"value":"Opera","len":5}},{"id":6,"required":1,"data":{"value":"Download","len":8}}],"link":{"url":"https://www.opera.com/mobile/opera","clicktrackers":["https://thirdpart-click.tracker.com","https://t-odx.op-mobile.opera.com/click"]},"imptrackers":["https://thirdpart-imp.tracker.com","https://t-odx.op-mobile.opera.com/impr"],"jstracker":""}}', 'adomain': [ 'opera.com', @@ -782,7 +782,7 @@ describe('Opera Ads Bid Adapter', function () { } const userSyncPixels = spec.getUserSyncs(syncOptions) expect(userSyncPixels).to.have.lengthOf(1); - expect(userSyncPixels[0].url).to.equal('https://s.adx.opera.com/usersync/page') + expect(userSyncPixels[0].url).to.equal('https://s.oa.opera.com/usersync/page') }); }); diff --git a/test/spec/modules/optableRtdProvider_spec.js b/test/spec/modules/optableRtdProvider_spec.js index 7aa4be3c8b2..271d31d0185 100644 --- a/test/spec/modules/optableRtdProvider_spec.js +++ b/test/spec/modules/optableRtdProvider_spec.js @@ -29,15 +29,15 @@ describe('Optable RTD Submodule', function () { expect(parseConfig(config).bundleUrl).to.equal('https://cdn.optable.co/bundle.js'); }); - it('throws an error for invalid bundleUrl format', function () { - expect(() => parseConfig({params: {bundleUrl: 'invalidURL'}})).to.throw(); - expect(() => parseConfig({params: {bundleUrl: 'www.invalid.com'}})).to.throw(); + it('returns null bundleUrl for invalid bundleUrl format', function () { + expect(parseConfig({params: {bundleUrl: 'invalidURL'}}).bundleUrl).to.be.null; + expect(parseConfig({params: {bundleUrl: 'www.invalid.com'}}).bundleUrl).to.be.null; }); - it('throws an error for non-HTTPS bundleUrl', function () { - expect(() => parseConfig({params: {bundleUrl: 'http://cdn.optable.co/bundle.js'}})).to.throw(); - expect(() => parseConfig({params: {bundleUrl: '//cdn.optable.co/bundle.js'}})).to.throw(); - expect(() => parseConfig({params: {bundleUrl: '/bundle.js'}})).to.throw(); + it('returns null bundleUrl for non-HTTPS bundleUrl', function () { + expect(parseConfig({params: {bundleUrl: 'http://cdn.optable.co/bundle.js'}}).bundleUrl).to.be.null; + expect(parseConfig({params: {bundleUrl: '//cdn.optable.co/bundle.js'}}).bundleUrl).to.be.null; + expect(parseConfig({params: {bundleUrl: '/bundle.js'}}).bundleUrl).to.be.null; }); it('defaults adserverTargeting to true if missing', function () { @@ -46,8 +46,8 @@ describe('Optable RTD Submodule', function () { ).adserverTargeting).to.be.true; }); - it('throws an error if handleRtd is not a function', function () { - expect(() => parseConfig({params: {handleRtd: 'notAFunction'}})).to.throw(); + it('returns null handleRtd if handleRtd is not a function', function () { + expect(parseConfig({params: {handleRtd: 'notAFunction'}}).handleRtd).to.be.null; }); }); @@ -81,7 +81,14 @@ describe('Optable RTD Submodule', function () { it('does nothing if targeting data is missing the ortb2 property', async function () { window.optable.instance.targetingFromCache.returns({}); - window.optable.instance.targeting.resolves({}); + + // Dispatch event with empty ortb2 data after a short delay + setTimeout(() => { + const event = new CustomEvent('optable-targeting:change', { + detail: {} + }); + window.dispatchEvent(event); + }, 10); await defaultHandleRtd(reqBidsConfigObj, {}, mergeFn); expect(mergeFn.called).to.be.false; @@ -98,7 +105,14 @@ describe('Optable RTD Submodule', function () { it('calls targeting function if no data is found in cache', async function () { const targetingData = {ortb2: {user: {ext: {optable: 'testData'}}}}; window.optable.instance.targetingFromCache.returns(null); - window.optable.instance.targeting.resolves(targetingData); + + // Dispatch event with targeting data after a short delay + setTimeout(() => { + const event = new CustomEvent('optable-targeting:change', { + detail: targetingData + }); + window.dispatchEvent(event); + }, 10); await defaultHandleRtd(reqBidsConfigObj, {}, mergeFn); expect(mergeFn.calledWith(reqBidsConfigObj.ortb2Fragments.global, targetingData.ortb2)).to.be.true; @@ -162,18 +176,33 @@ describe('Optable RTD Submodule', function () { it('calls callback when assuming the bundle is present', function (done) { moduleConfig.params.bundleUrl = null; + window.optable = { + cmd: [], + instance: { + targetingFromCache: sandbox.stub().returns(null) + } + }; getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {}); // Check that the function is queued expect(window.optable.cmd.length).to.equal(1); + + // Dispatch the event after a short delay + setTimeout(() => { + const event = new CustomEvent('optable-targeting:change', { + detail: {ortb2: {user: {ext: {optable: 'testData'}}}} + }); + window.dispatchEvent(event); + }, 10); + // Manually trigger the queued function window.optable.cmd[0](); setTimeout(() => { expect(callback.calledOnce).to.be.true; done(); - }, 50); + }, 100); }); it('mergeOptableData catches error and executes callback when something goes wrong', function (done) { @@ -194,26 +223,64 @@ describe('Optable RTD Submodule', function () { it('getBidRequestData catches error and executes callback when something goes wrong', function (done) { moduleConfig.params.bundleUrl = null; moduleConfig.params.handleRtd = 'not a function'; + window.optable = { + cmd: [], + instance: { + targetingFromCache: sandbox.stub().returns(null) + } + }; getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {}); - expect(window.optable.cmd.length).to.equal(0); + expect(window.optable.cmd.length).to.equal(1); + + // Dispatch event after a short delay + setTimeout(() => { + const event = new CustomEvent('optable-targeting:change', { + detail: {ortb2: {user: {ext: {optable: 'testData'}}}} + }); + window.dispatchEvent(event); + }, 10); + + // Execute the queued command + window.optable.cmd[0](); setTimeout(() => { expect(callback.calledOnce).to.be.true; done(); - }, 50); + }, 100); }); it("doesn't fail when optable is not available", function (done) { + moduleConfig.params.bundleUrl = null; delete window.optable; + getBidRequestData(reqBidsConfigObj, callback, moduleConfig, {}); - expect(window?.optable?.cmd?.length).to.be.undefined; + + // The code should have created window.optable with cmd array + expect(window.optable).to.exist; + expect(window.optable.cmd.length).to.equal(1); + + // Simulate optable bundle initializing and executing commands + window.optable.instance = { + targetingFromCache: () => null + }; + + // Dispatch event after a short delay + setTimeout(() => { + const event = new CustomEvent('optable-targeting:change', { + detail: {ortb2: {user: {ext: {optable: 'testData'}}}} + }); + window.dispatchEvent(event); + }, 10); + + // Execute the queued command (simulating optable bundle execution) + window.optable.cmd[0](); setTimeout(() => { expect(callback.calledOnce).to.be.true; done(); - }, 50); + }, 100); }); }); diff --git a/test/spec/modules/optimeraRtdProvider_spec.js b/test/spec/modules/optimeraRtdProvider_spec.js index adcc84dcf73..d3165bd3c7d 100644 --- a/test/spec/modules/optimeraRtdProvider_spec.js +++ b/test/spec/modules/optimeraRtdProvider_spec.js @@ -37,6 +37,7 @@ describe('Optimera RTD score file URL is properly set for v0', () => { }] }; optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScoresURL(); optimeraRTD.setScores(); expect(optimeraRTD.apiVersion).to.equal('v0'); expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost%3A9876/context.html.js'); @@ -54,6 +55,7 @@ describe('Optimera RTD score file URL is properly set for v0', () => { }] }; optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScoresURL(); optimeraRTD.setScores(); expect(optimeraRTD.apiVersion).to.equal('v0'); expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost%3A9876/context.html.js'); @@ -72,6 +74,7 @@ describe('Optimera RTD score file URL is properly set for v0', () => { }] }; optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScoresURL(); optimeraRTD.setScores(); expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost%3A9876/context.html.js'); }); @@ -91,6 +94,7 @@ describe('Optimera RTD score file URL is properly set for v1', () => { }] }; optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScoresURL(); optimeraRTD.setScores(); expect(optimeraRTD.apiVersion).to.equal('v1'); expect(optimeraRTD.scoresURL).to.equal('https://v1.oapi26b.com/api/products/scores?c=9999&h=localhost:9876&p=/context.html&s=de'); diff --git a/test/spec/modules/permutiveCombined_spec.js b/test/spec/modules/permutiveCombined_spec.js index dbf82d68fee..244558d8378 100644 --- a/test/spec/modules/permutiveCombined_spec.js +++ b/test/spec/modules/permutiveCombined_spec.js @@ -14,7 +14,7 @@ import { } from 'modules/permutiveRtdProvider.js' import { deepAccess, deepSetValue, mergeDeep } from '../../../src/utils.js' import { config } from 'src/config.js' -import { permutiveIdentityManagerIdSubmodule } from '../../../modules/permutiveIdentityManagerIdSystem.js' +import { permutiveIdentityManagerIdSubmodule, storage as permutiveIdStorage } from '../../../modules/permutiveIdentityManagerIdSystem.js' describe('permutiveRtdProvider', function () { beforeEach(function () { @@ -35,6 +35,84 @@ describe('permutiveRtdProvider', function () { }) }) + describe('consent handling', function () { + const publisherPurposeConsent = { + gdpr: { + gdprApplies: true, + vendorData: { + publisher: { consents: { 1: true }, legitimateInterests: {} }, + vendor: { consents: {}, legitimateInterests: {} }, + purpose: { consents: {}, legitimateInterests: {} }, + } + } + } + + const vendorPurposeConsent = { + gdpr: { + gdprApplies: true, + vendorData: { + publisher: { consents: {}, legitimateInterests: {} }, + vendor: { consents: { 361: true }, legitimateInterests: {} }, + purpose: { consents: { 1: true }, legitimateInterests: {} }, + } + } + } + + const missingVendorConsent = { + gdpr: { + gdprApplies: true, + vendorData: { + publisher: { consents: { 1: true }, legitimateInterests: {} }, + vendor: { consents: {}, legitimateInterests: {} }, + purpose: { consents: { 1: true }, legitimateInterests: {} }, + } + } + } + + it('allows publisher consent path when vendor check is disabled', function () { + expect(permutiveSubmodule.init({}, publisherPurposeConsent)).to.equal(true) + }) + + it('requires vendor consent when enforceVendorConsent is enabled', function () { + expect(permutiveSubmodule.init({ params: { enforceVendorConsent: true } }, missingVendorConsent)).to.equal(false) + }) + + it('allows vendor consent path when enforceVendorConsent is enabled', function () { + expect(permutiveSubmodule.init({ params: { enforceVendorConsent: true } }, vendorPurposeConsent)).to.equal(true) + }) + + describe('identity manager gating', function () { + const idKey = 'permutive-prebid-id' + const idPayload = { providers: { id5id: { userId: 'abc', expiryTime: Date.now() + 10000 } } } + + beforeEach(function () { + permutiveIdStorage.setDataInLocalStorage(idKey, JSON.stringify(idPayload)) + }) + + afterEach(function () { + permutiveIdStorage.removeDataFromLocalStorage(idKey) + }) + + it('returns ids with publisher consent when vendor enforcement is disabled', function () { + const response = permutiveIdentityManagerIdSubmodule.getId({}, publisherPurposeConsent) + + expect(response).to.deep.equal({ id: { id5id: 'abc' } }) + }) + + it('blocks ids when vendor consent is missing and enforcement is enabled', function () { + const response = permutiveIdentityManagerIdSubmodule.getId({ params: { enforceVendorConsent: true } }, missingVendorConsent) + + expect(response).to.be.undefined + }) + + it('returns ids when vendor consent is present and enforcement is enabled', function () { + const response = permutiveIdentityManagerIdSubmodule.getId({ params: { enforceVendorConsent: true } }, vendorPurposeConsent) + + expect(response).to.deep.equal({ id: { id5id: 'abc' } }) + }) + }) + }) + describe('getModuleConfig', function () { beforeEach(function () { // Reads data from the cache @@ -49,6 +127,7 @@ describe('permutiveRtdProvider', function () { maxSegs: 500, acBidders: [], overwrites: {}, + enforceVendorConsent: false, }, }) @@ -951,7 +1030,7 @@ describe('permutiveIdentityManagerIdSystem', () => { }) describe('decode', () => { - it('returns the input unchanged', () => { + it('returns the input unchanged for most IDs', () => { const input = { id5id: { uid: '0', @@ -965,6 +1044,17 @@ describe('permutiveIdentityManagerIdSystem', () => { const result = permutiveIdentityManagerIdSubmodule.decode(input) expect(result).to.be.equal(input) }) + + it('decodes the base64-encoded array for pairId', () => { + const input = { + pairId: 'WyJBeVhiNUF0dmsvVS8xQ1d2ejJuRVk5aFl4T1g3TVFPUTJVQk1BMFdiV1ZFbSJd' + } + const result = permutiveIdentityManagerIdSubmodule.decode(input) + const expected = { + pairId: ["AyXb5Atvk/U/1CWvz2nEY9hYxOX7MQOQ2UBMA0WbWVEm"] + } + expect(result).to.deep.equal(expected) + }) }) describe('getId', () => { @@ -988,6 +1078,46 @@ describe('permutiveIdentityManagerIdSystem', () => { expect(result).to.deep.equal(expected) }) + it('handles idl_env without pairId', () => { + const data = { + 'providers': { + 'idl_env': { + 'userId': 'ats_envelope_value' + } + } + } + storage.setDataInLocalStorage(STORAGE_KEY, JSON.stringify(data)) + const result = permutiveIdentityManagerIdSubmodule.getId({}) + const expected = { + 'id': { + 'idl_env': 'ats_envelope_value' + } + } + expect(result).to.deep.equal(expected) + }) + + it('handles idl_env with pairId', () => { + const data = { + 'providers': { + 'idl_env': { + 'userId': 'ats_envelope_value', + }, + 'pairId': { + 'userId': 'pair_id_encoded_value' + } + } + } + storage.setDataInLocalStorage(STORAGE_KEY, JSON.stringify(data)) + const result = permutiveIdentityManagerIdSubmodule.getId({}) + const expected = { + 'id': { + 'idl_env': 'ats_envelope_value', + 'pairId': 'pair_id_encoded_value' + } + } + expect(result).to.deep.equal(expected) + }) + it('returns undefined if no relevant IDs are found in localStorage', () => { storage.setDataInLocalStorage(STORAGE_KEY, '{}') const result = permutiveIdentityManagerIdSubmodule.getId({}) diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 4173cd88b1b..595b95c6db8 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -3,12 +3,11 @@ import { PrebidServer as Adapter, resetSyncedStatus, validateConfig, - s2sDefaultConfig, - processPBSRequest + s2sDefaultConfig } from 'modules/prebidServerBidAdapter/index.js'; import adapterManager, {PBS_ADAPTER_NAME} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; -import {deepAccess, deepClone, mergeDeep} from 'src/utils.js'; +import {deepAccess, deepClone, getWinDimensions, mergeDeep} from 'src/utils.js'; import {ajax} from 'src/ajax.js'; import {config} from 'src/config.js'; import * as events from 'src/events.js'; @@ -626,6 +625,7 @@ describe('S2S Adapter', function () { 'auctionId': '173afb6d132ba3', 'bidderRequestId': '3d1063078dfcc8', 'tid': '437fbbf5-33f5-487a-8e16-a7112903cfe5', + 'pageViewId': '84dfd20f-0a5a-4ac6-a86b-91569066d4f4', 'bids': [ { 'bidder': 'appnexus', @@ -1195,8 +1195,8 @@ describe('S2S Adapter', function () { const requestBid = JSON.parse(server.requests[0].requestBody); sinon.assert.match(requestBid.device, { ifa: '6D92078A-8246-4BA4-AE5B-76104861E7DC', - w: window.screen.width, - h: window.screen.height, + w: getWinDimensions().screen.width, + h: getWinDimensions().screen.height, }) sinon.assert.match(requestBid.app, { bundle: 'com.test.app', @@ -1227,8 +1227,8 @@ describe('S2S Adapter', function () { const requestBid = JSON.parse(server.requests[0].requestBody); sinon.assert.match(requestBid.device, { ifa: '6D92078A-8246-4BA4-AE5B-76104861E7DC', - w: window.screen.width, - h: window.screen.height, + w: getWinDimensions().screen.width, + h: getWinDimensions().screen.height, }) sinon.assert.match(requestBid.app, { bundle: 'com.test.app', @@ -1619,8 +1619,8 @@ describe('S2S Adapter', function () { adapter.callBids(await addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); sinon.assert.match(requestBid.device, { - w: window.screen.width, - h: window.screen.height, + w: getWinDimensions().screen.width, + h: getWinDimensions().screen.height, }) expect(requestBid.imp[0].native.ver).to.equal('1.2'); }); @@ -2384,7 +2384,7 @@ describe('S2S Adapter', function () { expect(requestBid.ext.prebid.targeting.includewinners).to.equal(true); }); - it('adds s2sConfig video.ext.prebid to request for ORTB', function () { + it('adds custom property in s2sConfig.extPrebid to request for ORTB', function () { const s2sConfig = Object.assign({}, CONFIG, { extPrebid: { foo: 'bar' @@ -2415,7 +2415,7 @@ describe('S2S Adapter', function () { }); }); - it('overrides request.ext.prebid properties using s2sConfig video.ext.prebid values for ORTB', function () { + it('overrides request.ext.prebid properties using s2sConfig.extPrebid values for ORTB', function () { const s2sConfig = Object.assign({}, CONFIG, { extPrebid: { targeting: { @@ -2448,7 +2448,7 @@ describe('S2S Adapter', function () { }); }); - it('overrides request.ext.prebid properties using s2sConfig video.ext.prebid values for ORTB', function () { + it('overrides request.ext.prebid properties and adds custom property from s2sConfig.extPrebid for ORTB', function () { const s2sConfig = Object.assign({}, CONFIG, { extPrebid: { cache: { @@ -2703,6 +2703,21 @@ describe('S2S Adapter', function () { expect(parsedRequestBody.ext.prebid.multibid).to.deep.equal(expected); }); + it('passes page view IDs per bidder in request', function () { + const clonedBidRequest = utils.deepClone(BID_REQUESTS[0]); + clonedBidRequest.bidderCode = 'some-other-bidder'; + clonedBidRequest.pageViewId = '490a1cbc-a03c-429a-b212-ba3649ca820c'; + const bidRequests = [BID_REQUESTS[0], clonedBidRequest]; + const expected = { + appnexus: '84dfd20f-0a5a-4ac6-a86b-91569066d4f4', + 'some-other-bidder': '490a1cbc-a03c-429a-b212-ba3649ca820c' + }; + + adapter.callBids(REQUEST, bidRequests, addBidResponse, done, ajax); + const parsedRequestBody = JSON.parse(server.requests[0].requestBody); + expect(parsedRequestBody.ext.prebid.page_view_ids).to.deep.equal(expected); + }); + it('sets and passes pbjs version in request if channel does not exist in s2sConfig', () => { const s2sBidRequest = utils.deepClone(REQUEST); const bidRequests = utils.deepClone(BID_REQUESTS); diff --git a/test/spec/modules/publicGoodBidAdapter_spec.js b/test/spec/modules/publicGoodBidAdapter_spec.js new file mode 100644 index 00000000000..87490ff2086 --- /dev/null +++ b/test/spec/modules/publicGoodBidAdapter_spec.js @@ -0,0 +1,188 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { spec, storage } from 'modules/publicGoodBidAdapter.js'; +import { hook } from 'src/hook.js'; + +describe('Public Good Adapter', function () { + let validBidRequests; + + beforeEach(function () { + validBidRequests = [ + { + bidder: 'publicgood', + params: { + partnerId: 'prebid-test', + slotId: 'test' + }, + placementCode: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [], + }, + }, + bidId: '23acc48ad47af5', + auctionId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729', + }, + ]; + }); + + describe('for requests', function () { + describe('without partner ID', function () { + it('rejects the bid', function () { + const invalidBid = { + bidder: 'publicgood', + params: { + slotId: 'all', + }, + }; + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + }); + + describe('without slot ID', function () { + it('rejects the bid', function () { + const invalidBid = { + bidder: 'publicgood', + params: { + partnerId: 'prebid-test', + }, + }; + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + }); + + describe('with a valid bid', function () { + it('accepts the bid', function () { + const validBid = { + bidder: 'publicgood', + params: { + partnerId: 'prebid-test', + slotId: 'test' + }, + }; + const isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.equal(true); + }); + }); + }); + + describe('for server responses', function () { + let serverResponse; + + describe('with no body', function () { + beforeEach(function() { + serverResponse = { + body: null, + }; + }); + + it('does not return any bids', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(0); + }); + }); + + describe('with action=hide', function () { + beforeEach(function() { + serverResponse = { + body: { + action: 'Hide', + }, + }; + }); + + it('does not return any bids', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(0); + }); + }); + + describe('with a valid campaign', function () { + beforeEach(function() { + serverResponse = { + body: { + "targetData": { + "deviceType": "desktop", + "parent_org": "prebid-test", + "cpm": 3, + "target_id": "a9b430ab-1f62-46f3-9d3a-1ece821dca61", + "deviceInfo": { + "os": { + "name": "Mac OS", + "version": "10.15.7" + }, + "engine": { + "name": "Blink", + "version": "130.0.0.0" + }, + "browser": { + "major": "130", + "name": "Chrome", + "version": "130.0.0.0" + }, + "cpu": {}, + "ua": "Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/130.0.0.0 Safari\/537.36", + "device": { + "vendor": "Apple", + "model": "Macintosh" + } + }, + "widget_type": "card", + "isInApp": false, + "partner_id": "prebid-test", + "countryCode": "US", + "metroCode": "602", + "hasReadMore": false, + "region": "IL", + "campaign_id": "a9b430ab-1f62-46f3-9d3a-1ece821dca61" + }, + "action": "Default", + "url": "https%3A%2F%2Fpublicgood.com%2F", + "content": { + "parent_org": "prebid-test", + "rules_match_info": null, + "content_id": 20446189, + "all_matches": [ + { + "analysis_tag": "a9b430ab-1f62-46f3-9d3a-1ece821dca61", + "guid": "a9b430ab-1f62-46f3-9d3a-1ece821dca61" + } + ], + "is_override": true, + "cid_match_type": "", + "target_id": "a9b430ab-1f62-46f3-9d3a-1ece821dca61", + "url_id": 128113623, + "title": "Public Good", + "hide": false, + "partner_id": "prebid-test", + "qa_verified": true, + "tag": "a9b430ab-1f62-46f3-9d3a-1ece821dca61", + "is_filter": false + } + } + }; + }); + + it('returns a complete bid', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(1); + expect(bids[0].cpm).to.equal(3); + expect(bids[0].width).to.equal(320); + expect(bids[0].height).to.equal(470); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].ad).to.have.string('data-pgs-partner-id="prebid-test"'); + }); + }); + }); +}); diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js index b890a9d575d..c0c3367881d 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -686,6 +686,47 @@ describe('pubmatic analytics adapter', function () { expect(trackerData.rd.ctr).to.equal('US'); }); + it('Logger: does not include identity partners when getUserIds is not a function', function () { + this.timeout(5000); + + // Make sure getUserIds is NOT a function so that getListOfIdentityPartners returns early + const namespace = getGlobal(); + const originalGetUserIds = namespace.getUserIds; + namespace.getUserIds = null; + + sandbox.stub(getGlobal(), 'getHighestCpmBids').callsFake(() => { + return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]]; + }); + + config.setConfig({ + testGroupId: 15 + }); + + // Standard event flow to trigger the logger + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + + clock.tick(2000 + 1000); // wait for SEND_TIMEOUT + + expect(requests.length).to.equal(1); // only the logger should fire + const request = requests[0]; + expect(request.url).to.equal('https://t.pubmatic.com/wl?v=1&psrc=web'); + + const data = getLoggerJsonFromRequest(request.requestBody); + + // fd should exist, but bdv (identity partners) should NOT be present + expect(data).to.have.property('fd'); + expect(data.fd.bdv).to.be.undefined; + + // restore original getUserIds to avoid side effects on other tests + namespace.getUserIds = originalGetUserIds; + }); + it('Logger: log floor fields when prebids floor shows setConfig in location property', function () { const BID_REQUESTED_COPY = utils.deepClone(MOCK.BID_REQUESTED); BID_REQUESTED_COPY['bids'][1]['floorData']['location'] = 'fetch'; diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 3f2fb212381..2ed65dd7311 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -501,6 +501,23 @@ describe('PubMatic adapter', () => { expect(imp[0].ext.pbcode).to.equal(validBidRequests[0].adUnitCode); }); + it('should not include ae or igs in imp.ext', () => { + const bidWithAe = utils.deepClone(validBidRequests[0]); + bidWithAe.ortb2Imp = bidWithAe.ortb2Imp || {}; + bidWithAe.ortb2Imp.ext = bidWithAe.ortb2Imp.ext || {}; + bidWithAe.ortb2Imp.ext.ae = 1; + bidWithAe.ortb2Imp.ext.igs = { ae: 1, biddable: 1 }; + bidWithAe.ortb2Imp.ext.paapi = { requestedSize: { width: 300, height: 250 } }; + + const req = spec.buildRequests([bidWithAe], bidderRequest); + const { imp } = req?.data; + expect(imp).to.be.an('array'); + expect(imp[0]).to.have.property('ext'); + expect(imp[0].ext).to.not.have.property('ae'); + expect(imp[0].ext).to.not.have.property('igs'); + expect(imp[0].ext).to.not.have.property('paapi'); + }); + it('should add bidfloor if kadfloor is present in parameters', () => { const request = spec.buildRequests(validBidRequests, bidderRequest); const { imp } = request?.data; @@ -1174,16 +1191,6 @@ describe('PubMatic adapter', () => { }); }); - describe('FLEDGE', () => { - it('should not send imp.ext.ae when FLEDGE is disabled, ', () => { - const request = spec.buildRequests(validBidRequests, bidderRequest); - expect(request.data).to.have.property('imp'); - expect(request.data.imp).to.be.an('array'); - expect(request.data.imp[0]).to.have.property('ext'); - expect(request.data.imp[0].ext).to.not.have.property('ae'); - }); - }) - describe('cpm adjustment', () => { beforeEach(() => { global.cpmAdjustment = {}; diff --git a/test/spec/modules/pubmaticRtdProvider_spec.js b/test/spec/modules/pubmaticRtdProvider_spec.js index 830fd585ddc..19536b5ebd4 100644 --- a/test/spec/modules/pubmaticRtdProvider_spec.js +++ b/test/spec/modules/pubmaticRtdProvider_spec.js @@ -1,74 +1,88 @@ import { expect } from 'chai'; -import * as priceFloors from '../../../modules/priceFloors.js'; -import * as utils from '../../../src/utils.js'; -import * as suaModule from '../../../src/fpd/sua.js'; -import { config as conf } from '../../../src/config.js'; -import * as hook from '../../../src/hook.js'; -import * as prebidGlobal from '../../../src/prebidGlobal.js'; -import { - registerSubModule, pubmaticSubmodule, getFloorsConfig, fetchData, - getCurrentTimeOfDay, getBrowserType, getOs, getDeviceType, getCountry, getUtm, getBidder, _country, - _profileConfigs, _floorsData, defaultValueTemplate, withTimeout, configMerged, - getProfileConfigs, setProfileConfigs, getTargetingData -} from '../../../modules/pubmaticRtdProvider.js'; import sinon from 'sinon'; +import * as utils from '../../../src/utils.js'; +import * as pubmaticRtdProvider from '../../../modules/pubmaticRtdProvider.js'; +import { FloorProvider } from '../../../libraries/pubmaticUtils/plugins/floorProvider.js'; +import { UnifiedPricingRule } from '../../../libraries/pubmaticUtils/plugins/unifiedPricingRule.js'; describe('Pubmatic RTD Provider', () => { let sandbox; + let fetchStub; + let logErrorStub; + let originalPluginManager; + let originalConfigJsonManager; + let pluginManagerStub; + let configJsonManagerStub; beforeEach(() => { sandbox = sinon.createSandbox(); - sandbox.stub(conf, 'getConfig').callsFake(() => { - return { - floors: { - 'enforcement': { - 'floorDeals': true, - 'enforceJS': true - } - }, - realTimeData: { - auctionDelay: 100 - } - }; + fetchStub = sandbox.stub(window, 'fetch'); + logErrorStub = sinon.stub(utils, 'logError'); + + // Store original implementations + originalPluginManager = Object.assign({}, pubmaticRtdProvider.pluginManager); + originalConfigJsonManager = Object.assign({}, pubmaticRtdProvider.configJsonManager); + + // Create stubs + pluginManagerStub = { + initialize: sinon.stub().resolves(), + executeHook: sinon.stub().resolves(), + register: sinon.stub() + }; + + configJsonManagerStub = { + fetchConfig: sinon.stub().resolves(true), + getYMConfig: sinon.stub(), + getConfigByName: sinon.stub(), + country: 'IN' + }; + + // Replace exported objects with stubs + Object.keys(pluginManagerStub).forEach(key => { + pubmaticRtdProvider.pluginManager[key] = pluginManagerStub[key]; }); + + Object.keys(configJsonManagerStub).forEach(key => { + if (key === 'country') { + Object.defineProperty(pubmaticRtdProvider.configJsonManager, key, { + get: () => configJsonManagerStub[key] + }); + } else { + pubmaticRtdProvider.configJsonManager[key] = configJsonManagerStub[key]; + } + }); + + // Reset _ymConfigPromise for each test + pubmaticRtdProvider.setYmConfigPromise(Promise.resolve()); }); afterEach(() => { sandbox.restore(); - }); + logErrorStub.restore(); - describe('registerSubModule', () => { - it('should register RTD submodule provider', () => { - const submoduleStub = sinon.stub(hook, 'submodule'); - registerSubModule(); - assert(submoduleStub.calledOnceWith('realTimeData', pubmaticSubmodule)); - submoduleStub.restore(); + // Restore original implementations + Object.keys(originalPluginManager).forEach(key => { + pubmaticRtdProvider.pluginManager[key] = originalPluginManager[key]; }); - }); - describe('submodule', () => { - describe('name', () => { - it('should be pubmatic', () => { - expect(pubmaticSubmodule.name).to.equal('pubmatic'); - }); + Object.keys(originalConfigJsonManager).forEach(key => { + if (key === 'country') { + Object.defineProperty(pubmaticRtdProvider.configJsonManager, 'country', { + get: () => originalConfigJsonManager[key] + }); + } else { + pubmaticRtdProvider.configJsonManager[key] = originalConfigJsonManager[key]; + } }); }); describe('init', () => { - let logErrorStub; - let continueAuctionStub; - - const getConfig = () => ({ + const validConfig = { params: { publisherId: 'test-publisher-id', profileId: 'test-profile-id' - }, - }); - - beforeEach(() => { - logErrorStub = sandbox.stub(utils, 'logError'); - continueAuctionStub = sandbox.stub(priceFloors, 'continueAuction'); - }); + } + }; it('should return false if publisherId is missing', () => { const config = { @@ -76,26 +90,33 @@ describe('Pubmatic RTD Provider', () => { profileId: 'test-profile-id' } }; - expect(pubmaticSubmodule.init(config)).to.be.false; + const result = pubmaticRtdProvider.pubmaticSubmodule.init(config); + expect(result).to.be.false; + expect(logErrorStub.calledOnce).to.be.true; + expect(logErrorStub.firstCall.args[0]).to.equal(`${pubmaticRtdProvider.CONSTANTS.LOG_PRE_FIX} Missing publisher Id.`); }); - it('should return false if profileId is missing', () => { + it('should accept numeric publisherId by converting to string', () => { const config = { params: { - publisherId: 'test-publisher-id' + publisherId: 123, + profileId: 'test-profile-id' } }; - expect(pubmaticSubmodule.init(config)).to.be.false; + const result = pubmaticRtdProvider.pubmaticSubmodule.init(config); + expect(result).to.be.true; }); - it('should accept numeric publisherId by converting to string', () => { + it('should return false if profileId is missing', () => { const config = { params: { - publisherId: 123, - profileId: 'test-profile-id' + publisherId: 'test-publisher-id' } }; - expect(pubmaticSubmodule.init(config)).to.be.true; + const result = pubmaticRtdProvider.pubmaticSubmodule.init(config); + expect(result).to.be.false; + expect(logErrorStub.calledOnce).to.be.true; + expect(logErrorStub.firstCall.args[0]).to.equal(`${pubmaticRtdProvider.CONSTANTS.LOG_PRE_FIX} Missing profile Id.`); }); it('should accept numeric profileId by converting to string', () => { @@ -105,1139 +126,199 @@ describe('Pubmatic RTD Provider', () => { profileId: 345 } }; - expect(pubmaticSubmodule.init(config)).to.be.true; + const result = pubmaticRtdProvider.pubmaticSubmodule.init(config); + expect(result).to.be.true; }); - it('should initialize successfully with valid config', () => { - expect(pubmaticSubmodule.init(getConfig())).to.be.true; - }); + it('should initialize successfully with valid config', async () => { + configJsonManagerStub.fetchConfig.resolves(true); + pluginManagerStub.initialize.resolves(); - it('should handle empty config object', () => { - expect(pubmaticSubmodule.init({})).to.be.false; - expect(logErrorStub.calledWith(sinon.match(/Missing publisher Id/))).to.be.true; - }); + const result = pubmaticRtdProvider.pubmaticSubmodule.init(validConfig); + expect(result).to.be.true; + expect(configJsonManagerStub.fetchConfig.calledOnce).to.be.true; + expect(configJsonManagerStub.fetchConfig.firstCall.args[0]).to.equal('test-publisher-id'); + expect(configJsonManagerStub.fetchConfig.firstCall.args[1]).to.equal('test-profile-id'); - it('should return false if continueAuction is not a function', () => { - continueAuctionStub.value(undefined); - expect(pubmaticSubmodule.init(getConfig())).to.be.false; - expect(logErrorStub.calledWith(sinon.match(/continueAuction is not a function/))).to.be.true; - }); - }); - - describe('getCurrentTimeOfDay', () => { - let clock; - - beforeEach(() => { - clock = sandbox.useFakeTimers(new Date('2024-01-01T12:00:00')); // Set fixed time for testing - }); - - afterEach(() => { - clock.restore(); - }); - - const testTimes = [ - { hour: 6, expected: 'morning' }, - { hour: 13, expected: 'afternoon' }, - { hour: 18, expected: 'evening' }, - { hour: 22, expected: 'night' }, - { hour: 4, expected: 'night' } - ]; - - testTimes.forEach(({ hour, expected }) => { - it(`should return ${expected} at ${hour}:00`, () => { - clock.setSystemTime(new Date().setHours(hour)); - const result = getCurrentTimeOfDay(); - expect(result).to.equal(expected); - }); - }); - }); - - describe('getBrowserType', () => { - let userAgentStub, getLowEntropySUAStub; - - const USER_AGENTS = { - chrome: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - firefox: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0', - edge: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edg/91.0.864.67 Safari/537.36', - safari: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.6 Mobile/15E148 Safari/604.1', - ie: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)', - opera: 'Opera/9.80 (Windows NT 6.1; WOW64) Presto/2.12.388 Version/12.16', - unknown: 'UnknownBrowser/1.0' - }; - - beforeEach(() => { - userAgentStub = sandbox.stub(navigator, 'userAgent'); - getLowEntropySUAStub = sandbox.stub(suaModule, 'getLowEntropySUA').returns(undefined); - }); - - afterEach(() => { - userAgentStub.restore(); - getLowEntropySUAStub.restore(); + // Wait for promise to resolve + await pubmaticRtdProvider.getYmConfigPromise(); + expect(pluginManagerStub.initialize.firstCall.args[0]).to.equal(pubmaticRtdProvider.configJsonManager); }); - it('should detect Chrome', () => { - userAgentStub.value(USER_AGENTS.chrome); - expect(getBrowserType()).to.equal('9'); - }); - - it('should detect Firefox', () => { - userAgentStub.value(USER_AGENTS.firefox); - expect(getBrowserType()).to.equal('12'); - }); - - it('should detect Edge', () => { - userAgentStub.value(USER_AGENTS.edge); - expect(getBrowserType()).to.equal('2'); - }); + it('should handle config fetch error gracefully', async () => { + configJsonManagerStub.fetchConfig.resolves(false); - it('should detect Internet Explorer', () => { - userAgentStub.value(USER_AGENTS.ie); - expect(getBrowserType()).to.equal('4'); - }); - - it('should detect Opera', () => { - userAgentStub.value(USER_AGENTS.opera); - expect(getBrowserType()).to.equal('3'); - }); - - it('should return 0 for unknown browser', () => { - userAgentStub.value(USER_AGENTS.unknown); - expect(getBrowserType()).to.equal('0'); - }); - - it('should return -1 when userAgent is null', () => { - userAgentStub.value(null); - expect(getBrowserType()).to.equal('-1'); - }); - }); - - describe('Utility functions', () => { - it('should set browser correctly', () => { - expect(getBrowserType()).to.be.a('string'); - }); - - it('should set OS correctly', () => { - expect(getOs()).to.be.a('string'); - }); - - it('should set device type correctly', () => { - expect(getDeviceType()).to.be.a('string'); - }); + const result = pubmaticRtdProvider.pubmaticSubmodule.init(validConfig); + expect(result).to.be.true; - it('should set time of day correctly', () => { - expect(getCurrentTimeOfDay()).to.be.a('string'); - }); - - it('should set country correctly', () => { - expect(getCountry()).to.satisfy(value => typeof value === 'string' || value === undefined); - }); - - it('should set UTM correctly', () => { - expect(getUtm()).to.be.a('string'); - expect(getUtm()).to.be.oneOf(['0', '1']); - }); - - it('should extract bidder correctly', () => { - expect(getBidder({ bidder: 'pubmatic' })).to.equal('pubmatic'); - expect(getBidder({})).to.be.undefined; - expect(getBidder(null)).to.be.undefined; - expect(getBidder(undefined)).to.be.undefined; - }); - }); - - describe('getFloorsConfig', () => { - let floorsData, profileConfigs; - let sandbox; - let logErrorStub; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - logErrorStub = sandbox.stub(utils, 'logError'); - floorsData = { - "currency": "USD", - "floorProvider": "PM", - "floorsSchemaVersion": 2, - "modelGroups": [ - { - "modelVersion": "M_1", - "modelWeight": 100, - "schema": { - "fields": [ - "domain" - ] - }, - "values": { - "*": 2.00 - } - } - ], - "skipRate": 0 - }; - profileConfigs = { - 'plugins': { - 'dynamicFloors': { - 'enabled': true, - 'config': { - 'enforcement': { - 'floorDeals': false, - 'enforceJS': false - }, - 'floorMin': 0.1111, - 'skipRate': 11, - 'defaultValues': { - "*|*": 0.2 - } - } - } - } + try { + await pubmaticRtdProvider.getYmConfigPromise(); + } catch (e) { + expect(e.message).to.equal('Failed to fetch configuration'); } - }); - - afterEach(() => { - sandbox.restore(); - }); - - it('should return correct config structure', () => { - const result = getFloorsConfig(floorsData, profileConfigs); - - expect(result.floors).to.be.an('object'); - expect(result.floors).to.be.an('object'); - expect(result.floors).to.have.property('enforcement'); - expect(result.floors.enforcement).to.have.property('floorDeals', false); - expect(result.floors.enforcement).to.have.property('enforceJS', false); - expect(result.floors).to.have.property('floorMin', 0.1111); - - // Verify the additionalSchemaFields structure - expect(result.floors.additionalSchemaFields).to.have.all.keys([ - 'deviceType', - 'timeOfDay', - 'browser', - 'os', - 'country', - 'utm', - 'bidder' - ]); - - Object.values(result.floors.additionalSchemaFields).forEach(field => { - expect(field).to.be.a('function'); - }); - }); - - it('should return undefined when plugin is disabled', () => { - profileConfigs.plugins.dynamicFloors.enabled = false; - const result = getFloorsConfig(floorsData, profileConfigs); - - expect(result).to.equal(undefined); - }); - - it('should initialise default values to empty object when not available', () => { - profileConfigs.plugins.dynamicFloors.config.defaultValues = undefined; - floorsData = undefined; - const result = getFloorsConfig(floorsData, profileConfigs); - - expect(result.floors.data).to.have.property('currency', 'USD'); - expect(result.floors.data).to.have.property('skipRate', 11); - expect(result.floors.data.schema).to.deep.equal(defaultValueTemplate.schema); - expect(result.floors.data.value).to.deep.equal(defaultValueTemplate.value); - }); - - it('should replace skipRate from config to data when avaialble', () => { - const result = getFloorsConfig(floorsData, profileConfigs); - - expect(result.floors.data).to.have.property('skipRate', 11); - }); - - it('should not replace skipRate from config to data when not avaialble', () => { - delete profileConfigs.plugins.dynamicFloors.config.skipRate; - const result = getFloorsConfig(floorsData, profileConfigs); - - expect(result.floors.data).to.have.property('skipRate', 0); - }); - it('should maintain correct function references', () => { - const result = getFloorsConfig(floorsData, profileConfigs); - - expect(result.floors.additionalSchemaFields.deviceType).to.equal(getDeviceType); - expect(result.floors.additionalSchemaFields.timeOfDay).to.equal(getCurrentTimeOfDay); - expect(result.floors.additionalSchemaFields.browser).to.equal(getBrowserType); - expect(result.floors.additionalSchemaFields.os).to.equal(getOs); - expect(result.floors.additionalSchemaFields.country).to.equal(getCountry); - expect(result.floors.additionalSchemaFields.utm).to.equal(getUtm); - expect(result.floors.additionalSchemaFields.bidder).to.equal(getBidder); - }); - - it('should log error when profileConfigs is not an object', () => { - profileConfigs = 'invalid'; - const result = getFloorsConfig(floorsData, profileConfigs); - expect(result).to.be.undefined; - expect(logErrorStub.calledWith(sinon.match(/profileConfigs is not an object or is empty/))).to.be.true; + expect(pluginManagerStub.initialize.called).to.be.false; }); }); - describe('fetchData for configs', () => { - let logErrorStub; - let fetchStub; - let confStub; - - beforeEach(() => { - logErrorStub = sandbox.stub(utils, 'logError'); - fetchStub = sandbox.stub(window, 'fetch'); - confStub = sandbox.stub(conf, 'setConfig'); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it('should successfully fetch profile configs', async () => { - const mockApiResponse = { - "profileName": "profie name", - "desc": "description", - "plugins": { - "dynamicFloors": { - "enabled": false - } + describe('getBidRequestData', () => { + const reqBidsConfigObj = { + ortb2Fragments: { + bidder: {} + }, + adUnits: [ + { + code: 'div-1', + bids: [{ bidder: 'pubmatic', params: {} }] + }, + { + code: 'div-2', + bids: [{ bidder: 'pubmatic', params: {} }] } - }; - - fetchStub.resolves(new Response(JSON.stringify(mockApiResponse), { status: 200 })); - - const result = await fetchData('1234', '123', 'CONFIGS'); - expect(result).to.deep.equal(mockApiResponse); - }); - - it('should log error when JSON parsing fails', async () => { - fetchStub.resolves(new Response('Invalid JSON', { status: 200 })); - - await fetchData('1234', '123', 'CONFIGS'); - expect(logErrorStub.calledWith(sinon.match(/Error while fetching\s*CONFIGS/))).to.be.true; - }); - - it('should log error when response is not ok', async () => { - fetchStub.resolves(new Response(null, { status: 500 })); - - await fetchData('1234', '123', 'CONFIGS'); - expect(logErrorStub.calledWith(sinon.match(/Error while fetching\s*CONFIGS/))).to.be.true; - }); - - it('should log error on network failure', async () => { - fetchStub.rejects(new Error('Network Error')); - - await fetchData('1234', '123', 'CONFIGS'); - expect(logErrorStub.called).to.be.true; - expect(logErrorStub.calledWith(sinon.match(/Error while fetching\s*CONFIGS/))).to.be.true; - }); - }); - - describe('fetchData for floors', () => { - let logErrorStub; - let fetchStub; - let confStub; + ] + }; + let callback; beforeEach(() => { - logErrorStub = sandbox.stub(utils, 'logError'); - fetchStub = sandbox.stub(window, 'fetch'); - confStub = sandbox.stub(conf, 'setConfig'); - global._country = undefined; - }); - - afterEach(() => { - sandbox.restore(); + callback = sinon.stub(); + pubmaticRtdProvider.setYmConfigPromise(Promise.resolve()); }); - it('should successfully fetch and parse floor rules', async () => { - const mockApiResponse = { - data: { - currency: 'USD', - modelGroups: [], - values: {} - } - }; + it('should call pluginManager executeHook with correct parameters', (done) => { + pluginManagerStub.executeHook.resolves(); - fetchStub.resolves(new Response(JSON.stringify(mockApiResponse), { status: 200, headers: { 'country_code': 'US' } })); + pubmaticRtdProvider.pubmaticSubmodule.getBidRequestData(reqBidsConfigObj, callback); - const result = await fetchData('1234', '123', 'FLOORS'); - expect(result).to.deep.equal(mockApiResponse); - expect(_country).to.equal('US'); + setTimeout(() => { + expect(pluginManagerStub.executeHook.calledOnce).to.be.true; + expect(pluginManagerStub.executeHook.firstCall.args[0]).to.equal('processBidRequest'); + expect(pluginManagerStub.executeHook.firstCall.args[1]).to.deep.equal(reqBidsConfigObj); + expect(callback.calledOnce).to.be.true; + done(); + }, 0); }); - it('should correctly extract the first unique country code from response headers', async () => { - fetchStub.resolves(new Response(JSON.stringify({}), { - status: 200, - headers: { 'country_code': 'US,IN,US' } - })); + it('should add country information to ORTB2', (done) => { + pluginManagerStub.executeHook.resolves(); - await fetchData('1234', '123', 'FLOORS'); - expect(_country).to.equal('US'); - }); - - it('should set _country to undefined if country_code header is missing', async () => { - fetchStub.resolves(new Response(JSON.stringify({}), { - status: 200 - })); - - await fetchData('1234', '123', 'FLOORS'); - expect(_country).to.be.undefined; - }); + pubmaticRtdProvider.pubmaticSubmodule.getBidRequestData(reqBidsConfigObj, callback); - it('should log error when JSON parsing fails', async () => { - fetchStub.resolves(new Response('Invalid JSON', { status: 200 })); - - await fetchData('1234', '123', 'FLOORS'); - expect(logErrorStub.calledWith(sinon.match(/Error while fetching\s*FLOORS/))).to.be.true; - }); - - it('should log error when response is not ok', async () => { - fetchStub.resolves(new Response(null, { status: 500 })); - - await fetchData('1234', '123', 'FLOORS'); - expect(logErrorStub.calledWith(sinon.match(/Error while fetching\s*FLOORS/))).to.be.true; - }); - - it('should log error on network failure', async () => { - fetchStub.rejects(new Error('Network Error')); - - await fetchData('1234', '123', 'FLOORS'); - expect(logErrorStub.called).to.be.true; - expect(logErrorStub.calledWith(sinon.match(/Error while fetching\s*FLOORS/))).to.be.true; - }); - }); - - describe('getBidRequestData', function () { - let callback, continueAuctionStub, mergeDeepStub, logErrorStub; - - const reqBidsConfigObj = { - adUnits: [{ code: 'ad-slot-code-0' }], - auctionId: 'auction-id-0', - ortb2Fragments: { - bidder: { + setTimeout(() => { + expect(reqBidsConfigObj.ortb2Fragments.bidder[pubmaticRtdProvider.CONSTANTS.SUBMODULE_NAME]).to.deep.equal({ user: { ext: { - ctr: 'US', + ctr: 'IN' } } - } - } - }; - - const ortb2 = { - user: { - ext: { - ctr: 'US', - } - } - } - - const hookConfig = { - reqBidsConfigObj, - context: this, - nextFn: () => true, - haveExited: false, - timer: null - }; - - beforeEach(() => { - callback = sinon.spy(); - continueAuctionStub = sandbox.stub(priceFloors, 'continueAuction'); - logErrorStub = sandbox.stub(utils, 'logError'); - - global.configMergedPromise = Promise.resolve(); - }); - - afterEach(() => { - sandbox.restore(); // Restore all stubs/spies - }); - - it('should call continueAuction with correct hookConfig', async function () { - configMerged(); - await pubmaticSubmodule.getBidRequestData(reqBidsConfigObj, callback); - - expect(continueAuctionStub.called).to.be.true; - expect(continueAuctionStub.firstCall.args[0]).to.have.property('reqBidsConfigObj', reqBidsConfigObj); - expect(continueAuctionStub.firstCall.args[0]).to.have.property('haveExited', false); - }); - - // it('should merge country data into ortb2Fragments.bidder', async function () { - // configMerged(); - // global._country = 'US'; - // pubmaticSubmodule.getBidRequestData(reqBidsConfigObj, callback); - - // expect(reqBidsConfigObj.ortb2Fragments.bidder).to.have.property('pubmatic'); - // // expect(reqBidsConfigObj.ortb2Fragments.bidder.pubmatic.user.ext.ctr).to.equal('US'); - // }); - - it('should call callback once after execution', async function () { - configMerged(); - await pubmaticSubmodule.getBidRequestData(reqBidsConfigObj, callback); - - expect(callback.called).to.be.true; + }); + done(); + }, 0); }); }); - describe('withTimeout', function () { - it('should resolve with the original promise value if it resolves before the timeout', async function () { - const promise = new Promise((resolve) => setTimeout(() => resolve('success'), 50)); - const result = await withTimeout(promise, 100); - expect(result).to.equal('success'); - }); - - it('should resolve with undefined if the promise takes longer than the timeout', async function () { - const promise = new Promise((resolve) => setTimeout(() => resolve('success'), 200)); - const result = await withTimeout(promise, 100); - expect(result).to.be.undefined; - }); - - it('should properly handle rejected promises', async function () { - const promise = new Promise((resolve, reject) => setTimeout(() => reject(new Error('Failure')), 50)); - try { - await withTimeout(promise, 100); - } catch (error) { - expect(error.message).to.equal('Failure'); + describe('getTargetingData', () => { + const adUnitCodes = ['div-1', 'div-2']; + const config = { + params: { + publisherId: 'test-publisher-id', + profileId: 'test-profile-id' } - }); - - it('should resolve with undefined if the original promise is rejected but times out first', async function () { - const promise = new Promise((resolve, reject) => setTimeout(() => reject(new Error('Failure')), 200)); - const result = await withTimeout(promise, 100); - expect(result).to.be.undefined; - }); + }; + const userConsent = {}; + const auction = {}; + const unifiedPricingRule = { + 'div-1': { key1: 'value1' }, + 'div-2': { key2: 'value2' } + }; - it('should clear the timeout when the promise resolves before the timeout', async function () { - const clock = sinon.useFakeTimers(); - const clearTimeoutSpy = sinon.spy(global, 'clearTimeout'); + it('should call pluginManager executeHook with correct parameters', () => { + pluginManagerStub.executeHook.returns(unifiedPricingRule); - const promise = new Promise((resolve) => setTimeout(() => resolve('success'), 50)); - const resultPromise = withTimeout(promise, 100); + const result = pubmaticRtdProvider.getTargetingData(adUnitCodes, config, userConsent, auction); - clock.tick(50); - await resultPromise; + expect(pluginManagerStub.executeHook.calledOnce).to.be.true; + expect(pluginManagerStub.executeHook.firstCall.args[0]).to.equal('getTargeting'); + expect(pluginManagerStub.executeHook.firstCall.args[1]).to.equal(adUnitCodes); + expect(pluginManagerStub.executeHook.firstCall.args[2]).to.equal(config); + expect(pluginManagerStub.executeHook.firstCall.args[3]).to.equal(userConsent); + expect(pluginManagerStub.executeHook.firstCall.args[4]).to.equal(auction); + expect(result).to.equal(unifiedPricingRule); + }); - expect(clearTimeoutSpy.called).to.be.true; + it('should return empty object if no targeting data', () => { + pluginManagerStub.executeHook.returns({}); - clearTimeoutSpy.restore(); - clock.restore(); + const result = pubmaticRtdProvider.getTargetingData(adUnitCodes, config, userConsent, auction); + expect(result).to.deep.equal({}); }); }); - describe('getTargetingData', function () { - let sandbox; - let logInfoStub; + describe('ConfigJsonManager', () => { + let configManager; beforeEach(() => { - sandbox = sinon.createSandbox(); - logInfoStub = sandbox.stub(utils, 'logInfo'); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it('should return empty object when profileConfigs is undefined', function () { - // Store the original value to restore it later - const originalProfileConfigs = getProfileConfigs(); - // Set profileConfigs to undefined - setProfileConfigs(undefined); - - const adUnitCodes = ['test-ad-unit']; - const config = {}; - const userConsent = {}; - const auction = {}; - - const result = getTargetingData(adUnitCodes, config, userConsent, auction); - - // Restore the original value - setProfileConfigs(originalProfileConfigs); - - expect(result).to.deep.equal({}); - expect(logInfoStub.calledWith(sinon.match(/pmTargetingKeys is disabled or profileConfigs is undefined/))).to.be.true; + configManager = pubmaticRtdProvider.ConfigJsonManager(); }); - it('should return empty object when pmTargetingKeys.enabled is false', function () { - // Create profileConfigs with pmTargetingKeys.enabled set to false - const profileConfigsMock = { - plugins: { - dynamicFloors: { - pmTargetingKeys: { - enabled: false - } - } - } + it('should fetch config successfully', async () => { + const mockResponse = { + ok: true, + headers: { + get: sinon.stub().withArgs('country_code').returns('US') + }, + json: sinon.stub().resolves({ plugins: { test: { enabled: true } } }) }; - // Store the original value to restore it later - const originalProfileConfigs = getProfileConfigs(); - // Set profileConfigs to our mock - setProfileConfigs(profileConfigsMock); - - const adUnitCodes = ['test-ad-unit']; - const config = {}; - const userConsent = {}; - const auction = {}; + fetchStub.resolves(mockResponse); - const result = getTargetingData(adUnitCodes, config, userConsent, auction); + const result = await configManager.fetchConfig('pub-123', 'profile-456'); - // Restore the original value - setProfileConfigs(originalProfileConfigs); - - expect(result).to.deep.equal({}); - expect(logInfoStub.calledWith(sinon.match(/pmTargetingKeys is disabled or profileConfigs is undefined/))).to.be.true; + expect(result).to.be.true; + expect(fetchStub.calledOnce).to.be.true; + expect(fetchStub.firstCall.args[0]).to.equal(`${pubmaticRtdProvider.CONSTANTS.ENDPOINTS.BASEURL}/pub-123/profile-456/${pubmaticRtdProvider.CONSTANTS.ENDPOINTS.CONFIGS}`); + expect(configManager.country).to.equal('US'); }); - it('should set pm_ym_flrs to 0 when no RTD floor is applied to any bid', function () { - // Create profileConfigs with pmTargetingKeys.enabled set to true - const profileConfigsMock = { - plugins: { - dynamicFloors: { - pmTargetingKeys: { - enabled: true - } - } - } - }; - - // Store the original value to restore it later - const originalProfileConfigs = getProfileConfigs(); - // Set profileConfigs to our mock - setProfileConfigs(profileConfigsMock); - - // Create multiple ad unit codes to test - const adUnitCodes = ['ad-unit-1', 'ad-unit-2']; - const config = {}; - const userConsent = {}; - - // Create a mock auction object with bids that don't have RTD floors applied - // This tests several scenarios where RTD floor is not applied: - // 1. No floorData - // 2. floorData but floorProvider is not 'PM' - // 3. floorData with floorProvider 'PM' but skipped is true - const auction = { - adUnits: [ - { - code: 'ad-unit-1', - bids: [ - { bidder: 'bidderA' }, // No floorData - { bidder: 'bidderB', floorData: { floorProvider: 'OTHER' } } // Not PM provider - ] - }, - { - code: 'ad-unit-2', - bids: [ - { bidder: 'bidderC', floorData: { floorProvider: 'PM', skipped: true } } // PM but skipped - ] - } - ], - bidsReceived: [ - { adUnitCode: 'ad-unit-1', bidder: 'bidderA' }, - { adUnitCode: 'ad-unit-1', bidder: 'bidderB', floorData: { floorProvider: 'OTHER' } }, - { adUnitCode: 'ad-unit-2', bidder: 'bidderC', floorData: { floorProvider: 'PM', skipped: true } } - ] + it('should handle missing country_code header and set country to undefined', async () => { + const mockResponse = { + ok: true, + headers: { + get: sinon.stub().withArgs('country_code').returns(null) + }, + json: sinon.stub().resolves({ plugins: { test: { enabled: true } } }) }; - const result = getTargetingData(adUnitCodes, config, userConsent, auction); + fetchStub.resolves(mockResponse); - // Restore the original value - setProfileConfigs(originalProfileConfigs); + const result = await configManager.fetchConfig('pub-123', 'profile-456'); - // Verify that for each ad unit code, only pm_ym_flrs is set to 0 - expect(result['ad-unit-1']).to.have.property('pm_ym_flrs', 0); - expect(result['ad-unit-2']).to.have.property('pm_ym_flrs', 0); + expect(result).to.be.true; + expect(fetchStub.calledOnce).to.be.true; + expect(fetchStub.firstCall.args[0]).to.equal(`${pubmaticRtdProvider.CONSTANTS.ENDPOINTS.BASEURL}/pub-123/profile-456/${pubmaticRtdProvider.CONSTANTS.ENDPOINTS.CONFIGS}`); + expect(configManager.country).to.be.undefined; }); - it('should set pm_ym_flrs to 1 when RTD floor is applied to a bid', function () { - // Create profileConfigs with pmTargetingKeys.enabled set to true - const profileConfigsMock = { - plugins: { - dynamicFloors: { - pmTargetingKeys: { - enabled: true - } - } - } - }; - - // Store the original value to restore it later - const originalProfileConfigs = getProfileConfigs(); - // Set profileConfigs to our mock - setProfileConfigs(profileConfigsMock); - - // Create multiple ad unit codes to test - const adUnitCodes = ['ad-unit-1', 'ad-unit-2']; - const config = {}; - const userConsent = {}; + it('should handle fetch errors', async () => { + fetchStub.rejects(new Error('Network error')); - // Create a mock auction object with bids that have RTD floors applied - const auction = { - adUnits: [ - { - code: 'ad-unit-1', - bids: [ - { bidder: 'bidderA', floorData: { floorProvider: 'PM', skipped: false } }, - { bidder: 'bidderB', floorData: { floorProvider: 'PM', skipped: false } } - ] - }, - { - code: 'ad-unit-2', - bids: [ - { bidder: 'bidderC', floorData: { floorProvider: 'PM', skipped: false } }, - { bidder: 'bidderD', floorData: { floorProvider: 'PM', skipped: false } } - ] - } - ], - bidsReceived: [ - { adUnitCode: 'ad-unit-1', bidder: 'bidderA', floorData: { floorProvider: 'PM', skipped: false } }, - { adUnitCode: 'ad-unit-1', bidder: 'bidderB', floorData: { floorProvider: 'PM', skipped: false } }, - { adUnitCode: 'ad-unit-2', bidder: 'bidderC', floorData: { floorProvider: 'PM', skipped: false } }, - { adUnitCode: 'ad-unit-2', bidder: 'bidderD', floorData: { floorProvider: 'PM', skipped: false } } - ] - }; + const result = await configManager.fetchConfig('pub-123', 'profile-456'); - const result = getTargetingData(adUnitCodes, config, userConsent, auction); - - // Restore the original value - setProfileConfigs(originalProfileConfigs); - - // Verify that for each ad unit code, pm_ym_flrs is set to 1 - expect(result['ad-unit-1']).to.have.property('pm_ym_flrs', 1); - expect(result['ad-unit-2']).to.have.property('pm_ym_flrs', 1); + expect(result).to.be.null; + expect(logErrorStub.calledOnce).to.be.true; + expect(logErrorStub.firstCall.args[0]).to.include('Error while fetching config'); }); - it('should set different targeting keys for winning bids (status 1) and floored bids (status 2)', function () { - // Create profileConfigs with pmTargetingKeys.enabled set to true - const profileConfigsMock = { + it('should get config by name', () => { + const mockConfig = { plugins: { - dynamicFloors: { - pmTargetingKeys: { - enabled: true - } - } + testPlugin: { enabled: true } } }; - const mockPbjs = { - getHighestCpmBids: (adUnitCode) => { - // For div2, return a winning bid - if (adUnitCode === 'div2') { - return [{ - adUnitCode: 'div2', - cpm: 5.5, - floorData: { - floorValue: 5.0, - floorProvider: 'PM' - } - }]; - } - // For all other ad units, return empty array (no winning bids) - return []; - } - }; - - // Stub getGlobal to return our mock object - const getGlobalStub = sandbox.stub(prebidGlobal, 'getGlobal').returns(mockPbjs); - - // Store the original value to restore it later - const originalProfileConfigs = getProfileConfigs(); - // Set profileConfigs to our mock - setProfileConfigs(profileConfigsMock); - - // Create ad unit codes to test - const adUnitCodes = ['div2', 'div3']; - const config = {}; - const userConsent = {}; - - // Create a mock auction object with bids that have RTD floors applied - const auction = { - adUnits: [ - { code: "div2", bids: [{ floorData: { floorProvider: "PM", skipped: false } }] }, - { code: "div3", bids: [{ floorData: { floorProvider: "PM", skipped: false } }] } - ], - adUnitCodes: ["div2", "div3"], - bidsReceived: [[ - { - "bidderCode": "appnexus", - "auctionId": "a262767c-5499-4e98-b694-af36dbcb50f6", - "mediaType": "banner", - "source": "client", - "cpm": 5.5, - "adUnitCode": "div2", - "adapterCode": "appnexus", - "originalCpm": 5.5, - "floorData": { - "floorValue": 5, - "floorRule": "banner|*|*|div2|*|*|*|*|*", - "floorRuleValue": 5, - "floorCurrency": "USD", - - }, - "bidder": "appnexus", - } - ]], - bidsRejected: [ - { adUnitCode: "div3", bidder: "pubmatic", cpm: 20, floorData: { floorValue: 40 }, rejectionReason: "Bid does not meet price floor" }] - }; - - const result = getTargetingData(adUnitCodes, config, userConsent, auction); - - // Restore the original value - setProfileConfigs(originalProfileConfigs); - - // Check the test results - - expect(result['div2']).to.have.property('pm_ym_flrs', 1); - expect(result['div2']).to.have.property('pm_ym_flrv', '5.50'); - expect(result['div2']).to.have.property('pm_ym_bid_s', 1); - - expect(result['div3']).to.have.property('pm_ym_flrs', 1); - expect(result['div3']).to.have.property('pm_ym_flrv', '32.00'); - expect(result['div3']).to.have.property('pm_ym_bid_s', 2); - getGlobalStub.restore(); - }); - - describe('should handle the no bid scenario correctly', function () { - it('should handle no bid scenario correctly', function () { - // Create profileConfigs with pmTargetingKeys enabled - const profileConfigsMock = { - plugins: { - dynamicFloors: { - pmTargetingKeys: { - enabled: true, - multiplier: { - nobid: 1.2 // Explicit nobid multiplier - } - } - } - } - }; - - // Store the original value to restore it later - const originalProfileConfigs = getProfileConfigs(); - // Set profileConfigs to our mock - setProfileConfigs(profileConfigsMock); - - // Create ad unit codes to test - const adUnitCodes = ['Div2']; - const config = {}; - const userConsent = {}; - - // Create a mock auction with no bids but with RTD floor applied - // For this test, we'll observe what the function actually does rather than - // try to match specific multiplier values - const auction = { - "auctionId": "faf0b7d0-3a12-4774-826a-3d56033d9a74", - "auctionStatus": "completed", - "adUnits": [ - { - "code": "Div2", - "sizes": [[300, 250]], - "mediaTypes": { - "banner": { "sizes": [[300, 250]] } - }, - "bids": [ - { - "bidder": "pubmatic", - "params": { - "publisherId": "164392", - "adSlot": "/4374asd3431/DMDemo1@160x600" - }, - "floorData": { - "floorProvider": "PM" - } - } - ] - } - ], - "adUnitCodes": ["Div2"], - "bidderRequests": [ - { - "bidderCode": "pubmatic", - "auctionId": "faf0b7d0-3a12-4774-826a-3d56033d9a74", - "bids": [ - { - "bidder": "pubmatic", - "adUnitCode": "Div2", - "floorData": { - "floorProvider": "PM" - }, - "mediaTypes": { - "banner": { "sizes": [[300, 250]] } - }, - "getFloor": () => { return { floor: 0.05, currency: 'USD' }; } - } - ] - } - ], - "noBids": [ - { - "bidder": "pubmatic", - "adUnitCode": "Div2", - "floorData": { - "floorProvider": "PM", - "floorMin": 0.05 - } - } - ], - "bidsReceived": [], - "bidsRejected": [], - "winningBids": [] - }; - - const result = getTargetingData(adUnitCodes, config, userConsent, auction); - - // Restore the original value - setProfileConfigs(originalProfileConfigs); - - // Verify correct values for no bid scenario - expect(result['Div2']['pm_ym_flrs']).to.equal(1); // RTD floor was applied - expect(result['Div2']['pm_ym_bid_s']).to.equal(0); // NOBID status - - // Since finding floor values from bidder requests depends on implementation details - // we'll just verify the type rather than specific value - expect(result['Div2']['pm_ym_flrv']).to.be.a('string'); - }); - - it('should handle no bid scenario correctly for single ad unit multiple size scenarios', function () { - // Create profileConfigs with pmTargetingKeys enabled - const profileConfigsMock = { - plugins: { - dynamicFloors: { - pmTargetingKeys: { - enabled: true, - multiplier: { - nobid: 1.2 // Explicit nobid multiplier - } - } - } - } - }; - - // Store the original value to restore it later - const originalProfileConfigs = getProfileConfigs(); - // Set profileConfigs to our mock - setProfileConfigs(profileConfigsMock); - - // Create ad unit codes to test - const adUnitCodes = ['Div2']; - const config = {}; - const userConsent = {}; - - // Create a mock auction with no bids but with RTD floor applied - // For this test, we'll observe what the function actually does rather than - // try to match specific multiplier values - const auction = { - "auctionId": "faf0b7d0-3a12-4774-826a-3d56033d9a74", - "auctionStatus": "completed", - "adUnits": [ - { - "code": "Div2", - "sizes": [[300, 250]], - "mediaTypes": { "banner": { "sizes": [[300, 250]] } }, - "bids": [ - { - "bidder": "pubmatic", - "params": { - "publisherId": "164392", - "adSlot": "/4374asd3431/DMDemo1@160x600" - }, - "floorData": { - "floorProvider": "PM" - } - } - ] - } - ], - "adUnitCodes": ["Div2"], - "bidderRequests": [ - { - "bidderCode": "pubmatic", - "auctionId": "faf0b7d0-3a12-4774-826a-3d56033d9a74", - "bids": [ - { - "bidder": "pubmatic", - "adUnitCode": "Div2", - "floorData": { - "floorProvider": "PM" - }, - "mediaTypes": { - "banner": { "sizes": [[300, 250]] } - }, - "getFloor": () => { return { floor: 5, currency: 'USD' }; } - } - ] - } - ], - "noBids": [ - { - "bidder": "pubmatic", - "adUnitCode": "Div2", - "floorData": { - "floorProvider": "PM", - "floorMin": 0.05 - } - } - ], - "bidsReceived": [], - "bidsRejected": [], - "winningBids": [] - }; - - const result = getTargetingData(adUnitCodes, config, userConsent, auction); - - // Restore the original value - setProfileConfigs(originalProfileConfigs); - - // Verify correct values for no bid scenario - expect(result['Div2']['pm_ym_flrs']).to.equal(1); // RTD floor was applied - expect(result['Div2']['pm_ym_bid_s']).to.equal(0); // NOBID status - - // Since finding floor values from bidder requests depends on implementation details - // we'll just verify the type rather than specific value - expect(result['Div2']['pm_ym_flrv']).to.be.a('string'); - expect(result['Div2']['pm_ym_flrv']).to.equal("6.00"); - }); - - it('should handle no bid scenario correctly for multi-format ad unit with different floors', function () { - // Create profileConfigs with pmTargetingKeys enabled - const profileConfigsMock = { - plugins: { - dynamicFloors: { - pmTargetingKeys: { - enabled: true, - multiplier: { - nobid: 1.2 // Explicit nobid multiplier - } - } - } - } - }; - - // Store the original value to restore it later - const originalProfileConfigs = getProfileConfigs(); - // Set profileConfigs to our mock - setProfileConfigs(profileConfigsMock); - - // Create ad unit codes to test - const adUnitCodes = ['multiFormatDiv']; - const config = {}; - const userConsent = {}; - - // Mock getFloor implementation that returns different floors for different media types - const mockGetFloor = (params) => { - const floors = { - 'banner': 0.50, // Higher floor for banner - 'video': 0.25 // Lower floor for video - }; - - return { - floor: floors[params.mediaType] || 0.10, - currency: 'USD' - }; - }; - - // Create a mock auction with a multi-format ad unit (banner + video) - const auction = { - "auctionId": "multi-format-test-auction", - "auctionStatus": "completed", - "adUnits": [ - { - "code": "multiFormatDiv", - "mediaTypes": { - "banner": { - "sizes": [[300, 250], [300, 600]] - }, - "video": { - "playerSize": [[640, 480]], - "context": "instream" - } - }, - "bids": [ - { - "bidder": "pubmatic", - "params": { - "publisherId": "test-publisher", - "adSlot": "/test/slot" - }, - "floorData": { - "floorProvider": "PM" - } - } - ] - } - ], - "adUnitCodes": ["multiFormatDiv"], - "bidderRequests": [ - { - "bidderCode": "pubmatic", - "auctionId": "multi-format-test-auction", - "bids": [ - { - "bidder": "pubmatic", - "adUnitCode": "multiFormatDiv", - "mediaTypes": { - "banner": { - "sizes": [[300, 250], [300, 600]] - }, - "video": { - "playerSize": [[640, 480]], - "context": "instream" - } - }, - "floorData": { - "floorProvider": "PM" - }, - "getFloor": mockGetFloor - } - ] - } - ], - "noBids": [ - { - "bidder": "pubmatic", - "adUnitCode": "multiFormatDiv", - "floorData": { - "floorProvider": "PM" - } - } - ], - "bidsReceived": [], - "bidsRejected": [], - "winningBids": [] - }; - - // Create a spy to monitor the getFloor calls - const getFloorSpy = sinon.spy(auction.bidderRequests[0].bids[0], "getFloor"); - - // Run the targeting function - const result = getTargetingData(adUnitCodes, config, userConsent, auction); - - // Restore the original value - setProfileConfigs(originalProfileConfigs); - - // Verify correct values for no bid scenario - expect(result['multiFormatDiv']['pm_ym_flrs']).to.equal(1); // RTD floor was applied - expect(result['multiFormatDiv']['pm_ym_bid_s']).to.equal(0); // NOBID status - - // Verify that getFloor was called with both media types - expect(getFloorSpy.called).to.be.true; - let bannerCallFound = false; - let videoCallFound = false; - - getFloorSpy.getCalls().forEach(call => { - const args = call.args[0]; - if (args.mediaType === 'banner') bannerCallFound = true; - if (args.mediaType === 'video') videoCallFound = true; - }); - - expect(bannerCallFound).to.be.true; // Verify banner format was checked - expect(videoCallFound).to.be.true; // Verify video format was checked - - // Since we created the mockGetFloor to return 0.25 for video (lower than 0.50 for banner), - // we expect the RTD provider to use the minimum floor value (0.25) - // We can't test the exact value due to multiplier application, but we can make sure - // it's derived from the lower value - expect(parseFloat(result['multiFormatDiv']['pm_ym_flrv'])).to.be.closeTo(0.25 * 1.2, 0.001); // 0.25 * nobid multiplier (1.2) + configManager.setYMConfig(mockConfig); - // Clean up - getFloorSpy.restore(); - }); + const result = configManager.getConfigByName('testPlugin'); + expect(result).to.deep.equal({ enabled: true }); }); }); }); diff --git a/test/spec/modules/realTimeDataModule_spec.js b/test/spec/modules/realTimeDataModule_spec.js index 883e8bcc3c7..1e8a6d53993 100644 --- a/test/spec/modules/realTimeDataModule_spec.js +++ b/test/spec/modules/realTimeDataModule_spec.js @@ -7,13 +7,14 @@ import 'src/prebid.js'; import {attachRealTimeDataProvider, onDataDeletionRequest} from 'modules/rtdModule/index.js'; import {GDPR_GVLIDS} from '../../../src/consentHandler.js'; import {MODULE_TYPE_RTD} from '../../../src/activities/modules.js'; - -const getBidRequestDataSpy = sinon.spy(); +import {registerActivityControl} from '../../../src/activities/rules.js'; +import {ACTIVITY_ENRICH_UFPD, ACTIVITY_TRANSMIT_EIDS} from '../../../src/activities/activities.js'; describe('Real time module', function () { let eventHandlers; let sandbox; let validSM, validSMWait, invalidSM, failureSM, nonConfSM, conf; + let getBidRequestDataStub; function mockEmitEvent(event, ...args) { (eventHandlers[event] || []).forEach((h) => h(...args)); @@ -22,6 +23,8 @@ describe('Real time module', function () { before(() => { eventHandlers = {}; sandbox = sinon.createSandbox(); + getBidRequestDataStub = sinon.stub(); + sandbox.stub(events, 'on').callsFake((event, handler) => { if (!eventHandlers.hasOwnProperty(event)) { eventHandlers[event] = []; @@ -41,7 +44,7 @@ describe('Real time module', function () { getTargetingData: (adUnitsCodes) => { return {'ad2': {'key': 'validSM'}} }, - getBidRequestData: getBidRequestDataSpy + getBidRequestData: getBidRequestDataStub }; validSMWait = { @@ -50,7 +53,7 @@ describe('Real time module', function () { getTargetingData: (adUnitsCodes) => { return {'ad1': {'key': 'validSMWait'}} }, - getBidRequestData: getBidRequestDataSpy + getBidRequestData: getBidRequestDataStub }; invalidSM = { @@ -112,18 +115,27 @@ describe('Real time module', function () { }) describe('', () => { - let PROVIDERS, _detachers; + let PROVIDERS, _detachers, rules; beforeEach(function () { PROVIDERS = [validSM, invalidSM, failureSM, nonConfSM, validSMWait]; _detachers = PROVIDERS.map(rtdModule.attachRealTimeDataProvider); rtdModule.init(config); config.setConfig(conf); + rules = [ + registerActivityControl(ACTIVITY_TRANSMIT_EIDS, 'test', (params) => { + return {allow: false}; + }), + registerActivityControl(ACTIVITY_ENRICH_UFPD, 'test', (params) => { + return {allow: false}; + }) + ] }); afterEach(function () { _detachers.forEach((f) => f()); config.resetConfig(); + rules.forEach(rule => rule()); }); it('should use only valid modules', function () { @@ -131,11 +143,49 @@ describe('Real time module', function () { }); it('should be able to modify bid request', function (done) { + const request = {bidRequest: {}}; + getBidRequestDataStub.callsFake((req) => { + req.foo = 'bar'; + }); + rtdModule.setBidRequestsData(() => { + assert(getBidRequestDataStub.calledTwice); + assert(getBidRequestDataStub.calledWith(sinon.match({bidRequest: {}}))); + expect(request.foo).to.eql('bar'); + done(); + }, request) + }); + + it('should apply guard to modules, but not affect ortb2Fragments otherwise', (done) => { + const ortb2Fragments = { + global: { + user: { + eids: ['id'] + } + }, + bidder: { + bidderA: { + user: { + eids: ['bid'] + } + } + } + }; + const request = {ortb2Fragments}; + getBidRequestDataStub.callsFake((req) => { + expect(req.ortb2Fragments.global.user.eids).to.not.exist; + expect(req.ortb2Fragments.bidder.bidderA.eids).to.not.exist; + req.ortb2Fragments.global.user.yob = 123; + req.ortb2Fragments.bidder.bidderB = { + user: { + yob: 123 + } + }; + }); rtdModule.setBidRequestsData(() => { - assert(getBidRequestDataSpy.calledTwice); - assert(getBidRequestDataSpy.calledWith(sinon.match({bidRequest: {}}))); + expect(request.ortb2Fragments.global.user.eids).to.eql(['id']); + expect(request.ortb2Fragments.bidder.bidderB?.user?.yob).to.not.exist; done(); - }, {bidRequest: {}}) + }, request); }); it('sould place targeting on adUnits', function (done) { diff --git a/test/spec/modules/revnewBidAdapter_spec.js b/test/spec/modules/revnewBidAdapter_spec.js new file mode 100644 index 00000000000..904b59589cb --- /dev/null +++ b/test/spec/modules/revnewBidAdapter_spec.js @@ -0,0 +1,625 @@ +import { expect } from 'chai'; +import { + spec, STORAGE, getRevnewLocalStorage, +} from 'modules/revnewBidAdapter.js'; +import sinon from 'sinon'; +import { getAmxId } from '../../../libraries/nexx360Utils/index.js'; +const sandbox = sinon.createSandbox(); + +describe('Revnew bid adapter tests', () => { + const DEFAULT_OPTIONS = { + gdprConsent: { + gdprApplies: true, + consentString: 'BOzZdA0OzZdA0AGABBENDJ-AAAAvh7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__79__3z3_9pxP78k89r7337Mw_v-_v-b7JCPN_Y3v-8Kg', + vendorData: {}, + }, + refererInfo: { + referer: 'https://www.prebid.org', + canonicalUrl: 'https://www.prebid.org/the/link/to/the/page', + }, + uspConsent: '111222333', + userId: { id5id: { uid: '1111' } }, + schain: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'exchange1.com', + sid: '1234', + hp: 1, + rid: 'bid-request-1', + name: 'publisher', + domain: 'publisher.com', + }], + }, + }; + + describe('isBidRequestValid()', () => { + let bannerBid; + beforeEach(() => { + bannerBid = { + bidder: 'revnew', + mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } }, + adUnitCode: 'div-1', + transactionId: '70bdc37e-9475-4b27-8c74-4634bdc2ee66', + sizes: [[300, 250], [300, 600]], + bidId: '4906582fc87d0c', + bidderRequestId: '332fda16002dbe', + auctionId: '98932591-c822-42e3-850e-4b3cf748d063', + } + }); + + it('We verify isBidRequestValid with incorrect tagid', () => { + bannerBid.params = { 'tagid': 'luvxjvgn' }; + expect(spec.isBidRequestValid(bannerBid)).to.be.equal(false); + }); + + it('We verify isBidRequestValid with correct tagId', () => { + bannerBid.params = { 'tagId': 'luvxjvgn' }; + expect(spec.isBidRequestValid(bannerBid)).to.be.equal(true); + }); + + it('We verify isBidRequestValid with correct placement', () => { + bannerBid.params = { 'placement': 'testad' }; + expect(spec.isBidRequestValid(bannerBid)).to.be.equal(true); + }); + }); + + describe('getRevnewLocalStorage disabled', () => { + before(() => { + sandbox.stub(STORAGE, 'localStorageIsEnabled').callsFake(() => false); + }); + it('We test if we get the revnewId', () => { + const output = getRevnewLocalStorage(); + expect(output).to.be.eql(null); + }); + after(() => { + sandbox.restore() + }); + }); + + describe('getRevnewLocalStorage enabled but nothing', () => { + before(() => { + sandbox.stub(STORAGE, 'localStorageIsEnabled').callsFake(() => true); + sandbox.stub(STORAGE, 'setDataInLocalStorage'); + sandbox.stub(STORAGE, 'getDataFromLocalStorage').callsFake(() => null); + }); + it('We test if we get the revnewId', () => { + const output = getRevnewLocalStorage(); + expect(typeof output.revnewId).to.be.eql('string'); + }); + after(() => { + sandbox.restore() + }); + }); + + describe('getRevnewLocalStorage enabled but wrong payload', () => { + before(() => { + sandbox.stub(STORAGE, 'localStorageIsEnabled').callsFake(() => true); + sandbox.stub(STORAGE, 'setDataInLocalStorage'); + sandbox.stub(STORAGE, 'getDataFromLocalStorage').callsFake(() => '{"revnewId":"5ad89a6e-7801-48e7-97bb-fe6f251f6cb4",}'); + }); + it('We test if we get the revnewId', () => { + const output = getRevnewLocalStorage(); + expect(output).to.be.eql(null); + }); + after(() => { + sandbox.restore() + }); + }); + + describe('getRevnewLocalStorage enabled', () => { + before(() => { + sandbox.stub(STORAGE, 'localStorageIsEnabled').callsFake(() => true); + sandbox.stub(STORAGE, 'setDataInLocalStorage'); + sandbox.stub(STORAGE, 'getDataFromLocalStorage').callsFake(() => '{"revnewId":"5ad89a6e-7801-48e7-97bb-fe6f251f6cb4"}'); + }); + it('We test if we get the revnewId', () => { + const output = getRevnewLocalStorage(); + expect(output.revnewId).to.be.eql('5ad89a6e-7801-48e7-97bb-fe6f251f6cb4'); + }); + after(() => { + sandbox.restore() + }); + }); + + describe('getAmxId() with localStorage enabled and data not set', () => { + before(() => { + sandbox.stub(STORAGE, 'localStorageIsEnabled').callsFake(() => true); + sandbox.stub(STORAGE, 'setDataInLocalStorage'); + sandbox.stub(STORAGE, 'getDataFromLocalStorage').callsFake(() => null); + }); + it('We test if we get the amxId', () => { + const output = getAmxId(STORAGE, 'revnew'); + expect(output).to.be.eql(null); + }); + after(() => { + sandbox.restore() + }); + }); + + describe('getAmxId() with localStorage enabled and data set', () => { + before(() => { + sandbox.stub(STORAGE, 'localStorageIsEnabled').callsFake(() => true); + sandbox.stub(STORAGE, 'setDataInLocalStorage'); + sandbox.stub(STORAGE, 'getDataFromLocalStorage').callsFake(() => 'abcdef'); + }); + it('We test if we get the amxId', () => { + const output = getAmxId(STORAGE, 'revnew'); + expect(output).to.be.eql('abcdef'); + }); + after(() => { + sandbox.restore() + }); + }); + + describe('buildRequests()', () => { + before(() => { + const documentStub = sandbox.stub(document, 'getElementById'); + documentStub.withArgs('div-1').returns({ + offsetWidth: 200, + offsetHeight: 250, + style: { + maxWidth: '400px', + maxHeight: '350px', + }, + getBoundingClientRect() { return { width: 200, height: 250 }; } + }); + sandbox.stub(STORAGE, 'localStorageIsEnabled').callsFake(() => true); + sandbox.stub(STORAGE, 'setDataInLocalStorage'); + sandbox.stub(STORAGE, 'getDataFromLocalStorage').callsFake(() => 'abcdef'); + }); + describe('We test with a multiple display bids', () => { + const sampleBids = [ + { + bidder: 'revnew', + params: { + tagId: 'luvxjvgn', + divId: 'div-1', + adUnitName: 'header-ad', + adUnitPath: '/12345/revnew/Homepage/HP/Header-Ad', + }, + ortb2Imp: { + ext: { + gpid: '/12345/revnew/Homepage/HP/Header-Ad', + } + }, + adUnitCode: 'header-ad-1234', + transactionId: '469a570d-f187-488d-b1cb-48c1a2009be9', + sizes: [[300, 250], [300, 600]], + bidId: '44a2706ac3574', + bidderRequestId: '359bf8a3c06b2e', + auctionId: '2e684815-b44e-4e04-b812-56da54adbe74', + }, + { + bidder: 'revnew', + params: { + placement: 'testPlacement', + allBids: true, + }, + mediaTypes: { + banner: { + sizes: [[728, 90], [970, 250]] + } + }, + + adUnitCode: 'div-2-abcd', + transactionId: '6196885d-4e76-40dc-a09c-906ed232626b', + sizes: [[728, 90], [970, 250]], + bidId: '5ba94555219a03', + bidderRequestId: '359bf8a3c06b2e', + auctionId: '2e684815-b44e-4e04-b812-56da54adbe74', + } + ]; + const bidderRequest = { + bidderCode: 'revnew', + auctionId: '2e684815-b44e-4e04-b812-56da54adbe74', + bidderRequestId: '359bf8a3c06b2e', + refererInfo: { + reachedTop: true, + isAmp: false, + numIframes: 0, + stack: [ + 'https://test.nexx360.io/adapter/index.html' + ], + topmostLocation: 'https://test.nexx360.io/adapter/index.html', + location: 'https://test.nexx360.io/adapter/index.html', + canonicalUrl: null, + page: 'https://test.nexx360.io/adapter/index.html', + domain: 'test.nexx360.io', + ref: null, + legacy: { + reachedTop: true, + isAmp: false, + numIframes: 0, + stack: [ + 'https://test.nexx360.io/adapter/index.html' + ], + referer: 'https://test.nexx360.io/adapter/index.html', + canonicalUrl: null + }, + }, + gdprConsent: { + gdprApplies: true, + consentString: 'CPhdLUAPhdLUAAKAsAENCmCsAP_AAE7AAAqIJFNd_H__bW9r-f5_aft0eY1P9_r37uQzDhfNk-8F3L_W_LwX52E7NF36tq4KmR4ku1LBIUNlHMHUDUmwaokVryHsak2cpzNKJ7BEknMZOydYGF9vmxtj-QKY7_5_d3bx2D-t_9v239z3z81Xn3d53-_03LCdV5_9Dfn9fR_bc9KPt_58v8v8_____3_e__3_7997BIiAaADgAJYBnwEeAJXAXmAwQBj4DtgHcgPBAeKBIgAA.YAAAAAAAAAAA', + } + }; + it('We perform a test with 2 display adunits', () => { + const displayBids = structuredClone(sampleBids); + displayBids[0].mediaTypes = { + banner: { + sizes: [[300, 250], [300, 600]] + } + }; + const request = spec.buildRequests(displayBids, bidderRequest); + const requestContent = request.data; + expect(request).to.have.property('method').and.to.equal('POST'); + expect(requestContent.imp[0].ext.revnew.tagId).to.be.eql('luvxjvgn'); + expect(requestContent.imp[0].ext.revnew.divId).to.be.eql('div-1'); + expect(requestContent.imp[1].ext.revnew.placement).to.be.eql('testPlacement'); + expect(requestContent.ext.bidderVersion).to.be.eql('1.0'); + }); + + if (FEATURES.VIDEO) { + it('We perform a test with a multiformat adunit', () => { + const multiformatBids = structuredClone(sampleBids); + multiformatBids[0].mediaTypes = { + banner: { + sizes: [[300, 250], [300, 600]] + }, + video: { + context: 'outstream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [2], + skip: 1, + playback_method: ['auto_play_sound_off'] + } + }; + const request = spec.buildRequests(multiformatBids, bidderRequest); + const video = request.data.imp[0].video; + const expectedVideo = { + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [2], + skip: 1, + w: 640, + h: 480, + ext: { + playerSize: [640, 480], + context: 'outstream', + }, + }; + expect(video).to.eql(expectedVideo); + }); + + it('We perform a test with a instream adunit', () => { + const videoBids = structuredClone(sampleBids); + videoBids[0].mediaTypes = { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6], + playbackmethod: [2], + skip: 1 + } + }; + const request = spec.buildRequests(videoBids, bidderRequest); + const requestContent = request.data; + expect(request).to.have.property('method').and.to.equal('POST'); + expect(requestContent.imp[0].video.ext.context).to.be.eql('instream'); + expect(requestContent.imp[0].video.playbackmethod[0]).to.be.eql(2); + }); + } + }); + after(() => { + sandbox.restore() + }); + }); + + describe('We test interpretResponse', () => { + it('empty response', () => { + const response = { + body: '' + }; + const output = spec.interpretResponse(response); + expect(output.length).to.be.eql(0); + }); + it('banner responses with adm', () => { + const response = { + body: { + id: 'a8d3a675-a4ba-4d26-807f-c8f2fad821e0', + cur: 'USD', + seatbid: [ + { + bid: [ + { + id: '4427551302944024629', + impid: '226175918ebeda', + price: 1.5, + adomain: [ + 'http://prebid.org', + ], + crid: '98493581', + ssp: 'appnexus', + h: 600, + w: 300, + adm: '
TestAd
', + cat: [ + 'IAB3-1', + ], + ext: { + adUnitCode: 'div-1', + mediaType: 'banner', + adUrl: 'https://fast.nexx360.io/cache?uuid=fdddcebc-1edf-489d-880d-1418d8bdc493', + ssp: 'appnexus', + }, + }, + ], + seat: 'appnexus', + }, + ], + ext: { + id: 'de3de7c7-e1cf-4712-80a9-94eb26bfc718', + cookies: [], + }, + }, + }; + const output = spec.interpretResponse(response); + const expectedOutput = [{ + requestId: '226175918ebeda', + cpm: 1.5, + width: 300, + height: 600, + creativeId: '98493581', + currency: 'USD', + netRevenue: true, + ttl: 120, + mediaType: 'banner', + meta: { + advertiserDomains: [ + 'http://prebid.org', + ], + demandSource: 'appnexus', + }, + ad: '
TestAd
', + }]; + expect(output).to.eql(expectedOutput); + }); + + it('instream responses', () => { + const response = { + body: { + id: '2be64380-ba0c-405a-ab53-51f51c7bde51', + cur: 'USD', + seatbid: [ + { + bid: [ + { + id: '8275140264321181514', + impid: '263cba3b8bfb72', + price: 5, + adomain: [ + 'appnexus.com', + ], + crid: '97517771', + h: 1, + w: 1, + adm: 'vast', + ext: { + mediaType: 'instream', + ssp: 'appnexus', + adUnitCode: 'video1', + }, + }, + ], + seat: 'appnexus', + }, + ], + ext: { + cookies: [], + }, + }, + }; + + const output = spec.interpretResponse(response); + const expectedOutput = [{ + requestId: '263cba3b8bfb72', + cpm: 5, + width: 1, + height: 1, + creativeId: '97517771', + currency: 'USD', + netRevenue: true, + ttl: 120, + mediaType: 'video', + meta: { advertiserDomains: ['appnexus.com'], demandSource: 'appnexus' }, + vastXml: 'vast', + }]; + expect(output).to.eql(expectedOutput); + }); + + it('outstream responses', () => { + const response = { + body: { + id: '40c23932-135e-4602-9701-ca36f8d80c07', + cur: 'USD', + seatbid: [ + { + bid: [ + { + id: '1186971142548769361', + impid: '4ce809b61a3928', + price: 5, + adomain: [ + 'appnexus.com', + ], + crid: '97517771', + h: 1, + w: 1, + adm: 'vast', + ext: { + mediaType: 'outstream', + ssp: 'appnexus', + adUnitCode: 'div-1', + divId: 'div-1', + }, + }, + ], + seat: 'appnexus', + }, + ], + ext: { + cookies: [], + }, + }, + }; + + const output = spec.interpretResponse(response); + const expectedOutut = [{ + requestId: '4ce809b61a3928', + cpm: 5, + width: 1, + height: 1, + creativeId: '97517771', + currency: 'USD', + netRevenue: true, + divId: 'div-1', + ttl: 120, + mediaType: 'video', + meta: { advertiserDomains: ['appnexus.com'], demandSource: 'appnexus' }, + vastXml: 'vast', + renderer: output[0].renderer, + }]; + expect(output).to.eql(expectedOutut); + }); + + it('native responses', () => { + const response = { + body: { + id: '3c0290c1-6e75-4ef7-9e37-17f5ebf3bfa3', + cur: 'USD', + seatbid: [ + { + bid: [ + { + id: '6624930625245272225', + impid: '23e11d845514bb', + price: 10, + adomain: [ + 'prebid.org', + ], + crid: '97494204', + h: 1, + w: 1, + cat: [ + 'IAB3-1', + ], + ext: { + mediaType: 'native', + ssp: 'appnexus', + adUnitCode: '/19968336/prebid_native_example_1', + }, + adm: '{"ver":"1.2","assets":[{"id":1,"img":{"url":"https:\\/\\/vcdn.adnxs.com\\/p\\/creative-image\\/f8\\/7f\\/0f\\/13\\/f87f0f13-230c-4f05-8087-db9216e393de.jpg","w":989,"h":742,"ext":{"appnexus":{"prevent_crop":0}}}},{"id":0,"title":{"text":"This is a Prebid Native Creative"}},{"id":2,"data":{"value":"Prebid.org"}}],"link":{"url":"https:\\/\\/ams3-ib.adnxs.com\\/click?AAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQKZS4ZZl5vVbR6p-A-MwnyTZ7QVkAAAAAOLoyQBtJAAAbSQAAAIAAAC8pM8FnPgWAAAAAABVU0QAVVNEAAEAAQBNXQAAAAABAgMCAAAAALoAURe69gAAAAA.\\/bcr=AAAAAAAA8D8=\\/pp=${AUCTION_PRICE}\\/cnd=%21JBC72Aj8-LwKELzJvi4YnPFbIAQoADEAAAAAAAAkQDoJQU1TMzo2MTM1QNAwSQAAAAAAAPA_UQAAAAAAAAAAWQAAAAAAAAAAYQAAAAAAAAAAaQAAAAAAAAAAcQAAAAAAAAAAeACJAQAAAAAAAAAA\\/cca=OTMyNSNBTVMzOjYxMzU=\\/bn=97062\\/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html"},"eventtrackers":[{"event":1,"method":1,"url":"https:\\/\\/ams3-ib.adnxs.com\\/it?an_audit=0&referrer=https%3A%2F%2Ftest.nexx360.io%2Fadapter%2Fnative%2Ftest.html&e=wqT_3QKJCqAJBQAAAwDWAAUBCNnbl6AGEKalhbfZzPn6WxjH1PqbsJzMzyQqNgkAAAECCCRAEQEHEAAAJEAZEQkAIREJACkRCQAxEQmoMOLRpwY47UhA7UhIAlC8yb4uWJzxW2AAaM26dXim9gWAAQGKAQNVU0SSAQEG9F4BmAEBoAEBqAEBsAEAuAECwAEDyAEC0AEJ2AEA4AEA8AEAigIpdWYoJ2EnLCAyNTI5ODg1LCAwKTt1ZigncicsIDk3NDk0MjA0LCAwKTuSAvEDIS0xRDNJQWo4LUx3S0VMekp2aTRZQUNDYzhWc3dBRGdBUUFSSTdVaFE0dEduQmxnQVlQX19fXzhQYUFCd0FYZ0JnQUVCaUFFQmtBRUJtQUVCb0FFQnFBRURzQUVBdVFIenJXcWtBQUFrUU1FQjg2MXFwQUFBSkVESkFYSUtWbWViSmZJXzJRRUFBQUFBQUFEd1AtQUJBUFVCQUFBQUFKZ0NBS0FDQUxVQ0FBQUFBTDBDQUFBQUFNQUNBY2dDQWRBQ0FkZ0NBZUFDQU9nQ0FQZ0NBSUFEQVpnREFib0RDVUZOVXpNNk5qRXpOZUFEMERDSUJBQ1FCQUNZQkFIQkJBQUFBQUFBQUFBQXlRUUFBCQscQUFOZ0VBUEURlSxBQUFDSUJmY3ZxUVUBDQRBQQGoCDdFRgEKCQEMREJCUQkKAQEAeRUoAUwyKAAAWi4oALg0QVhBaEQzd0JhTEQzd0w0QmQyMG1nR0NCZ05WVTBTSUJnQ1FCZ0dZQmdDaEJnQQFONEFBQ1JBcUFZQnNnWWtDHXQARR0MAEcdDABJHQw8dUFZS5oClQEhSkJDNzJBajL1ASRuUEZiSUFRb0FEFfhUa1FEb0pRVTFUTXpvMk1UTTFRTkF3UxFRDFBBX1URDAxBQUFXHQwAWR0MAGEdDABjHQwQZUFDSkEdEMjYAvfpA-ACrZhI6gIwaHR0cHM6Ly90ZXN0Lm5leHgzNjAuaW8vYWRhcHRlci9uYXRpdmUJH_CaaHRtbIADAIgDAZADAJgDFKADAaoDAMAD4KgByAMA2AMA4AMA6AMA-AMDgAQAkgQJL29wZW5ydGIymAQAqAQAsgQMCAAQABgAIAAwADgAuAQAwASA2rgiyAQA0gQOOTMyNSNBTVMzOjYxMzXaBAIIAeAEAPAEvMm-LvoEEgkAAABAPG1IQBEAAACgV8oCQIgFAZgFAKAF______8BBbABqgUkM2MwMjkwYzEtNmU3NS00ZWY3LTllMzctMTdmNWViZjNiZmEzwAUAyQWJFxTwP9IFCQkJDHgAANgFAeAFAfAFmfQh-gUECAAQAJAGAZgGALgGAMEGCSUo8D_QBvUv2gYWChAJERkBAdpg4AYM8gYCCACABwGIBwCgB0HIB6b2BdIHDRVkASYI2gcGAV1oGADgBwDqBwIIAPAHAIoIAhAAlQgAAIA_mAgB&s=ccf63f2e483a37091d2475d895e7cf7c911d1a78&pp=${AUCTION_PRICE}"}]}', + }, + ], + seat: 'appnexus', + }, + ], + ext: { + cookies: [], + }, + }, + }; + + const output = spec.interpretResponse(response); + const expectOutput = [{ + requestId: '23e11d845514bb', + cpm: 10, + width: 1, + height: 1, + creativeId: '97494204', + currency: 'USD', + netRevenue: true, + ttl: 120, + mediaType: 'native', + meta: { + advertiserDomains: [ + 'prebid.org', + ], + demandSource: 'appnexus', + }, + native: { + ortb: { + ver: '1.2', + assets: [ + { + id: 1, + img: { + url: 'https://vcdn.adnxs.com/p/creative-image/f8/7f/0f/13/f87f0f13-230c-4f05-8087-db9216e393de.jpg', + w: 989, + h: 742, + ext: { + appnexus: { + prevent_crop: 0, + }, + }, + }, + }, + { + id: 0, + title: { + text: 'This is a Prebid Native Creative', + }, + }, + { + id: 2, + data: { + value: 'Prebid.org', + }, + }, + ], + link: { + url: 'https://ams3-ib.adnxs.com/click?AAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQKZS4ZZl5vVbR6p-A-MwnyTZ7QVkAAAAAOLoyQBtJAAAbSQAAAIAAAC8pM8FnPgWAAAAAABVU0QAVVNEAAEAAQBNXQAAAAABAgMCAAAAALoAURe69gAAAAA./bcr=AAAAAAAA8D8=/pp=${AUCTION_PRICE}/cnd=%21JBC72Aj8-LwKELzJvi4YnPFbIAQoADEAAAAAAAAkQDoJQU1TMzo2MTM1QNAwSQAAAAAAAPA_UQAAAAAAAAAAWQAAAAAAAAAAYQAAAAAAAAAAaQAAAAAAAAAAcQAAAAAAAAAAeACJAQAAAAAAAAAA/cca=OTMyNSNBTVMzOjYxMzU=/bn=97062/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html', + }, + eventtrackers: [ + { + event: 1, + method: 1, + url: 'https://ams3-ib.adnxs.com/it?an_audit=0&referrer=https%3A%2F%2Ftest.nexx360.io%2Fadapter%2Fnative%2Ftest.html&e=wqT_3QKJCqAJBQAAAwDWAAUBCNnbl6AGEKalhbfZzPn6WxjH1PqbsJzMzyQqNgkAAAECCCRAEQEHEAAAJEAZEQkAIREJACkRCQAxEQmoMOLRpwY47UhA7UhIAlC8yb4uWJzxW2AAaM26dXim9gWAAQGKAQNVU0SSAQEG9F4BmAEBoAEBqAEBsAEAuAECwAEDyAEC0AEJ2AEA4AEA8AEAigIpdWYoJ2EnLCAyNTI5ODg1LCAwKTt1ZigncicsIDk3NDk0MjA0LCAwKTuSAvEDIS0xRDNJQWo4LUx3S0VMekp2aTRZQUNDYzhWc3dBRGdBUUFSSTdVaFE0dEduQmxnQVlQX19fXzhQYUFCd0FYZ0JnQUVCaUFFQmtBRUJtQUVCb0FFQnFBRURzQUVBdVFIenJXcWtBQUFrUU1FQjg2MXFwQUFBSkVESkFYSUtWbWViSmZJXzJRRUFBQUFBQUFEd1AtQUJBUFVCQUFBQUFKZ0NBS0FDQUxVQ0FBQUFBTDBDQUFBQUFNQUNBY2dDQWRBQ0FkZ0NBZUFDQU9nQ0FQZ0NBSUFEQVpnREFib0RDVUZOVXpNNk5qRXpOZUFEMERDSUJBQ1FCQUNZQkFIQkJBQUFBQUFBQUFBQXlRUUFBCQscQUFOZ0VBUEURlSxBQUFDSUJmY3ZxUVUBDQRBQQGoCDdFRgEKCQEMREJCUQkKAQEAeRUoAUwyKAAAWi4oALg0QVhBaEQzd0JhTEQzd0w0QmQyMG1nR0NCZ05WVTBTSUJnQ1FCZ0dZQmdDaEJnQQFONEFBQ1JBcUFZQnNnWWtDHXQARR0MAEcdDABJHQw8dUFZS5oClQEhSkJDNzJBajL1ASRuUEZiSUFRb0FEFfhUa1FEb0pRVTFUTXpvMk1UTTFRTkF3UxFRDFBBX1URDAxBQUFXHQwAWR0MAGEdDABjHQwQZUFDSkEdEMjYAvfpA-ACrZhI6gIwaHR0cHM6Ly90ZXN0Lm5leHgzNjAuaW8vYWRhcHRlci9uYXRpdmUJH_CaaHRtbIADAIgDAZADAJgDFKADAaoDAMAD4KgByAMA2AMA4AMA6AMA-AMDgAQAkgQJL29wZW5ydGIymAQAqAQAsgQMCAAQABgAIAAwADgAuAQAwASA2rgiyAQA0gQOOTMyNSNBTVMzOjYxMzXaBAIIAeAEAPAEvMm-LvoEEgkAAABAPG1IQBEAAACgV8oCQIgFAZgFAKAF______8BBbABqgUkM2MwMjkwYzEtNmU3NS00ZWY3LTllMzctMTdmNWViZjNiZmEzwAUAyQWJFxTwP9IFCQkJDHgAANgFAeAFAfAFmfQh-gUECAAQAJAGAZgGALgGAMEGCSUo8D_QBvUv2gYWChAJERkBAdpg4AYM8gYCCACABwGIBwCgB0HIB6b2BdIHDRVkASYI2gcGAV1oGADgBwDqBwIIAPAHAIoIAhAAlQgAAIA_mAgB&s=ccf63f2e483a37091d2475d895e7cf7c911d1a78&pp=${AUCTION_PRICE}', + }, + ], + }, + }, + }]; + expect(output).to.eql(expectOutput); + }); + }); + + describe('getUserSyncs()', () => { + const response = { body: { cookies: [] } }; + it('Verifies user sync without cookie in bid response', () => { + const syncs = spec.getUserSyncs({}, [response], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + expect(syncs).to.eql([]); + }); + it('Verifies user sync with cookies in bid response', () => { + response.body.ext = { + cookies: [{'type': 'image', 'url': 'http://www.cookie.sync.org/'}] + }; + const syncs = spec.getUserSyncs({}, [response], DEFAULT_OPTIONS.gdprConsent); + const expectedSyncs = [{ type: 'image', url: 'http://www.cookie.sync.org/' }]; + expect(syncs).to.eql(expectedSyncs); + }); + it('Verifies user sync with no bid response', () => { + var syncs = spec.getUserSyncs({}, null, DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + expect(syncs).to.eql([]); + }); + it('Verifies user sync with no bid body response', () => { + let syncs = spec.getUserSyncs({}, [], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + expect(syncs).to.eql([]); + syncs = spec.getUserSyncs({}, [{}], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + expect(syncs).to.eql([]); + }); + }); +}); diff --git a/test/spec/modules/richaudienceBidAdapter_spec.js b/test/spec/modules/richaudienceBidAdapter_spec.js index 5f20024fec2..fb90c793188 100644 --- a/test/spec/modules/richaudienceBidAdapter_spec.js +++ b/test/spec/modules/richaudienceBidAdapter_spec.js @@ -430,7 +430,6 @@ describe('Richaudience adapter tests', function () { expect(requestContent).to.have.property('timeout').and.to.equal(600); expect(requestContent).to.have.property('numIframes').and.to.equal(0); expect(typeof requestContent.scr_rsl === 'string') - expect(typeof requestContent.cpuc === 'number') expect(typeof requestContent.gpid === 'string') expect(requestContent).to.have.property('kws').and.to.equal('key1=value1;key2=value2'); }) diff --git a/test/spec/modules/rtbhouseBidAdapter_spec.js b/test/spec/modules/rtbhouseBidAdapter_spec.js index f44ccd1651d..e75190037bd 100644 --- a/test/spec/modules/rtbhouseBidAdapter_spec.js +++ b/test/spec/modules/rtbhouseBidAdapter_spec.js @@ -344,17 +344,17 @@ describe('RTBHouseAdapter', () => { expect(data.source.tid).to.equal('bidderrequest-auction-id'); }); - it('should include bidfloor from floor module if avaiable', () => { + it('should include bidfloor from floor module if available', () => { const bidRequest = Object.assign([], bidRequests); - bidRequest[0].getFloor = () => ({floor: 1.22}); + bidRequest[0].getFloor = () => ({floor: 1.22, currency: 'USD'}); const request = spec.buildRequests(bidRequest, bidderRequest); const data = JSON.parse(request.data); expect(data.imp[0].bidfloor).to.equal(1.22) }); - it('should use bidfloor from floor module if both floor module and bid floor avaiable', () => { + it('should use bidfloor from floor module if both floor module and bid floor available', () => { const bidRequest = Object.assign([], bidRequests); - bidRequest[0].getFloor = () => ({floor: 1.22}); + bidRequest[0].getFloor = () => ({floor: 1.22, currency: 'USD'}); bidRequest[0].params.bidfloor = 0.01; const request = spec.buildRequests(bidRequest, bidderRequest); const data = JSON.parse(request.data); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index b96a5e4fd4f..70e55c5a7eb 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -2890,88 +2890,6 @@ describe('the rubicon adapter', function () { expect(slotParams.kw).to.equal('a,b,c'); }); - it('should pass along o_ae param when fledge is enabled', () => { - const localBidRequest = Object.assign({}, bidderRequest.bids[0]); - localBidRequest.ortb2Imp.ext.ae = true; - - const slotParams = spec.createSlotParams(localBidRequest, bidderRequest); - - expect(slotParams['o_ae']).to.equal(1) - }); - - it('should pass along desired segtaxes, but not non-desired ones', () => { - const localBidderRequest = Object.assign({}, bidderRequest); - localBidderRequest.refererInfo = {domain: 'bob'}; - config.setConfig({ - rubicon: { - sendUserSegtax: [9], - sendSiteSegtax: [10] - } - }); - localBidderRequest.ortb2.user = { - data: [{ - ext: { - segtax: '404' - }, - segment: [{id: 5}, {id: 6}] - }, { - ext: { - segtax: '508' - }, - segment: [{id: 5}, {id: 2}] - }, { - ext: { - segtax: '9' - }, - segment: [{id: 1}, {id: 2}] - }] - } - localBidderRequest.ortb2.site = { - content: { - data: [{ - ext: { - segtax: '10' - }, - segment: [{id: 2}, {id: 3}] - }, { - ext: { - segtax: '507' - }, - segment: [{id: 3}, {id: 4}] - }] - } - } - const slotParams = spec.createSlotParams(bidderRequest.bids[0], localBidderRequest); - expect(slotParams['tg_i.tax507']).is.equal('3,4'); - expect(slotParams['tg_v.tax508']).is.equal('5,2'); - expect(slotParams['tg_v.tax9']).is.equal('1,2'); - expect(slotParams['tg_i.tax10']).is.equal('2,3'); - expect(slotParams['tg_v.tax404']).is.equal(undefined); - }); - - it('should support IAB segtax 7 in site segments', () => { - const localBidderRequest = Object.assign({}, bidderRequest); - localBidderRequest.refererInfo = {domain: 'bob'}; - config.setConfig({ - rubicon: { - sendUserSegtax: [4], - sendSiteSegtax: [1, 2, 5, 6, 7] - } - }); - localBidderRequest.ortb2.site = { - content: { - data: [{ - ext: { - segtax: '7' - }, - segment: [{id: 8}, {id: 9}] - }] - } - }; - const slotParams = spec.createSlotParams(bidderRequest.bids[0], localBidderRequest); - expect(slotParams['tg_i.tax7']).to.equal('8,9'); - }); - it('should add p_site.mobile if mobile is a number in ortb2.site', function () { // Set up a bidRequest with mobile property as a number const localBidderRequest = Object.assign({}, bidderRequest); @@ -3816,43 +3734,6 @@ describe('the rubicon adapter', function () { expect(bids).to.be.lengthOf(0); }); - it('Should support recieving an auctionConfig and pass it along to Prebid', function () { - const response = { - 'status': 'ok', - 'account_id': 14062, - 'site_id': 70608, - 'zone_id': 530022, - 'size_id': 15, - 'alt_size_ids': [ - 43 - ], - 'tracking': '', - 'inventory': {}, - 'ads': [{ - 'status': 'ok', - 'cpm': 0, - 'size_id': 15 - }], - 'component_auction_config': [{ - 'random': 'value', - 'bidId': '5432' - }, - { - 'random': 'string', - 'bidId': '6789' - }] - }; - - const {bids, paapi} = spec.interpretResponse({body: response}, { - bidRequest: bidderRequest.bids[0] - }); - - expect(bids).to.be.lengthOf(1); - expect(paapi[0].bidId).to.equal('5432'); - expect(paapi[0].config.random).to.equal('value'); - expect(paapi[1].bidId).to.equal('6789'); - }); - it('should handle an error', function () { const response = { 'status': 'ok', diff --git a/test/spec/modules/screencoreBidAdapter_spec.js b/test/spec/modules/screencoreBidAdapter_spec.js index 4e9177e8ce5..bd1aad95edf 100644 --- a/test/spec/modules/screencoreBidAdapter_spec.js +++ b/test/spec/modules/screencoreBidAdapter_spec.js @@ -1,789 +1,396 @@ import { expect } from 'chai'; -import { createDomain, spec as adapter, storage } from 'modules/screencoreBidAdapter.js'; -import { getGlobal } from 'src/prebidGlobal.js'; -import { - extractCID, - extractPID, - extractSubDomain, - getStorageItem, - getUniqueDealId, - hashCode, - setStorageItem, - tryParseJSON, -} from 'libraries/vidazooUtils/bidderUtils.js'; +import { createDomain, spec as adapter } from 'modules/screencoreBidAdapter.js'; import { config } from 'src/config.js'; import { BANNER, VIDEO, NATIVE } from 'src/mediaTypes.js'; -import { version } from 'package.json'; -import * as utils from 'src/utils.js'; -import sinon, { useFakeTimers } from 'sinon'; - -export const TEST_ID_SYSTEMS = ['criteoId', 'id5id', 'netId', 'tdid', 'pubProvidedId', 'intentIqId', 'liveIntentId']; - -const SUB_DOMAIN = 'exchange'; +import sinon from 'sinon'; const BID = { - 'bidId': '2d52001cabd527', - 'adUnitCode': 'div-gpt-ad-12345-0', - 'params': { - 'subDomain': SUB_DOMAIN, - 'cId': '59db6b3b4ffaa70004f45cdc', - 'pId': '59ac17c192832d0011283fe3', - 'bidFloor': 0.1, - 'ext': { - 'param1': 'loremipsum', - 'param2': 'dolorsitamet' - }, - 'placementId': 'testBanner' + bidId: '2d52001cabd527', + bidder: 'screencore', + adUnitCode: 'div-gpt-ad-12345-0', + transactionId: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', + params: { + placementId: 'testPlacement', + endpointId: 'testEndpoint' }, - 'placementCode': 'div-gpt-ad-1460505748561-0', - 'sizes': [[300, 250], [300, 600]], - 'bidderRequestId': '1fdb5ff1b6eaa7', - 'bidRequestsCount': 4, - 'bidderRequestsCount': 3, - 'bidderWinsCount': 1, - 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', - 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', - 'mediaTypes': [BANNER], - 'ortb2Imp': { - 'ext': { - 'gpid': '0123456789', - 'tid': 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf' + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + ortb2Imp: { + ext: { + gpid: '0123456789', + tid: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf' } } }; const VIDEO_BID = { - 'bidId': '2d52001cabd527', - 'adUnitCode': '63550ad1ff6642d368cba59dh5884270560', - 'bidderRequestId': '12a8ae9ada9c13', - 'transactionId': '56e184c6-bde9-497b-b9b9-cf47a61381ee', - 'bidRequestsCount': 4, - 'bidderRequestsCount': 3, - 'bidderWinsCount': 1, - 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', - 'params': { - 'subDomain': SUB_DOMAIN, - 'cId': '635509f7ff6642d368cb9837', - 'pId': '59ac17c192832d0011283fe3', - 'bidFloor': 0.1, - 'placementId': 'testBanner' + bidId: '2d52001cabd528', + bidder: 'screencore', + adUnitCode: 'video-ad-unit', + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + params: { + placementId: 'testVideoPlacement', + endpointId: 'testVideoEndpoint' }, - 'sizes': [[545, 307]], - 'mediaTypes': { - 'video': { - 'playerSize': [[545, 307]], - 'context': 'instream', - 'mimes': [ - 'video/mp4', - 'application/javascript' - ], - 'protocols': [2, 3, 5, 6], - 'maxduration': 60, - 'minduration': 0, - 'startdelay': 0, - 'linearity': 1, - 'api': [2], - 'placement': 1 + mediaTypes: { + video: { + playerSize: [[545, 307]], + context: 'instream', + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 3, 5, 6], + maxduration: 60, + minduration: 0, + startdelay: 0, + linearity: 1, + api: [2], + placement: 1 } }, - 'ortb2Imp': { - 'ext': { - 'tid': '56e184c6-bde9-497b-b9b9-cf47a61381ee' + ortb2Imp: { + ext: { + tid: '56e184c6-bde9-497b-b9b9-cf47a61381ee' } } -} - -const ORTB2_DEVICE = { - sua: { - 'source': 2, - 'platform': { - 'brand': 'Android', - 'version': ['8', '0', '0'] - }, - 'browsers': [ - {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, - {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, - {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} - ], - 'mobile': 1, - 'model': 'SM-G955U', - 'bitness': '64', - 'architecture': '' +}; + +const NATIVE_BID = { + bidId: '2d52001cabd529', + bidder: 'screencore', + adUnitCode: 'native-ad-unit', + transactionId: '77e184c6-bde9-497b-b9b9-cf47a61381ee', + params: { + placementId: 'testNativePlacement' }, - w: 980, - h: 1720, - dnt: 0, - ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/125.0.6422.80 Mobile/15E148 Safari/604.1', - language: 'en', - devicetype: 1, - make: 'Apple', - model: 'iPhone 12 Pro Max', - os: 'iOS', - osv: '17.4', - ext: {fiftyonedegrees_deviceId: '17595-133085-133468-18092'}, + mediaTypes: { + native: { + title: { required: true }, + image: { required: true }, + sponsoredBy: { required: false } + } + } }; const BIDDER_REQUEST = { - 'gdprConsent': { - 'consentString': 'consent_string', - 'gdprApplies': true - }, - 'gppString': 'gpp_string', - 'gppSid': [7], - 'uspConsent': 'consent_string', - 'refererInfo': { - 'page': 'https://www.greatsite.com', - 'ref': 'https://www.somereferrer.com' + refererInfo: { + page: 'https://www.example.com', + ref: 'https://www.referrer.com' }, - 'ortb2': { - 'site': { - 'content': { - 'language': 'en' - } - }, - 'regs': { - 'gpp': 'gpp_string', - 'gpp_sid': [7], - 'coppa': 0 - }, - 'device': ORTB2_DEVICE, + ortb2: { + device: { + w: 1920, + h: 1080, + language: 'en' + } } }; const SERVER_RESPONSE = { - body: { - cid: 'testcid123', - results: [{ - 'ad': '', - 'price': 0.8, - 'creativeId': '12610997325162499419', - 'exp': 30, - 'width': 300, - 'height': 250, - 'advertiserDomains': ['securepubads.g.doubleclick.net'], - 'cookies': [{ - 'src': 'https://sync.com', - 'type': 'iframe' - }, { - 'src': 'https://sync.com', - 'type': 'img' - }] - }] - } + body: [{ + requestId: '2d52001cabd527', + cpm: 0.8, + creativeId: '12610997325162499419', + ttl: 30, + currency: 'USD', + width: 300, + height: 250, + mediaType: 'banner', + ad: '', + adomain: ['securepubads.g.doubleclick.net'] + }] }; const VIDEO_SERVER_RESPONSE = { - body: { - 'cid': '635509f7ff6642d368cb9837', - 'results': [{ - 'ad': '', - 'advertiserDomains': ['screencore.io'], - 'exp': 60, - 'width': 545, - 'height': 307, - 'mediaType': 'video', - 'creativeId': '12610997325162499419', - 'price': 2, - 'cookies': [] - }] - } -}; - -const ORTB2_OBJ = { - "device": ORTB2_DEVICE, - "regs": {"coppa": 0, "gpp": "gpp_string", "gpp_sid": [7]}, - "site": {"content": {"language": "en"} - } + body: [{ + requestId: '2d52001cabd528', + cpm: 2, + creativeId: '12610997325162499419', + ttl: 60, + currency: 'USD', + width: 545, + height: 307, + mediaType: 'video', + vastXml: '', + adomain: ['screencore.io'] + }] }; const REQUEST = { data: { - width: 300, - height: 250, - bidId: '2d52001cabd527' + placements: [{ + bidId: '2d52001cabd527', + adFormat: 'banner', + sizes: [[300, 250], [300, 600]] + }] } }; -function getTopWindowQueryParams() { - try { - const parsedUrl = utils.parseUrl(window.top.document.URL, { decodeSearchAsString: true }); - return parsedUrl.search; - } catch (e) { - return ''; - } -} - describe('screencore bid adapter', function () { before(() => config.resetConfig()); after(() => config.resetConfig()); describe('validate spec', function () { - it('exists and is a function', function () { + it('should have isBidRequestValid as a function', function () { expect(adapter.isBidRequestValid).to.exist.and.to.be.a('function'); }); - it('exists and is a function', function () { + it('should have buildRequests as a function', function () { expect(adapter.buildRequests).to.exist.and.to.be.a('function'); }); - it('exists and is a function', function () { + it('should have interpretResponse as a function', function () { expect(adapter.interpretResponse).to.exist.and.to.be.a('function'); }); - it('exists and is a function', function () { + it('should have getUserSyncs as a function', function () { expect(adapter.getUserSyncs).to.exist.and.to.be.a('function'); }); - it('exists and is a string', function () { + it('should have code as a string', function () { expect(adapter.code).to.exist.and.to.be.a('string'); + expect(adapter.code).to.equal('screencore'); }); - it('exists and contains media types', function () { + it('should have supportedMediaTypes with BANNER, VIDEO, NATIVE', function () { expect(adapter.supportedMediaTypes).to.exist.and.to.be.an('array').with.length(3); expect(adapter.supportedMediaTypes).to.contain.members([BANNER, VIDEO, NATIVE]); }); + + it('should have gvlid', function () { + expect(adapter.gvlid).to.exist.and.to.equal(1473); + }); + + it('should have version', function () { + expect(adapter.version).to.exist.and.to.equal('1.0.0'); + }); }); describe('validate bid requests', function () { - it('should require cId', function () { + it('should return false when placementId and endpointId are missing', function () { const isValid = adapter.isBidRequestValid({ - params: { - pId: 'pid', - }, + bidId: '123', + params: {}, + mediaTypes: { banner: { sizes: [[300, 250]] } } }); expect(isValid).to.be.false; }); - it('should require pId', function () { + it('should return false when mediaTypes is missing', function () { const isValid = adapter.isBidRequestValid({ - params: { - cId: 'cid', - }, + bidId: '123', + params: { placementId: 'test' } }); expect(isValid).to.be.false; }); - it('should validate correctly', function () { + it('should return true when placementId is present with banner mediaType', function () { const isValid = adapter.isBidRequestValid({ - params: { - cId: 'cid', - pId: 'pid', - }, + bidId: '123', + params: { placementId: 'test' }, + mediaTypes: { banner: { sizes: [[300, 250]] } } + }); + expect(isValid).to.be.true; + }); + + it('should return true when endpointId is present with banner mediaType', function () { + const isValid = adapter.isBidRequestValid({ + bidId: '123', + params: { endpointId: 'test' }, + mediaTypes: { banner: { sizes: [[300, 250]] } } + }); + expect(isValid).to.be.true; + }); + + it('should return true when placementId is present with video mediaType', function () { + const isValid = adapter.isBidRequestValid({ + bidId: '123', + params: { placementId: 'test' }, + mediaTypes: { video: { playerSize: [[640, 480]] } } + }); + expect(isValid).to.be.true; + }); + + it('should return true when placementId is present with native mediaType', function () { + const isValid = adapter.isBidRequestValid({ + bidId: '123', + params: { placementId: 'test' }, + mediaTypes: { native: { title: { required: true } } } }); expect(isValid).to.be.true; }); }); describe('build requests', function () { - let sandbox; - before(function () { - getGlobal().bidderSettings = { - screencore: { - storageAllowed: true, - }, - }; - sandbox = sinon.createSandbox(); - sandbox.stub(Date, 'now').returns(1000); + it('should build banner request', function () { + const requests = adapter.buildRequests([BID], BIDDER_REQUEST); + expect(requests).to.exist; + expect(requests.method).to.equal('POST'); + expect(requests.url).to.include('screencore.io/prebid'); + expect(requests.data).to.exist; + expect(requests.data.placements).to.be.an('array'); + expect(requests.data.placements[0].bidId).to.equal(BID.bidId); + expect(requests.data.placements[0].adFormat).to.equal(BANNER); }); it('should build video request', function () { - const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); - config.setConfig({ - bidderTimeout: 3000, - }); const requests = adapter.buildRequests([VIDEO_BID], BIDDER_REQUEST); - expect(requests).to.have.length(1); - expect(requests[0]).to.deep.equal({ - method: 'POST', - url: `${createDomain()}/prebid/multi/635509f7ff6642d368cb9837`, - data: { - adUnitCode: '63550ad1ff6642d368cba59dh5884270560', - bidFloor: 0.1, - bidId: '2d52001cabd527', - bidderVersion: adapter.version, - bidderRequestId: '12a8ae9ada9c13', - cb: 1000, - gdpr: 1, - gdprConsent: 'consent_string', - usPrivacy: 'consent_string', - gppString: 'gpp_string', - gppSid: [7], - prebidVersion: version, - transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', - bidRequestsCount: 4, - bidderRequestsCount: 3, - bidderWinsCount: 1, - bidderTimeout: 3000, - publisherId: '59ac17c192832d0011283fe3', - url: 'https%3A%2F%2Fwww.greatsite.com', - referrer: 'https://www.somereferrer.com', - res: `${window.top.screen.width}x${window.top.screen.height}`, - schain: VIDEO_BID.schain, - sizes: ['545x307'], - sua: { - 'source': 2, - 'platform': { - 'brand': 'Android', - 'version': ['8', '0', '0'] - }, - 'browsers': [ - { 'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0'] }, - { 'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119'] }, - {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} - ], - 'mobile': 1, - 'model': 'SM-G955U', - 'bitness': '64', - 'architecture': '' - }, - device: ORTB2_DEVICE, - uniqueDealId: `${hashUrl}_${Date.now().toString()}`, - uqs: getTopWindowQueryParams(), - mediaTypes: { - video: { - api: [2], - context: 'instream', - linearity: 1, - maxduration: 60, - mimes: [ - 'video/mp4', - 'application/javascript' - ], - minduration: 0, - placement: 1, - playerSize: [[545, 307]], - protocols: [2, 3, 5, 6], - startdelay: 0 - } - }, - gpid: '', - cat: [], - contentLang: 'en', - contentData: [], - isStorageAllowed: true, - pagecat: [], - ortb2Imp: VIDEO_BID.ortb2Imp, - ortb2: ORTB2_OBJ, - placementId: "testBanner", - userData: [], - coppa: 0 - } - }); + expect(requests).to.exist; + expect(requests.method).to.equal('POST'); + expect(requests.data.placements).to.be.an('array'); + expect(requests.data.placements[0].bidId).to.equal(VIDEO_BID.bidId); + expect(requests.data.placements[0].adFormat).to.equal(VIDEO); }); - it('should build banner request for each size', function () { - const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); - config.setConfig({ - bidderTimeout: 3000 - }); - const requests = adapter.buildRequests([BID], BIDDER_REQUEST); - expect(requests).to.have.length(1); - expect(requests[0]).to.deep.equal({ - method: 'POST', - url: `${createDomain(SUB_DOMAIN)}/prebid/multi/59db6b3b4ffaa70004f45cdc`, - data: { - gdprConsent: 'consent_string', - gdpr: 1, - gppString: 'gpp_string', - gppSid: [7], - usPrivacy: 'consent_string', - transactionId: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', - bidRequestsCount: 4, - bidderRequestsCount: 3, - bidderWinsCount: 1, - bidderTimeout: 3000, - bidderRequestId: '1fdb5ff1b6eaa7', - sizes: ['300x250', '300x600'], - sua: { - 'source': 2, - 'platform': { - 'brand': 'Android', - 'version': ['8', '0', '0'] - }, - 'browsers': [ - { 'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0'] }, - { 'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119'] }, - {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} - ], - 'mobile': 1, - 'model': 'SM-G955U', - 'bitness': '64', - 'architecture': '' - }, - device: ORTB2_DEVICE, - url: 'https%3A%2F%2Fwww.greatsite.com', - referrer: 'https://www.somereferrer.com', - cb: 1000, - bidFloor: 0.1, - bidId: '2d52001cabd527', - adUnitCode: 'div-gpt-ad-12345-0', - publisherId: '59ac17c192832d0011283fe3', - uniqueDealId: `${hashUrl}_${Date.now().toString()}`, - bidderVersion: adapter.version, - prebidVersion: version, - schain: BID.schain, - res: `${window.top.screen.width}x${window.top.screen.height}`, - mediaTypes: [BANNER], - gpid: '0123456789', - uqs: getTopWindowQueryParams(), - 'ext.param1': 'loremipsum', - 'ext.param2': 'dolorsitamet', - cat: [], - contentLang: 'en', - contentData: [], - isStorageAllowed: true, - pagecat: [], - ortb2Imp: BID.ortb2Imp, - ortb2: ORTB2_OBJ, - placementId: "testBanner", - userData: [], - coppa: 0 - } - }); + it('should build native request', function () { + const requests = adapter.buildRequests([NATIVE_BID], BIDDER_REQUEST); + expect(requests).to.exist; + expect(requests.data.placements).to.be.an('array'); + expect(requests.data.placements[0].bidId).to.equal(NATIVE_BID.bidId); + expect(requests.data.placements[0].adFormat).to.equal(NATIVE); }); - after(function () { - getGlobal().bidderSettings = {}; - sandbox.restore(); + it('should include gpid when available', function () { + const requests = adapter.buildRequests([BID], BIDDER_REQUEST); + expect(requests.data.placements[0].gpid).to.equal('0123456789'); }); - }); - describe('getUserSyncs', function () { - it('should have valid user sync with iframeEnabled', function () { - const result = adapter.getUserSyncs({ iframeEnabled: true }, [SERVER_RESPONSE]); + it('should include placementId in placement when present', function () { + const requests = adapter.buildRequests([BID], BIDDER_REQUEST); + expect(requests.data.placements[0].placementId).to.equal('testPlacement'); + expect(requests.data.placements[0].type).to.equal('publisher'); + }); - expect(result).to.deep.equal([{ - type: 'iframe', - url: 'https://cs.screencore.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0', - }]); + it('should include endpointId in placement when placementId is not present', function () { + const bidWithEndpoint = { + bidId: '2d52001cabd530', + bidder: 'screencore', + adUnitCode: 'div-gpt-ad-endpoint', + transactionId: 'd881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', + params: { + endpointId: 'testEndpointOnly' + }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } + }; + const requests = adapter.buildRequests([bidWithEndpoint], BIDDER_REQUEST); + expect(requests.data.placements[0].endpointId).to.equal('testEndpointOnly'); + expect(requests.data.placements[0].type).to.equal('network'); }); + }); - it('should have valid user sync with cid on response', function () { + describe('getUserSyncs', function () { + it('should return iframe sync when iframeEnabled', function () { + config.setConfig({ coppa: 0 }); const result = adapter.getUserSyncs({ iframeEnabled: true }, [SERVER_RESPONSE]); - expect(result).to.deep.equal([{ - type: 'iframe', - url: 'https://cs.screencore.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0', - }]); + expect(result).to.be.an('array').with.length(1); + expect(result[0].type).to.equal('iframe'); + expect(result[0].url).to.include('https://cs.screencore.io/iframe?pbjs=1'); }); - it('should have valid user sync with pixelEnabled', function () { + it('should return image sync when pixelEnabled', function () { + config.setConfig({ coppa: 0 }); const result = adapter.getUserSyncs({ pixelEnabled: true }, [SERVER_RESPONSE]); - - expect(result).to.deep.equal([{ - 'url': 'https://cs.screencore.io/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0', - 'type': 'image', - }]); + expect(result).to.be.an('array').with.length(1); + expect(result[0].type).to.equal('image'); + expect(result[0].url).to.include('https://cs.screencore.io/image?pbjs=1'); }); - it('should have valid user sync with coppa 1 on response', function () { - config.setConfig({ - coppa: 1, - }); + it('should include coppa parameter', function () { + config.setConfig({ coppa: 1 }); const result = adapter.getUserSyncs({ iframeEnabled: true }, [SERVER_RESPONSE]); - expect(result).to.deep.equal([{ - type: 'iframe', - url: 'https://cs.screencore.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=1', - }]); + expect(result[0].url).to.include('coppa=1'); }); - it('should generate url with consent data', function () { + it('should include gdpr consent when provided', function () { + config.setConfig({ coppa: 0 }); const gdprConsent = { gdprApplies: true, - consentString: 'consent_string', + consentString: 'consent_string' }; - const uspConsent = 'usp_string'; + const result = adapter.getUserSyncs({ iframeEnabled: true }, [SERVER_RESPONSE], gdprConsent); + expect(result[0].url).to.include('gdpr=1'); + expect(result[0].url).to.include('gdpr_consent=consent_string'); + }); + + it('should include gpp consent when provided', function () { + config.setConfig({ coppa: 0 }); const gppConsent = { gppString: 'gpp_string', - applicableSections: [7], + applicableSections: [7] }; - - const result = adapter.getUserSyncs({ pixelEnabled: true }, [SERVER_RESPONSE], gdprConsent, uspConsent, gppConsent); - - expect(result).to.deep.equal([{ - 'url': 'https://cs.screencore.io/api/sync/image/?cid=testcid123&gdpr=1&gdpr_consent=consent_string&us_privacy=usp_string&coppa=1&gpp=gpp_string&gpp_sid=7', - 'type': 'image', - }]); + const result = adapter.getUserSyncs({ pixelEnabled: true }, [SERVER_RESPONSE], null, null, gppConsent); + expect(result[0].url).to.include('gpp=gpp_string'); + expect(result[0].url).to.include('gpp_sid=7'); }); }); describe('interpret response', function () { - it('should return empty array when there is no response', function () { - const responses = adapter.interpretResponse(null); - expect(responses).to.be.empty; - }); - - it('should return empty array when there is no ad', function () { - const responses = adapter.interpretResponse({ price: 1, ad: '' }); - expect(responses).to.be.empty; - }); - - it('should return empty array when there is no price', function () { - const responses = adapter.interpretResponse({ price: null, ad: 'great ad' }); + it('should return empty array when body is empty array', function () { + const responses = adapter.interpretResponse({ body: [] }); expect(responses).to.be.empty; }); it('should return an array of interpreted banner responses', function () { const responses = adapter.interpretResponse(SERVER_RESPONSE, REQUEST); expect(responses).to.have.length(1); - expect(responses[0]).to.deep.equal({ - requestId: '2d52001cabd527', - cpm: 0.8, - width: 300, - height: 250, - creativeId: '12610997325162499419', - currency: 'USD', - netRevenue: true, - ttl: 30, - ad: '', - meta: { - advertiserDomains: ['securepubads.g.doubleclick.net'], - }, - }); - }); - - it('should get meta from response metaData', function () { - const serverResponse = utils.deepClone(SERVER_RESPONSE); - serverResponse.body.results[0].metaData = { - advertiserDomains: ['screencore.io'], - agencyName: 'Agency Name', - }; - const responses = adapter.interpretResponse(serverResponse, REQUEST); - expect(responses[0].meta).to.deep.equal({ - advertiserDomains: ['screencore.io'], - agencyName: 'Agency Name', - }); + expect(responses[0].requestId).to.equal('2d52001cabd527'); + expect(responses[0].cpm).to.equal(0.8); + expect(responses[0].width).to.equal(300); + expect(responses[0].height).to.equal(250); + expect(responses[0].creativeId).to.equal('12610997325162499419'); + expect(responses[0].currency).to.equal('USD'); + expect(responses[0].ttl).to.equal(30); + expect(responses[0].ad).to.equal(''); + expect(responses[0].meta.advertiserDomains).to.deep.equal(['securepubads.g.doubleclick.net']); }); it('should return an array of interpreted video responses', function () { const responses = adapter.interpretResponse(VIDEO_SERVER_RESPONSE, REQUEST); expect(responses).to.have.length(1); - expect(responses[0]).to.deep.equal({ - requestId: '2d52001cabd527', - cpm: 2, - width: 545, - height: 307, - mediaType: 'video', - creativeId: '12610997325162499419', - currency: 'USD', - netRevenue: true, - ttl: 60, - vastXml: '', - meta: { - advertiserDomains: ['screencore.io'], - }, - }); - }); - - it('should take default TTL', function () { - const serverResponse = utils.deepClone(SERVER_RESPONSE); - delete serverResponse.body.results[0].exp; - const responses = adapter.interpretResponse(serverResponse, REQUEST); - expect(responses).to.have.length(1); - expect(responses[0].ttl).to.equal(300); + expect(responses[0].requestId).to.equal('2d52001cabd528'); + expect(responses[0].cpm).to.equal(2); + expect(responses[0].width).to.equal(545); + expect(responses[0].height).to.equal(307); + expect(responses[0].mediaType).to.equal('video'); + expect(responses[0].vastXml).to.equal(''); }); }); - describe('user id system', function () { - TEST_ID_SYSTEMS.forEach((idSystemProvider) => { - const id = Date.now().toString(); - const bid = utils.deepClone(BID); - - const userId = (function () { - switch (idSystemProvider) { - case 'lipb': - return { lipbid: id }; - case 'id5id': - return { uid: id }; - default: - return id; - } - })(); - - bid.userId = { - [idSystemProvider]: userId, - }; - - it(`should include 'uid.${idSystemProvider}' in request params`, function () { - const requests = adapter.buildRequests([bid], BIDDER_REQUEST); - expect(requests[0].data[`uid.${idSystemProvider}`]).to.equal(id); + describe('createDomain test', function () { + it('should return correct domain for US timezone', function () { + const stub = sinon.stub(Intl, 'DateTimeFormat').returns({ + resolvedOptions: () => ({ timeZone: 'America/New_York' }) }); - }); - // testing bid.userIdAsEids handling - it("should include user ids from bid.userIdAsEids (length=1)", function() { - const bid = utils.deepClone(BID); - bid.userIdAsEids = [ - { - "source": "audigent.com", - "uids": [{"id": "fakeidi6j6dlc6e"}] - } - ] - const requests = adapter.buildRequests([bid], BIDDER_REQUEST); - expect(requests[0].data['uid.audigent.com']).to.equal("fakeidi6j6dlc6e"); - }) - it("should include user ids from bid.userIdAsEids (length=2)", function() { - const bid = utils.deepClone(BID); - bid.userIdAsEids = [ - { - "source": "audigent.com", - "uids": [{"id": "fakeidi6j6dlc6e"}] - }, - { - "source": "rwdcntrl.net", - "uids": [{"id": "fakeid6f35197d5c", "atype": 1}] - } - ] - const requests = adapter.buildRequests([bid], BIDDER_REQUEST); - expect(requests[0].data['uid.audigent.com']).to.equal("fakeidi6j6dlc6e"); - expect(requests[0].data['uid.rwdcntrl.net']).to.equal("fakeid6f35197d5c"); - }) - // testing user.ext.eid handling - it("should include user ids from user.ext.eid (length=1)", function() { - const bid = utils.deepClone(BID); - bid.user = { - ext: { - eids: [ - { - "source": "pubcid.org", - "uids": [{"id": "fakeid8888dlc6e"}] - } - ] - } - } - const requests = adapter.buildRequests([bid], BIDDER_REQUEST); - expect(requests[0].data['uid.pubcid.org']).to.equal("fakeid8888dlc6e"); - }) - it("should include user ids from user.ext.eid (length=2)", function() { - const bid = utils.deepClone(BID); - bid.user = { - ext: { - eids: [ - { - "source": "pubcid.org", - "uids": [{"id": "fakeid8888dlc6e"}] - }, - { - "source": "adserver.org", - "uids": [{"id": "fakeid495ff1"}] - } - ] - } - } - const requests = adapter.buildRequests([bid], BIDDER_REQUEST); - expect(requests[0].data['uid.pubcid.org']).to.equal("fakeid8888dlc6e"); - expect(requests[0].data['uid.adserver.org']).to.equal("fakeid495ff1"); - }) - }); - - describe('alternate param names extractors', function () { - it('should return undefined when param not supported', function () { - const cid = extractCID({ 'c_id': '1' }); - const pid = extractPID({ 'p_id': '1' }); - const subDomain = extractSubDomain({ 'sub_domain': 'prebid' }); - expect(cid).to.be.undefined; - expect(pid).to.be.undefined; - expect(subDomain).to.be.undefined; - }); - it('should return value when param supported', function () { - const cid = extractCID({ 'cID': '1' }); - const pid = extractPID({ 'Pid': '2' }); - const subDomain = extractSubDomain({ 'subDOMAIN': 'prebid' }); - expect(cid).to.be.equal('1'); - expect(pid).to.be.equal('2'); - expect(subDomain).to.be.equal('prebid'); - }); - }); - - describe('unique deal id', function () { - before(function () { - getGlobal().bidderSettings = { - screencore: { - storageAllowed: true, - }, - }; - }); - after(function () { - getGlobal().bidderSettings = {}; - }); - const key = 'myKey'; - let uniqueDealId; - beforeEach(() => { - uniqueDealId = getUniqueDealId(storage, key, 0); - }); + const domain = createDomain(); + expect(domain).to.equal('https://taqus.screencore.io'); - it('should get current unique deal id', function (done) { - // waiting some time so `now` will become past - setTimeout(() => { - const current = getUniqueDealId(storage, key); - expect(current).to.be.equal(uniqueDealId); - done(); - }, 200); - }); - - it('should get new unique deal id on expiration', function (done) { - setTimeout(() => { - const current = getUniqueDealId(storage, key, 100); - expect(current).to.not.be.equal(uniqueDealId); - done(); - }, 200); + stub.restore(); }); - }); - describe('storage utils', function () { - before(function () { - getGlobal().bidderSettings = { - screencore: { - storageAllowed: true, - }, - }; - }); - after(function () { - getGlobal().bidderSettings = {}; - }); - it('should get value from storage with create param', function () { - const now = Date.now(); - const clock = useFakeTimers({ - shouldAdvanceTime: true, - now, + it('should return correct domain for EU timezone', function () { + const stub = sinon.stub(Intl, 'DateTimeFormat').returns({ + resolvedOptions: () => ({ timeZone: 'Europe/London' }) }); - setStorageItem(storage, 'myKey', 2020); - const { value, created } = getStorageItem(storage, 'myKey'); - expect(created).to.be.equal(now); - expect(value).to.be.equal(2020); - expect(typeof value).to.be.equal('number'); - expect(typeof created).to.be.equal('number'); - clock.restore(); - }); - - it('should get external stored value', function () { - const value = 'superman'; - window.localStorage.setItem('myExternalKey', value); - const item = getStorageItem(storage, 'myExternalKey'); - expect(item).to.be.equal(value); - }); - it('should parse JSON value', function () { - const data = JSON.stringify({ event: 'send' }); - const { event } = tryParseJSON(data); - expect(event).to.be.equal('send'); - }); + const domain = createDomain(); + expect(domain).to.equal('https://taqeu.screencore.io'); - it('should get original value on parse fail', function () { - const value = 21; - const parsed = tryParseJSON(value); - expect(typeof parsed).to.be.equal('number'); - expect(parsed).to.be.equal(value); + stub.restore(); }); - }); - describe('createDomain test', function () { - it('should return correct domain', function () { + it('should return correct domain for APAC timezone', function () { const stub = sinon.stub(Intl, 'DateTimeFormat').returns({ - resolvedOptions: () => ({ timeZone: 'America/New_York' }), + resolvedOptions: () => ({ timeZone: 'Asia/Tokyo' }) }); - const responses = createDomain(); - expect(responses).to.be.equal('https://taqus.screencore.io'); + const domain = createDomain(); + expect(domain).to.equal('https://taqapac.screencore.io'); stub.restore(); }); diff --git a/test/spec/modules/seedtagBidAdapter_spec.js b/test/spec/modules/seedtagBidAdapter_spec.js index db65b3dcbc7..3a448c90d78 100644 --- a/test/spec/modules/seedtagBidAdapter_spec.js +++ b/test/spec/modules/seedtagBidAdapter_spec.js @@ -31,7 +31,7 @@ function getSlotConfigs(mediaTypes, params) { bidId: '30b31c1838de1e', bidderRequestId: '22edbae2733bf6', auctionId: '1d1a030790a475', - bidRequestsCount: 1, + bidderRequestsCount: 1, bidder: 'seedtag', mediaTypes: mediaTypes, src: 'client', diff --git a/test/spec/modules/seenthisBrandStories_spec.js b/test/spec/modules/seenthisBrandStories_spec.js new file mode 100644 index 00000000000..c565a33fa88 --- /dev/null +++ b/test/spec/modules/seenthisBrandStories_spec.js @@ -0,0 +1,333 @@ +import { expect } from "chai"; +import { + addStyleToSingleChildAncestors, + applyAutoHeight, + applyFullWidth, + calculateMargins, + DEFAULT_MARGINS, + findAdWrapper, + getFrameByEvent, + SEENTHIS_EVENTS, +} from "modules/seenthisBrandStories.ts"; +import * as boundingClientRect from "../../../libraries/boundingClientRect/boundingClientRect.js"; +import * as utils from "../../../src/utils.js"; +import * as winDimensions from "src/utils/winDimensions.js"; + +describe("seenthisBrandStories", function () { + describe("constants", function () { + it("should have correct DEFAULT_MARGINS", function () { + expect(DEFAULT_MARGINS).to.equal("16px"); + }); + + it("should have correct SEENTHIS_EVENTS array", function () { + expect(SEENTHIS_EVENTS).to.be.an("array").with.length(9); + expect(SEENTHIS_EVENTS).to.include("@seenthis_storylines/ready"); + expect(SEENTHIS_EVENTS).to.include("@seenthis_enabled"); + expect(SEENTHIS_EVENTS).to.include("@seenthis_modal/opened"); + }); + }); + + describe("calculateMargins", function () { + let mockElement; + let getBoundingClientRectStub; + let getComputedStyleStub; + + beforeEach(function () { + mockElement = { + style: { + setProperty: sinon.stub(), + }, + }; + + getBoundingClientRectStub = sinon.stub( + boundingClientRect, + "getBoundingClientRect" + ); + getComputedStyleStub = sinon.stub(window, "getComputedStyle"); + }); + + afterEach(function () { + sinon.restore(); + }); + + it("should set margins correctly with non-zero values", function () { + getBoundingClientRectStub.returns({ left: 32, width: 300 }); + getComputedStyleStub.returns({ marginLeft: "16px" }); + + calculateMargins(mockElement); + + expect( + mockElement.style.setProperty.calledWith( + "--storylines-margin-left", + "-16px" + ) + ).to.be.true; + expect( + mockElement.style.setProperty.calledWith("--storylines-margins", "32px") + ).to.be.true; + }); + + it("should use default margins when width is 0", function () { + getBoundingClientRectStub.returns({ left: 16, width: 0 }); + getComputedStyleStub.returns({ marginLeft: "0px" }); + + calculateMargins(mockElement); + + expect( + mockElement.style.setProperty.calledWith("--storylines-margins", "16px") + ).to.be.true; + expect( + mockElement.style.setProperty.calledWith( + "--storylines-margin-left", + "16px" + ) + ).to.be.true; + }); + + it("should use default margins when margin left is 0", function () { + getBoundingClientRectStub.returns({ left: 16, width: 300 }); + getComputedStyleStub.returns({ marginLeft: "16px" }); + + calculateMargins(mockElement); + + expect( + mockElement.style.setProperty.calledWith("--storylines-margins", "16px") + ).to.be.true; + expect( + mockElement.style.setProperty.calledWith( + "--storylines-margin-left", + "16px" + ) + ).to.be.true; + }); + }); + + describe("getFrameByEvent", function () { + let getElementsByTagNameStub; + let mockIframes; + let mockEventSource; + + beforeEach(function () { + mockEventSource = { id: "frame2" }; + + mockIframes = [ + { contentWindow: { id: "frame1" } }, + { contentWindow: mockEventSource }, // This will match + { contentWindow: { id: "frame3" } }, + ]; + + getElementsByTagNameStub = sinon + .stub(document, "getElementsByTagName") + .returns(mockIframes); + }); + + afterEach(function () { + sinon.restore(); + }); + + it("should return iframe matching event source", function () { + const mockEvent = { + source: mockEventSource, // This should match mockIframes[1].contentWindow + }; + + const result = getFrameByEvent(mockEvent); + + expect(result).to.equal(mockIframes[1]); + expect(getElementsByTagNameStub.calledWith("iframe")).to.be.true; + }); + + it("should return undefined if no iframe matches", function () { + const mockEvent = { + source: { id: "nonexistent" }, // This won't match any iframe + }; + + const result = getFrameByEvent(mockEvent); + + expect(result).to.be.null; + }); + }); + + describe("addStyleToSingleChildAncestors", function () { + beforeEach(function () { + sinon + .stub(winDimensions, "getWinDimensions") + .returns({ innerWidth: 1024, innerHeight: 768 }); + }); + + afterEach(function () { + sinon.restore(); + }); + + it("should apply style to element when width is less than window width", function () { + const mockElement = { + style: { + setProperty: sinon.stub(), + width: "", // key exists + }, + offsetWidth: 400, + parentElement: null, + }; + + addStyleToSingleChildAncestors(mockElement, { + key: "width", + value: "100%", + }); + + expect(mockElement.style.setProperty.calledWith("width", "100%")).to.be + .true; + }); + + it("should not apply style when element width equals window width", function () { + const mockElement = { + style: { + setProperty: sinon.stub(), + width: "", + }, + offsetWidth: 1024, + parentElement: null, + }; + + addStyleToSingleChildAncestors(mockElement, { + key: "width", + value: "100%", + }); + + expect(mockElement.style.setProperty.called).to.be.false; + }); + + it("should recursively apply to single child ancestors", function () { + const grandParent = { + style: { + setProperty: sinon.stub(), + width: "", + }, + offsetWidth: 800, + parentElement: null, + children: { length: 1 }, + }; + + const parent = { + style: { + setProperty: sinon.stub(), + width: "", + }, + offsetWidth: 600, + parentElement: grandParent, + children: { length: 1 }, + }; + + const child = { + style: { + setProperty: sinon.stub(), + width: "", + }, + offsetWidth: 400, + parentElement: parent, + }; + + addStyleToSingleChildAncestors(child, { key: "width", value: "100%" }); + + expect(child.style.setProperty.calledWith("width", "100%")).to.be.true; + expect(parent.style.setProperty.calledWith("width", "100%")).to.be.true; + expect(grandParent.style.setProperty.calledWith("width", "100%")).to.be + .true; + }); + + it("should stop recursion when parent has multiple children", function () { + const parent = { + style: { + setProperty: sinon.stub(), + width: "", + }, + offsetWidth: 600, + parentElement: null, + children: { length: 2 }, // Multiple children + }; + + const child = { + style: { + setProperty: sinon.stub(), + width: "", + }, + offsetWidth: 400, + parentElement: parent, + }; + + addStyleToSingleChildAncestors(child, { key: "width", value: "100%" }); + + expect(child.style.setProperty.calledWith("width", "100%")).to.be.true; + expect(parent.style.setProperty.called).to.be.false; + }); + + it("should not apply style when key is not in element style", function () { + const mockElement = { + style: { + setProperty: sinon.stub(), + // 'width' key not present + }, + offsetWidth: 400, + parentElement: null, + }; + + addStyleToSingleChildAncestors(mockElement, { + key: "width", + value: "100%", + }); + + expect(mockElement.style.setProperty.called).to.be.false; + }); + }); + + describe("findAdWrapper", function () { + it("should return grandparent element", function () { + const grandParent = {}; + const parent = { parentElement: grandParent }; + const target = { parentElement: parent }; + + const result = findAdWrapper(target); + + expect(result).to.equal(grandParent); + }); + }); + + describe("applyFullWidth", function () { + let findAdWrapperStub; + let addStyleToSingleChildAncestorsStub; + + beforeEach(function () { + findAdWrapperStub = sinon.stub(); + addStyleToSingleChildAncestorsStub = sinon.stub(); + }); + + afterEach(function () { + sinon.restore(); + }); + + it("should call addStyleToSingleChildAncestors with width 100% when adWrapper exists", function () { + const mockTarget = {}; + + expect(() => applyFullWidth(mockTarget)).to.not.throw(); + }); + + it("should handle null adWrapper gracefully", function () { + const mockTarget = {}; + + expect(() => applyFullWidth(mockTarget)).to.not.throw(); + }); + }); + + describe("applyAutoHeight", function () { + it("should call addStyleToSingleChildAncestors with height auto when adWrapper exists", function () { + const mockTarget = {}; + + // Test that function executes without errors + expect(() => applyAutoHeight(mockTarget)).to.not.throw(); + }); + + it("should handle null adWrapper gracefully", function () { + const mockTarget = {}; + + expect(() => applyAutoHeight(mockTarget)).to.not.throw(); + }); + }); +}); diff --git a/test/spec/modules/sevioBidAdapter_spec.js b/test/spec/modules/sevioBidAdapter_spec.js index 9e6050640c2..63b36465dad 100644 --- a/test/spec/modules/sevioBidAdapter_spec.js +++ b/test/spec/modules/sevioBidAdapter_spec.js @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { spec } from 'modules/sevioBidAdapter.js'; - +import { config } from 'src/config.js'; const ENDPOINT_URL = 'https://req.adx.ws/prebid'; describe('sevioBidAdapter', function () { @@ -274,4 +274,251 @@ describe('sevioBidAdapter', function () { expect(requests[0].data.keywords).to.have.property('tokens'); expect(requests[0].data.keywords.tokens).to.deep.equal(['keyword1', 'keyword2']); }); + + // Minimal env shims some helpers rely on + Object.defineProperty(window, 'visualViewport', { + value: { width: 1200, height: 800 }, + configurable: true + }); + Object.defineProperty(window, 'screen', { + value: { width: 1920, height: 1080 }, + configurable: true + }); + + function mkBid(overrides) { + return Object.assign({ + bidId: 'bid-1', + bidder: 'sevio', + params: { zone: 'zone-123', referenceId: 'ref-abc', keywords: ['k1', 'k2'] }, + mediaTypes: { banner: { sizes: [[300, 250]] } }, + refererInfo: { page: 'https://example.com/page', referer: 'https://referrer.example' }, + userIdAsEids: [] + }, overrides || {}); + } + + const baseBidderRequest = { + timeout: 1200, + refererInfo: { page: 'https://example.com/page', referer: 'https://referrer.example' }, + gdprConsent: { consentString: 'TCF-STRING' }, + uspConsent: { uspString: '1NYN' }, + gppConsent: { consentString: 'GPP-STRING' }, + ortb2: { device: {}, ext: {} } + }; + + describe('Sevio adapter helper coverage via buildRequests (JS)', () => { + let stubs = []; + + afterEach(() => { + while (stubs.length) stubs.pop().restore(); + document.title = ''; + document.head.innerHTML = ''; + try { + Object.defineProperty(navigator, 'connection', { value: undefined, configurable: true }); + } catch (e) {} + }); + + it('getReferrerInfo → data.referer', () => { + const out = spec.buildRequests([mkBid()], baseBidderRequest); + expect(out).to.have.lengthOf(1); + expect(out[0].data.referer).to.equal('https://example.com/page'); + }); + + it('getPageTitle prefers top.title; falls back to og:title (top document)', () => { + window.top.document.title = 'Doc Title'; + let out = spec.buildRequests([mkBid()], baseBidderRequest); + expect(out[0].data.context[0].text).to.equal('Doc Title'); + + window.top.document.title = ''; + const meta = window.top.document.createElement('meta'); + meta.setAttribute('property', 'og:title'); + meta.setAttribute('content', 'OG Title'); + window.top.document.head.appendChild(meta); + + out = spec.buildRequests([mkBid()], baseBidderRequest); + expect(out[0].data.context[0].text).to.equal('OG Title'); + + meta.remove(); + }); + + it('getPageTitle cross-origin fallback (window.top throws) uses local document.*', function () { + document.title = 'Local Title'; + + // In jsdom, window.top === window; try to simulate cross-origin by throwing from getter. + let restored = false; + try { + const original = Object.getOwnPropertyDescriptor(window, 'top'); + Object.defineProperty(window, 'top', { + configurable: true, + get() { throw new Error('cross-origin'); } + }); + const out = spec.buildRequests([mkBid()], baseBidderRequest); + expect(out[0].data.context[0].text).to.equal('Local Title'); + Object.defineProperty(window, 'top', original); + restored = true; + } catch (e) { + // Environment didn’t allow redefining window.top; skip this case + this.skip(); + } finally { + if (!restored) { + try { Object.defineProperty(window, 'top', { value: window, configurable: true }); } catch (e) {} + } + } + }); + + it('computeTTFB via navigation entries (top.performance) and cached within call', () => { + const perfTop = window.top.performance; + + const original = perfTop.getEntriesByType; + Object.defineProperty(perfTop, 'getEntriesByType', { + configurable: true, writable: true, + value: (type) => (type === 'navigation' ? [{ responseStart: 152, requestStart: 100 }] : []) + }); + + const out = spec.buildRequests([mkBid({ bidId: 'A' }), mkBid({ bidId: 'B' })], baseBidderRequest); + expect(out).to.have.lengthOf(2); + expect(out[0].data.timeToFirstByte).to.equal('52'); + expect(out[1].data.timeToFirstByte).to.equal('52'); + + Object.defineProperty(perfTop, 'getEntriesByType', { configurable: true, writable: true, value: original }); + }); + + it('computeTTFB falls back to top.performance.timing when no navigation entries', () => { + const perfTop = window.top.performance; + const originalGetEntries = perfTop.getEntriesByType; + const originalTimingDesc = Object.getOwnPropertyDescriptor(perfTop, 'timing'); + + Object.defineProperty(perfTop, 'getEntriesByType', { + configurable: true, writable: true, value: () => [] + }); + + Object.defineProperty(perfTop, 'timing', { + configurable: true, + value: { responseStart: 250, requestStart: 200 } + }); + + const out = spec.buildRequests([mkBid()], baseBidderRequest); + expect(out[0].data.timeToFirstByte).to.equal('50'); + + Object.defineProperty(perfTop, 'getEntriesByType', { + configurable: true, writable: true, value: originalGetEntries + }); + if (originalTimingDesc) { + Object.defineProperty(perfTop, 'timing', originalTimingDesc); + } else { + Object.defineProperty(perfTop, 'timing', { configurable: true, value: undefined }); + } + }); + + it('handles multiple sizes correctly', function () { + const multiSizeBidRequests = [ + { + bidder: 'sevio', + params: { zone: 'zoneId' }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90], + [160, 600], + ] + } + }, + bidId: 'multi123', + } + ]; + + const bidderRequests = { + refererInfo: { + numIframes: 0, + reachedTop: true, + referer: 'https://example.com', + stack: ['https://example.com'] + } + }; + + const request = spec.buildRequests(multiSizeBidRequests, bidderRequests); + const sizes = request[0].data.ads[0].sizes; + + expect(sizes).to.deep.equal([ + { width: 300, height: 250 }, + { width: 728, height: 90 }, + { width: 160, height: 600 }, + ]); + }); + }); + + describe('currency handling', function () { + let bidRequests; + let bidderRequests; + + beforeEach(function () { + bidRequests = [{ + bidder: 'sevio', + params: { zone: 'zoneId' }, + mediaTypes: { banner: { sizes: [[300, 250]] } }, + bidId: '123' + }]; + + bidderRequests = { + refererInfo: { + referer: 'https://example.com', + page: 'https://example.com', + } + }; + }); + + afterEach(function () { + if (typeof config.resetConfig === 'function') { + config.resetConfig(); + } else if (typeof config.setConfig === 'function') { + config.setConfig({ currency: null }); + } + }); + + it('includes EUR currency when EUR is set in prebid config', function () { + config.setConfig({ + currency: { + adServerCurrency: 'EUR' + } + }); + + const req = spec.buildRequests(bidRequests, bidderRequests); + const payload = req[0].data; + + expect(payload.currency).to.equal('EUR'); + }); + + it('includes GBP currency when GBP is set in prebid config', function () { + config.setConfig({ + currency: { + adServerCurrency: 'GBP' + } + }); + + const req = spec.buildRequests(bidRequests, bidderRequests); + const payload = req[0].data; + + expect(payload.currency).to.equal('GBP'); + }); + + it('does NOT include currency when no currency config is set', function () { + const req = spec.buildRequests(bidRequests, bidderRequests); + const payload = req[0].data; + + expect(payload).to.not.have.property('currency'); + }); + + it('parses comma-separated keywords string into tokens array', function () { + const singleBidRequest = [{ + bidder: 'sevio', + params: { zone: 'zoneId', keywords: 'play, games, fun ' }, // string CSV + mediaTypes: { banner: { sizes: [[728, 90]] } }, + bidId: 'bid-kw-str' + }]; + const requests = spec.buildRequests(singleBidRequest, baseBidderRequest); + expect(requests).to.be.an('array').that.is.not.empty; + expect(requests[0].data).to.have.nested.property('keywords.tokens'); + expect(requests[0].data.keywords.tokens).to.deep.equal(['play', 'games', 'fun']); + }); + }); }); diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index 42641ccca1f..56f238ff1ba 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -4,9 +4,9 @@ import * as sinon from 'sinon'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config'; import * as utils from 'src/utils'; -import { deepSetValue } from '../../../src/utils.js'; +import * as equativUtils from '../../../libraries/equativUtils/equativUtils.js'; import { getImpIdMap, setIsEqtvTest } from '../../../modules/sharethroughBidAdapter.js'; -import * as equativUtils from '../../../libraries/equativUtils/equativUtils.js' +import { deepSetValue } from '../../../src/utils.js'; const spec = newBidder(sharethroughAdapterSpec).getSpec(); @@ -73,7 +73,7 @@ describe('sharethrough adapter spec', function () { bidder: 'sharethrough', params: { pkey: 111, - equativNetworkId: 73 + equativNetworkId: 73, }, requestId: 'efgh5678', ortb2Imp: { @@ -81,7 +81,7 @@ describe('sharethrough adapter spec', function () { tid: 'zsfgzzg', }, }, - } + }, ]; const videoBidRequests = [ @@ -113,7 +113,7 @@ describe('sharethrough adapter spec', function () { bidder: 'sharethrough', params: { pkey: 111, - equativNetworkIdId: 73 + equativNetworkIdId: 73, }, requestId: 'abcd1234', ortb2Imp: { @@ -121,65 +121,71 @@ describe('sharethrough adapter spec', function () { tid: 'zsgzgzz', }, }, - } + }, ]; const nativeOrtbRequest = { - assets: [{ - id: 0, - required: 1, - title: { - len: 140 - } - }, - { - id: 1, - required: 1, - img: { - type: 3, - w: 300, - h: 600 - } - }, - { - id: 2, - required: 1, - data: { - type: 1 - } - }], + assets: [ + { + id: 0, + required: 1, + title: { + len: 140, + }, + }, + { + id: 1, + required: 1, + img: { + type: 3, + w: 300, + h: 600, + }, + }, + { + id: 2, + required: 1, + data: { + type: 1, + }, + }, + ], context: 1, - eventtrackers: [{ - event: 1, - methods: [1, 2] - }], + eventtrackers: [ + { + event: 1, + methods: [1, 2], + }, + ], plcmttype: 1, privacy: 1, ver: '1.2', }; - const nativeBidRequests = [{ - bidder: 'sharethrough', - adUnitCode: 'sharethrough_native_42', - bidId: 'bidId3', - sizes: [], - mediaTypes: { - native: { - ...nativeOrtbRequest + const nativeBidRequests = [ + { + bidder: 'sharethrough', + adUnitCode: 'sharethrough_native_42', + bidId: 'bidId3', + sizes: [], + mediaTypes: { + native: { + ...nativeOrtbRequest, + }, }, - }, - nativeOrtbRequest, - params: { - pkey: 777, - equativNetworkId: 73 - }, - requestId: 'sharethrough_native_reqid_42', - ortb2Imp: { - ext: { - tid: 'sharethrough_native_tid_42', + nativeOrtbRequest, + params: { + pkey: 777, + equativNetworkId: 73, + }, + requestId: 'sharethrough_native_reqid_42', + ortb2Imp: { + ext: { + tid: 'sharethrough_native_tid_42', + }, }, }, - }] + ]; beforeEach(() => { config.setConfig({ @@ -334,9 +340,9 @@ describe('sharethrough adapter spec', function () { hp: 1, }, ], - } - } - } + }, + }, + }, }, getFloor: () => ({ currency: 'USD', floor: 42 }), }, @@ -383,14 +389,14 @@ describe('sharethrough adapter spec', function () { mediaTypes: { banner: bannerBidRequests[0].mediaTypes.banner, video: videoBidRequests[0].mediaTypes.video, - native: nativeBidRequests[0].mediaTypes.native + native: nativeBidRequests[0].mediaTypes.native, }, sizes: [], nativeOrtbRequest, bidder: 'sharethrough', params: { pkey: 111, - equativNetworkId: 73 + equativNetworkId: 73, }, requestId: 'efgh5678', ortb2Imp: { @@ -403,8 +409,8 @@ describe('sharethrough adapter spec', function () { return { floor: 1.1 }; } return { floor: 0.9 }; - } - } + }, + }, ]; bidderRequest = { @@ -422,7 +428,7 @@ describe('sharethrough adapter spec', function () { afterEach(() => { setIsEqtvTest(null); - }) + }); describe('buildRequests', function () { describe('top level object', () => { @@ -631,32 +637,38 @@ describe('sharethrough adapter spec', function () { regs: { ext: { dsa: { - 'dsarequired': 1, - 'pubrender': 0, - 'datatopub': 1, - 'transparency': [{ - 'domain': 'good-domain', - 'dsaparams': [1, 2] - }, { - 'domain': 'bad-setup', - 'dsaparams': ['1', 3] - }] - } - } - } - } + dsarequired: 1, + pubrender: 0, + datatopub: 1, + transparency: [ + { + domain: 'good-domain', + dsaparams: [1, 2], + }, + { + domain: 'bad-setup', + dsaparams: ['1', 3], + }, + ], + }, + }, + }, + }; const openRtbReq = spec.buildRequests(bidRequests, bidderRequest)[0].data; expect(openRtbReq.regs.ext.dsa.dsarequired).to.equal(1); expect(openRtbReq.regs.ext.dsa.pubrender).to.equal(0); expect(openRtbReq.regs.ext.dsa.datatopub).to.equal(1); - expect(openRtbReq.regs.ext.dsa.transparency).to.deep.equal([{ - 'domain': 'good-domain', - 'dsaparams': [1, 2] - }, { - 'domain': 'bad-setup', - 'dsaparams': ['1', 3] - }]); + expect(openRtbReq.regs.ext.dsa.transparency).to.deep.equal([ + { + domain: 'good-domain', + dsaparams: [1, 2], + }, + { + domain: 'bad-setup', + dsaparams: ['1', 3], + }, + ]); }); }); @@ -723,7 +735,7 @@ describe('sharethrough adapter spec', function () { // act const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; - const ACTUAL_BATTR_VALUES = builtRequest.data.imp[0].banner.battr + const ACTUAL_BATTR_VALUES = builtRequest.data.imp[0].banner.battr; // assert expect(ACTUAL_BATTR_VALUES).to.deep.equal(EXPECTED_BATTR_VALUES); @@ -747,7 +759,7 @@ describe('sharethrough adapter spec', function () { // act const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; - const ACTUAL_BATTR_VALUES = builtRequest.data.imp[0].banner.battr + const ACTUAL_BATTR_VALUES = builtRequest.data.imp[0].banner.battr; // assert expect(ACTUAL_BATTR_VALUES).to.deep.equal(EXPECTED_BATTR_VALUES); @@ -761,7 +773,7 @@ describe('sharethrough adapter spec', function () { // act const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; - const ACTUAL_BATTR_VALUES = builtRequest.data.imp[0].banner.battr + const ACTUAL_BATTR_VALUES = builtRequest.data.imp[0].banner.battr; // assert expect(ACTUAL_BATTR_VALUES).to.deep.equal(EXPECTED_BATTR_VALUES); @@ -838,18 +850,34 @@ describe('sharethrough adapter spec', function () { it('should not set a property if no corresponding property is detected on mediaTypes.video', () => { // arrange const propertiesToConsider = [ - 'api', 'battr', 'companionad', 'companiontype', 'delivery', 'linearity', 'maxduration', 'mimes', 'minduration', 'placement', 'playbackmethod', 'plcmt', 'protocols', 'skip', 'skipafter', 'skipmin', 'startdelay' - ] + 'api', + 'battr', + 'companionad', + 'companiontype', + 'delivery', + 'linearity', + 'maxduration', + 'mimes', + 'minduration', + 'placement', + 'playbackmethod', + 'plcmt', + 'protocols', + 'skip', + 'skipafter', + 'skipmin', + 'startdelay', + ]; // act - propertiesToConsider.forEach(propertyToConsider => { + propertiesToConsider.forEach((propertyToConsider) => { delete bidRequests[1].mediaTypes.video[propertyToConsider]; }); const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[1]; const videoImp = builtRequest.data.imp[0].video; // assert - propertiesToConsider.forEach(propertyToConsider => { + propertiesToConsider.forEach((propertyToConsider) => { expect(videoImp[propertyToConsider]).to.be.undefined; }); }); @@ -867,43 +895,6 @@ describe('sharethrough adapter spec', function () { }); }); - describe('cookie deprecation', () => { - it('should not add cdep if we do not get it in an impression request', () => { - const builtRequests = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id', - ortb2: { - device: { - ext: { - propThatIsNotCdep: 'value-we-dont-care-about', - }, - }, - }, - }); - const noCdep = builtRequests.every((builtRequest) => { - const ourCdepValue = builtRequest.data.device?.ext?.cdep; - return ourCdepValue === undefined; - }); - expect(noCdep).to.be.true; - }); - - it('should add cdep if we DO get it in an impression request', () => { - const builtRequests = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id', - ortb2: { - device: { - ext: { - cdep: 'cdep-value', - }, - }, - }, - }); - const cdepPresent = builtRequests.every((builtRequest) => { - return builtRequest.data.device.ext.cdep === 'cdep-value'; - }); - expect(cdepPresent).to.be.true; - }); - }); - describe('first party data', () => { const firstPartyData = { site: { @@ -990,30 +981,27 @@ describe('sharethrough adapter spec', function () { describe('isEqtvTest', () => { it('should set publisher id if equativNetworkId param is present', () => { - const builtRequest = spec.buildRequests(multiImpBidRequests, bidderRequest)[0] - expect(builtRequest.data.site.publisher.id).to.equal(73) - }) + const builtRequest = spec.buildRequests(multiImpBidRequests, bidderRequest)[0]; + expect(builtRequest.data.site.publisher.id).to.equal(73); + }); it('should not set publisher id if equativNetworkId param is not present', () => { const bidRequest = { ...bidRequests[0], params: { ...bidRequests[0].params, - equativNetworkId: undefined - } - } + equativNetworkId: undefined, + }, + }; - const builtRequest = spec.buildRequests([bidRequest], bidderRequest)[0] - expect(builtRequest.data.site.publisher).to.equal(undefined) - }) + const builtRequest = spec.buildRequests([bidRequest], bidderRequest)[0]; + expect(builtRequest.data.site.publisher).to.equal(undefined); + }); it('should generate a 14-char id for each imp object', () => { - const request = spec.buildRequests( - bannerBidRequests, - bidderRequest - ); + const request = spec.buildRequests(bannerBidRequests, bidderRequest); - request[0].data.imp.forEach(imp => { + request[0].data.imp.forEach((imp) => { expect(imp.id).to.have.lengthOf(14); }); }); @@ -1022,24 +1010,21 @@ describe('sharethrough adapter spec', function () { const bids = [ { ...bannerBidRequests[0], - getFloor: ({ size }) => ({ floor: size[0] * size[1] / 100_000 }) - } + getFloor: ({ size }) => ({ floor: (size[0] * size[1]) / 100_000 }), + }, ]; - const request = spec.buildRequests( - bids, - bidderRequest - ); + const request = spec.buildRequests(bids, bidderRequest); expect(request[0].data.imp).to.have.lengthOf(2); const firstImp = request[0].data.imp[0]; - expect(firstImp.bidfloor).to.equal(300 * 250 / 100_000); + expect(firstImp.bidfloor).to.equal((300 * 250) / 100_000); expect(firstImp.banner.format).to.have.lengthOf(1); expect(firstImp.banner.format[0]).to.deep.equal({ w: 300, h: 250 }); const secondImp = request[0].data.imp[1]; - expect(secondImp.bidfloor).to.equal(300 * 600 / 100_000); + expect(secondImp.bidfloor).to.equal((300 * 600) / 100_000); expect(secondImp.banner.format).to.have.lengthOf(1); expect(secondImp.banner.format[0]).to.deep.equal({ w: 300, h: 600 }); }); @@ -1064,7 +1049,7 @@ describe('sharethrough adapter spec', function () { // expect(secondImp).to.not.have.property('native'); // expect(secondImp).to.have.property('video'); // }); - }) + }); it('should return correct native properties from ORTB converter', () => { if (FEATURES.NATIVE) { @@ -1074,19 +1059,19 @@ describe('sharethrough adapter spec', function () { const asset1 = assets[0]; expect(asset1.id).to.equal(0); expect(asset1.required).to.equal(1); - expect(asset1.title).to.deep.equal({ 'len': 140 }); + expect(asset1.title).to.deep.equal({ len: 140 }); const asset2 = assets[1]; expect(asset2.id).to.equal(1); expect(asset2.required).to.equal(1); - expect(asset2.img).to.deep.equal({ 'type': 3, 'w': 300, 'h': 600 }); + expect(asset2.img).to.deep.equal({ type: 3, w: 300, h: 600 }); const asset3 = assets[2]; expect(asset3.id).to.equal(2); expect(asset3.required).to.equal(1); - expect(asset3.data).to.deep.equal({ 'type': 1 }) + expect(asset3.data).to.deep.equal({ type: 1 }); } - }) + }); }); describe('interpretResponse', function () { @@ -1148,7 +1133,7 @@ describe('sharethrough adapter spec', function () { it('should set requestId from impIdMap when isEqtvTest is true', () => { setIsEqtvTest(true); - request = spec.buildRequests(bannerBidRequests, bidderRequest)[0] + request = spec.buildRequests(bannerBidRequests, bidderRequest)[0]; response = { body: { seatbid: [ @@ -1172,15 +1157,15 @@ describe('sharethrough adapter spec', function () { }; const impIdMap = getImpIdMap(); - impIdMap['aaaabbbbccccdd'] = 'abcd1234' + impIdMap['aaaabbbbccccdd'] = 'abcd1234'; const resp = spec.interpretResponse(response, request)[0]; - expect(resp.requestId).to.equal('abcd1234') - }) + expect(resp.requestId).to.equal('abcd1234'); + }); it('should set ttl when bid.exp is a number > 0', () => { - request = spec.buildRequests(bannerBidRequests, bidderRequest)[0] + request = spec.buildRequests(bannerBidRequests, bidderRequest)[0]; response = { body: { seatbid: [ @@ -1196,7 +1181,7 @@ describe('sharethrough adapter spec', function () { dealid: 'deal', adomain: ['domain.com'], adm: 'markup', - exp: 100 + exp: 100, }, ], }, @@ -1206,10 +1191,10 @@ describe('sharethrough adapter spec', function () { const resp = spec.interpretResponse(response, request)[0]; expect(resp.ttl).to.equal(100); - }) + }); it('should set ttl to 360 when bid.exp is a number <= 0', () => { - request = spec.buildRequests(bannerBidRequests, bidderRequest)[0] + request = spec.buildRequests(bannerBidRequests, bidderRequest)[0]; response = { body: { seatbid: [ @@ -1225,7 +1210,7 @@ describe('sharethrough adapter spec', function () { dealid: 'deal', adomain: ['domain.com'], adm: 'markup', - exp: -1 + exp: -1, }, ], }, @@ -1235,16 +1220,16 @@ describe('sharethrough adapter spec', function () { const resp = spec.interpretResponse(response, request)[0]; expect(resp.ttl).to.equal(360); - }) + }); it('should return correct properties when fledgeAuctionEnabled is true and isEqtvTest is false', () => { - request = spec.buildRequests(bidRequests, bidderRequest)[0] + request = spec.buildRequests(bidRequests, bidderRequest)[0]; response = { body: { ext: { auctionConfigs: { - key: 'value' - } + key: 'value', + }, }, seatbid: [ { @@ -1259,7 +1244,7 @@ describe('sharethrough adapter spec', function () { dealid: 'deal', adomain: ['domain.com'], adm: 'markup', - exp: -1 + exp: -1, }, { id: 'efgh5678', @@ -1271,7 +1256,7 @@ describe('sharethrough adapter spec', function () { dealid: 'deal', adomain: ['domain.com'], adm: 'markup', - exp: -1 + exp: -1, }, ], }, @@ -1281,8 +1266,8 @@ describe('sharethrough adapter spec', function () { const resp = spec.interpretResponse(response, request); expect(resp.bids.length).to.equal(2); - expect(resp.paapi).to.deep.equal({ 'key': 'value' }) - }) + expect(resp.paapi).to.deep.equal({ key: 'value' }); + }); }); describe('video', () => { @@ -1354,9 +1339,9 @@ describe('sharethrough adapter spec', function () { it('should set correct ortb property', () => { const resp = spec.interpretResponse(response, request)[0]; - expect(resp.native.ortb).to.deep.equal({ 'ad': 'ad' }) - }) - }) + expect(resp.native.ortb).to.deep.equal({ ad: 'ad' }); + }); + }); describe('meta object', () => { beforeEach(() => { @@ -1397,8 +1382,8 @@ describe('sharethrough adapter spec', function () { expect(bid.meta.brandName).to.be.null; expect(bid.meta.demandSource).to.be.null; expect(bid.meta.dchain).to.be.null; - expect(bid.meta.primaryCatId).to.be.null; - expect(bid.meta.secondaryCatIds).to.be.null; + expect(bid.meta.primaryCatId).to.equal(''); + expect(bid.meta.secondaryCatIds).to.be.an('array').that.is.empty; expect(bid.meta.mediaType).to.be.null; }); @@ -1482,15 +1467,14 @@ describe('sharethrough adapter spec', function () { it('should call handleCookieSync with correct parameters and return its result', () => { setIsEqtvTest(true); - const expectedResult = [ - { type: 'iframe', url: 'https://sync.example.com' }, - ]; + const expectedResult = [{ type: 'iframe', url: 'https://sync.example.com' }]; - handleCookieSyncStub.returns(expectedResult) + handleCookieSyncStub.returns(expectedResult); - const result = spec.getUserSyncs({ iframeEnabled: true }, - SAMPLE_RESPONSE, - { gdprApplies: true, vendorData: { vendor: { consents: {} } } }); + const result = spec.getUserSyncs({ iframeEnabled: true }, SAMPLE_RESPONSE, { + gdprApplies: true, + vendorData: { vendor: { consents: {} } }, + }); sinon.assert.calledWithMatch( handleCookieSyncStub, diff --git a/test/spec/modules/shinezRtbBidAdapter_spec.js b/test/spec/modules/shinezRtbBidAdapter_spec.js index 443999989da..2dd33d7ef5b 100644 --- a/test/spec/modules/shinezRtbBidAdapter_spec.js +++ b/test/spec/modules/shinezRtbBidAdapter_spec.js @@ -14,7 +14,7 @@ import { tryParseJSON, getUniqueDealId, } from '../../../libraries/vidazooUtils/bidderUtils.js'; -import {parseUrl, deepClone} from 'src/utils.js'; +import {parseUrl, deepClone, getWinDimensions} from 'src/utils.js'; import {version} from 'package.json'; import {useFakeTimers} from 'sinon'; import {BANNER, VIDEO} from '../../../src/mediaTypes.js'; @@ -428,7 +428,7 @@ describe('ShinezRtbBidAdapter', function () { bidderVersion: adapter.version, prebidVersion: version, schain: BID.schain, - res: `${window.top.screen.width}x${window.top.screen.height}`, + res: `${getWinDimensions().screen.width}x${getWinDimensions().screen.height}`, mediaTypes: [BANNER], gpid: '0123456789', uqs: getTopWindowQueryParams(), diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index 58608705073..c442b1d53db 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -240,55 +240,6 @@ describe('sovrnBidAdapter', function() { expect(payload.imp[0]?.ext?.tid).to.equal('1a2c032473f4983') }) - it('when FLEDGE is enabled, should send ortb2imp.ext.ae', function () { - const bidderRequest = { - ...baseBidderRequest, - paapi: {enabled: true} - } - const bidRequest = { - ...baseBidRequest, - ortb2Imp: { - ext: { - ae: 1 - } - }, - } - const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest).data) - expect(payload.imp[0].ext.ae).to.equal(1) - }) - - it('when FLEDGE is not enabled, should not send ortb2imp.ext.ae', function () { - const bidRequest = { - ...baseBidRequest, - ortb2Imp: { - ext: { - ae: 1 - } - }, - } - const payload = JSON.parse(spec.buildRequests([bidRequest], baseBidderRequest).data) - expect(payload.imp[0].ext.ae).to.be.undefined - }) - - it('when FLEDGE is enabled, but env is malformed, should not send ortb2imp.ext.ae', function () { - const bidderRequest = { - ...baseBidderRequest, - paapi: { - enabled: true - } - } - const bidRequest = { - ...baseBidRequest, - ortb2Imp: { - ext: { - ae: 'malformed' - } - }, - } - const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest).data) - expect(payload.imp[0].ext.ae).to.be.undefined - }) - it('includes the ad unit code in the request', function() { const impression = payload.imp[0] expect(impression.adunitcode).to.equal('adunit-code') @@ -961,158 +912,6 @@ describe('sovrnBidAdapter', function() { }) }) - describe('fledge response', function () { - const fledgeResponse = { - body: { - id: '37386aade21a71', - seatbid: [{ - bid: [{ - id: 'a_403370_332fdb9b064040ddbec05891bd13ab28', - crid: 'creativelycreatedcreativecreative', - impid: '263c448586f5a1', - price: 0.45882675, - nurl: '', - adm: '', - h: 90, - w: 728 - }] - }], - ext: { - seller: 'seller.lijit.com', - decisionLogicUrl: 'https://decision.lijit.com', - igbid: [{ - impid: 'test_imp_id', - igbuyer: [{ - igdomain: 'ap.lijit.com', - buyerdata: { - base_bid_micros: 0.1, - use_bid_multiplier: true, - multiplier: '1.3' - } - }, { - igdomain: 'buyer2.com', - buyerdata: {} - }, { - igdomain: 'buyer3.com', - buyerdata: {} - }] - }, { - impid: 'test_imp_id_2', - igbuyer: [{ - igdomain: 'ap2.lijit.com', - buyerdata: { - base_bid_micros: '0.2', - } - }] - }, { - impid: '', - igbuyer: [{ - igdomain: 'ap3.lijit.com', - buyerdata: { - base_bid_micros: '0.3', - } - }] - }, { - impid: 'test_imp_id_3', - igbuyer: [{ - igdomain: '', - buyerdata: { - base_bid_micros: '0.3', - } - }] - }, { - impid: 'test_imp_id_4', - igbuyer: [] - }] - } - } - } - const emptyFledgeResponse = { - body: { - id: '37386aade21a71', - seatbid: [{ - bid: [{ - id: 'a_403370_332fdb9b064040ddbec05891bd13ab28', - crid: 'creativelycreatedcreativecreative', - impid: '263c448586f5a1', - price: 0.45882675, - nurl: '', - adm: '', - h: 90, - w: 728 - }] - }], - ext: { - igbid: { - } - } - } - } - const expectedResponse = { - requestId: '263c448586f5a1', - cpm: 0.45882675, - width: 728, - height: 90, - creativeId: 'creativelycreatedcreativecreative', - dealId: null, - currency: 'USD', - netRevenue: true, - mediaType: 'banner', - ttl: 60000, - meta: { advertiserDomains: [] }, - ad: decodeURIComponent(`>`) - } - const expectedFledgeResponse = [ - { - bidId: 'test_imp_id', - config: { - seller: 'seller.lijit.com', - decisionLogicUrl: 'https://decision.lijit.com', - sellerTimeout: undefined, - auctionSignals: {}, - interestGroupBuyers: ['ap.lijit.com', 'buyer2.com', 'buyer3.com'], - perBuyerSignals: { - 'ap.lijit.com': { - base_bid_micros: 0.1, - use_bid_multiplier: true, - multiplier: '1.3' - }, - 'buyer2.com': {}, - 'buyer3.com': {} - } - } - }, - { - bidId: 'test_imp_id_2', - config: { - seller: 'seller.lijit.com', - decisionLogicUrl: 'https://decision.lijit.com', - sellerTimeout: undefined, - auctionSignals: {}, - interestGroupBuyers: ['ap2.lijit.com'], - perBuyerSignals: { - 'ap2.lijit.com': { - base_bid_micros: '0.2', - } - } - } - } - ] - - it('should return valid fledge auction configs alongside bids', function () { - const result = spec.interpretResponse(fledgeResponse) - expect(result).to.have.property('bids') - expect(result).to.have.property('paapi') - expect(result.paapi.length).to.equal(2) - expect(result.paapi).to.deep.equal(expectedFledgeResponse) - }) - it('should ignore empty fledge auction configs array', function () { - const result = spec.interpretResponse(emptyFledgeResponse) - expect(result.length).to.equal(1) - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse)) - }) - }) - describe('interpretResponse video', function () { let videoResponse const bidAdm = 'key%3Dvalue' diff --git a/test/spec/modules/sparteoBidAdapter_spec.js b/test/spec/modules/sparteoBidAdapter_spec.js index e65fbbd88ef..12b29d9003b 100644 --- a/test/spec/modules/sparteoBidAdapter_spec.js +++ b/test/spec/modules/sparteoBidAdapter_spec.js @@ -1,11 +1,11 @@ -import {expect} from 'chai'; +import { expect } from 'chai'; import { deepClone, mergeDeep } from 'src/utils'; -import {spec as adapter} from 'modules/sparteoBidAdapter'; +import { spec as adapter } from 'modules/sparteoBidAdapter'; const CURRENCY = 'EUR'; const TTL = 60; const HTTP_METHOD = 'POST'; -const REQUEST_URL = 'https://bid.sparteo.com/auction'; +const REQUEST_URL = 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=dev.sparteo.com'; const USER_SYNC_URL_IFRAME = 'https://sync.sparteo.com/sync/iframe.html?from=prebidjs'; const VALID_BID_BANNER = { @@ -79,6 +79,7 @@ const VALID_REQUEST_BANNER = { } }], 'site': { + 'domain': 'dev.sparteo.com', 'publisher': { 'ext': { 'params': { @@ -123,6 +124,7 @@ const VALID_REQUEST_VIDEO = { } }], 'site': { + 'domain': 'dev.sparteo.com', 'publisher': { 'ext': { 'params': { @@ -186,6 +188,7 @@ const VALID_REQUEST = { } }], 'site': { + 'domain': 'dev.sparteo.com', 'publisher': { 'ext': { 'params': { @@ -199,17 +202,26 @@ const VALID_REQUEST = { } }; +const ORTB2_GLOBAL = { + site: { + domain: 'dev.sparteo.com' + } +}; + const BIDDER_REQUEST = { - bids: [VALID_BID_BANNER, VALID_BID_VIDEO] -} + bids: [VALID_BID_BANNER, VALID_BID_VIDEO], + ortb2: ORTB2_GLOBAL +}; const BIDDER_REQUEST_BANNER = { - bids: [VALID_BID_BANNER] -} + bids: [VALID_BID_BANNER], + ortb2: ORTB2_GLOBAL +}; const BIDDER_REQUEST_VIDEO = { - bids: [VALID_BID_VIDEO] -} + bids: [VALID_BID_VIDEO], + ortb2: ORTB2_GLOBAL +}; describe('SparteoAdapter', function () { describe('isBidRequestValid', function () { @@ -250,54 +262,49 @@ describe('SparteoAdapter', function () { describe('buildRequests', function () { describe('Check method return', function () { + it('should return the right formatted banner requests', function () { + const request = adapter.buildRequests([VALID_BID_BANNER], BIDDER_REQUEST_BANNER); + delete request.data.id; + + expect(request).to.deep.equal(VALID_REQUEST_BANNER); + }); if (FEATURES.VIDEO) { - it('should return the right formatted requests', function() { + it('should return the right formatted requests', function () { const request = adapter.buildRequests([VALID_BID_BANNER, VALID_BID_VIDEO], BIDDER_REQUEST); delete request.data.id; expect(request).to.deep.equal(VALID_REQUEST); }); - } - - it('should return the right formatted banner requests', function() { - const request = adapter.buildRequests([VALID_BID_BANNER], BIDDER_REQUEST_BANNER); - delete request.data.id; - expect(request).to.deep.equal(VALID_REQUEST_BANNER); - }); - - if (FEATURES.VIDEO) { - it('should return the right formatted video requests', function() { + it('should return the right formatted video requests', function () { const request = adapter.buildRequests([VALID_BID_VIDEO], BIDDER_REQUEST_VIDEO); delete request.data.id; expect(request).to.deep.equal(VALID_REQUEST_VIDEO); }); - } - it('should return the right formatted request with endpoint test', function() { - const endpoint = 'https://bid-test.sparteo.com/auction'; + it('should return the right formatted request with endpoint test', function () { + const endpoint = 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=dev.sparteo.com'; - const bids = mergeDeep(deepClone([VALID_BID_BANNER, VALID_BID_VIDEO]), { - params: { - endpoint: endpoint - } - }); + const bids = deepClone([VALID_BID_BANNER, VALID_BID_VIDEO]); + bids[0].params.endpoint = endpoint; - const requests = mergeDeep(deepClone(VALID_REQUEST)); + const expectedRequest = deepClone(VALID_REQUEST); + expectedRequest.url = endpoint; + expectedRequest.data.imp[0].ext.sparteo.params.endpoint = endpoint; - const request = adapter.buildRequests(bids, BIDDER_REQUEST); - requests.url = endpoint; - delete request.data.id; + const request = adapter.buildRequests(bids, BIDDER_REQUEST); + delete request.data.id; - expect(requests).to.deep.equal(requests); - }); + expect(request).to.deep.equal(expectedRequest); + }); + } }); }); - describe('interpretResponse', function() { + describe('interpretResponse', function () { describe('Check method return', function () { - it('should return the right formatted response', function() { + it('should return the right formatted response', function () { const response = { body: { 'id': '63f4d300-6896-4bdc-8561-0932f73148b1', @@ -458,9 +465,9 @@ describe('SparteoAdapter', function () { }); }); - describe('onBidWon', function() { + describe('onBidWon', function () { describe('Check methods succeed', function () { - it('should not throw error', function() { + it('should not throw error', function () { const bids = [ { requestId: '1a2b3c4d', @@ -500,16 +507,16 @@ describe('SparteoAdapter', function () { } ]; - bids.forEach(function(bid) { + bids.forEach(function (bid) { expect(adapter.onBidWon.bind(adapter, bid)).to.not.throw(); }); }); }); }); - describe('getUserSyncs', function() { + describe('getUserSyncs', function () { describe('Check methods succeed', function () { - it('should return the sync url', function() { + it('should return the sync url', function () { const syncOptions = { 'iframeEnabled': true, 'pixelEnabled': false @@ -531,4 +538,426 @@ describe('SparteoAdapter', function () { }); }); }); + + describe('replaceMacros via buildRequests', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${SITE_DOMAIN_QUERY}${APP_DOMAIN_QUERY}${BUNDLE_QUERY}'; + + it('replaces macros for site traffic (site_domain only)', function () { + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + bid.params.networkId = '1234567a-eb1b-1fae-1d23-e1fbaef234cf'; + + const bidderReq = { + bids: [bid], + ortb2: { + site: { + domain: 'site.sparteo.com', + publisher: { domain: 'dev.sparteo.com' } + } + } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=site.sparteo.com' + ); + }); + + it('uses site.page hostname when site.domain is missing', function () { + const ENDPOINT2 = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${SITE_DOMAIN_QUERY}'; + + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT2; + bid.params.networkId = '1234567a-eb1b-1fae-1d23-e1fbaef234cf'; + + const bidderReq = { + bids: [bid], + ortb2: { + site: { + page: 'https://www.dev.sparteo.com:3000/p/some?x=1' + } + } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=dev.sparteo.com' + ); + }); + + it('omits domain query and leaves network_id empty when neither site nor app is present', function () { + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + bid.params.networkId = '1234567a-eb1b-1fae-1d23-e1fbaef234cf'; + + const bidderReq = { bids: [bid], ortb2: {} }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=' + ); + }); + + it('sets site_domain=unknown when site.domain is null', function () { + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { + site: { + domain: null + } + } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=unknown' + ); + }); + + it('replaces ${NETWORK_ID} with empty when undefined', function () { + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + delete bid.params.networkId; + + const bidderReq = { + bids: [bid], + ortb2: { + site: { + domain: 'dev.sparteo.com' + } + } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=&site_domain=dev.sparteo.com' + ); + }); + + it('replaces ${NETWORK_ID} with empty when null', function () { + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + bid.params.networkId = null; + + const bidderReq = { + bids: [bid], + ortb2: { + site: { + domain: 'dev.sparteo.com' + } + } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=&site_domain=dev.sparteo.com' + ); + }); + + it('appends &bundle=... and uses app_domain when app.bundle is present', function () { + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { + app: { + domain: 'dev.sparteo.com', + bundle: 'com.sparteo.app' + } + } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&app_domain=dev.sparteo.com&bundle=com.sparteo.app' + ); + }); + + it('does not append &bundle when app is missing; uses site_domain when site exists', function () { + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { + site: { domain: 'dev.sparteo.com' } + } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=dev.sparteo.com' + ); + }); + + it('prefers site over app when both are present', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${SITE_DOMAIN_QUERY}${APP_DOMAIN_QUERY}${BUNDLE_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { + site: { domain: 'site.sparteo.com' }, + app: { domain: 'app.sparteo.com', bundle: 'com.sparteo.app' } + } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=site.sparteo.com' + ); + expect(req.data.site?.publisher?.ext?.params?.networkId).to.equal('1234567a-eb1b-1fae-1d23-e1fbaef234cf'); + expect(req.data.app).to.be.undefined; + }); + + ['', ' ', 'null', 'NuLl'].forEach((val) => { + it(`app bundle "${val}" produces &bundle=unknown`, function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${APP_DOMAIN_QUERY}${BUNDLE_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { app: { domain: 'dev.sparteo.com', bundle: val } } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&app_domain=dev.sparteo.com&bundle=unknown' + ); + }); + }); + + it('app domain missing becomes app_domain=unknown while keeping bundle', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${APP_DOMAIN_QUERY}${BUNDLE_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { app: { domain: '', bundle: 'com.sparteo.app' } } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&app_domain=unknown&bundle=com.sparteo.app' + ); + }); + + it('uses network_id from app.publisher.ext for app-only traffic', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${APP_DOMAIN_QUERY}${BUNDLE_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { app: { domain: 'dev.sparteo.com', bundle: 'com.sparteo.app' } } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.data.site).to.be.undefined; + expect(req.data.app?.publisher?.ext?.params?.networkId).to.equal('1234567a-eb1b-1fae-1d23-e1fbaef234cf'); + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&app_domain=dev.sparteo.com&bundle=com.sparteo.app' + ); + }); + + it('unparsable site.page yields site_domain=unknown', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${SITE_DOMAIN_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { site: { page: 'not a url' } } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=unknown' + ); + }); + + it('literal "null" in site.page yields site_domain=unknown', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${SITE_DOMAIN_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { site: { domain: '', page: 'null' } } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=unknown' + ); + }); + + it('does not create site on app-only request', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${APP_DOMAIN_QUERY}${BUNDLE_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { app: { domain: 'dev.sparteo.com', bundle: 'com.sparteo.app' } } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.data.site).to.be.undefined; + expect(req.data.app).to.exist; + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&app_domain=dev.sparteo.com&bundle=com.sparteo.app' + ); + }); + + it('propagates adUnitCode into imp.ext.sparteo.params.adUnitCode', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${SITE_DOMAIN_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const req = adapter.buildRequests([bid], { bids: [bid], ortb2: { site: { domain: 'dev.sparteo.com' } } }); + delete req.data.id; + + expect(req.data.imp[0]?.ext?.sparteo?.params?.adUnitCode).to.equal(bid.adUnitCode); + }); + + it('sets pbjsVersion and networkId under site root', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${SITE_DOMAIN_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { bids: [bid], ortb2: { site: { domain: 'dev.sparteo.com' } } }; + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + const params = req.data.site?.publisher?.ext?.params; + expect(params?.pbjsVersion).to.equal('$prebid.version$'); + expect(params?.networkId).to.equal('1234567a-eb1b-1fae-1d23-e1fbaef234cf'); + expect(req.data.app?.publisher?.ext?.params?.pbjsVersion).to.be.undefined; + }); + + it('sets pbjsVersion and networkId under app root', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${APP_DOMAIN_QUERY}${BUNDLE_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { bids: [bid], ortb2: { app: { domain: 'dev.sparteo.com', bundle: 'com.sparteo.app' } } }; + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + const params = req.data.app?.publisher?.ext?.params; + expect(params?.pbjsVersion).to.equal('$prebid.version$'); + expect(params?.networkId).to.equal('1234567a-eb1b-1fae-1d23-e1fbaef234cf'); + expect(req.data.site).to.be.undefined; + }); + + it('app-only without networkId leaves network_id empty', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${APP_DOMAIN_QUERY}${BUNDLE_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + delete bid.params.networkId; + + const bidderReq = { bids: [bid], ortb2: { app: { domain: 'dev.sparteo.com', bundle: 'com.sparteo.app' } } }; + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=&app_domain=dev.sparteo.com&bundle=com.sparteo.app' + ); + }); + }); + + describe('domain normalization (strip www., port, path, trim)', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${SITE_DOMAIN_QUERY}'; + + const CASES = [ + { + label: 'strips leading "www." from site.domain', + site: { domain: 'www.dev.sparteo.com' }, + expected: 'dev.sparteo.com' + }, + { + label: 'trims whitespace and strips "www."', + site: { domain: ' www.dev.sparteo.com ' }, + expected: 'dev.sparteo.com' + }, + { + label: 'preserves non-"www" prefixes like "www2."', + site: { domain: 'www2.dev.sparteo.com' }, + expected: 'www2.dev.sparteo.com' + }, + { + label: 'removes port from site.page', + site: { page: 'https://dev.sparteo.com:8080/path?q=1' }, + expected: 'dev.sparteo.com' + }, + { + label: 'removes "www." and path from site.page', + site: { page: 'http://www.dev.sparteo.com/p?q=1' }, + expected: 'dev.sparteo.com' + }, + { + label: 'removes port when it appears in site.domain', + site: { domain: 'dev.sparteo.com:8443' }, + expected: 'dev.sparteo.com' + }, + { + label: 'removes accidental path in site.domain', + site: { domain: 'dev.sparteo.com/some/path' }, + expected: 'dev.sparteo.com' + } + ]; + + CASES.forEach(({ label, site, expected }) => { + it(label, function () { + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + const bidderReq = { bids: [bid], ortb2: { site } }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + `https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=${expected}` + ); + }); + }); + }); }); diff --git a/test/spec/modules/ssp_genieeBidAdapter_spec.js b/test/spec/modules/ssp_genieeBidAdapter_spec.js index 980d97c4c12..ab7e99ab5e5 100644 --- a/test/spec/modules/ssp_genieeBidAdapter_spec.js +++ b/test/spec/modules/ssp_genieeBidAdapter_spec.js @@ -82,42 +82,56 @@ describe('ssp_genieeBidAdapter', function () { afterEach(function () { sandbox.restore(); + config.resetConfig(); }); describe('isBidRequestValid', function () { - it('should return true when params.zoneId exists and params.currency does not exist', function () { - expect(spec.isBidRequestValid(BANNER_BID)).to.be.true; + it('should return false when params.zoneId does not exist', function () { + expect(spec.isBidRequestValid({ ...BANNER_BID, params: {} })).to.be.false; }); - it('should return true when params.zoneId and params.currency exist and params.currency is JPY or USD', function () { - config.setConfig({ currency: { adServerCurrency: 'JPY' } }); - expect( - spec.isBidRequestValid({ - ...BANNER_BID, - params: { ...BANNER_BID.params }, - }) - ).to.be.true; - config.setConfig({ currency: { adServerCurrency: 'USD' } }); - expect( - spec.isBidRequestValid({ - ...BANNER_BID, - params: { ...BANNER_BID.params }, - }) - ).to.be.true; - }); + describe('when params.currency is specified', function() { + it('should return true if currency is USD', function() { + const bid = { ...BANNER_BID, params: { ...BANNER_BID.params, currency: 'USD' } }; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); - it('should return false when params.zoneId does not exist', function () { - expect(spec.isBidRequestValid({ ...BANNER_BID, params: {} })).to.be.false; + it('should return true if currency is JPY', function() { + const bid = { ...BANNER_BID, params: { ...BANNER_BID.params, currency: 'JPY' } }; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + + it('should return false if currency is not supported (e.g., EUR)', function() { + const bid = { ...BANNER_BID, params: { ...BANNER_BID.params, currency: 'EUR' } }; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should return true if currency is valid, ignoring adServerCurrency', function() { + config.setConfig({ currency: { adServerCurrency: 'EUR' } }); + const bid = { ...BANNER_BID, params: { ...BANNER_BID.params, currency: 'USD' } }; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); }); - it('should return false when params.zoneId and params.currency exist and params.currency is neither JPY nor USD', function () { - config.setConfig({ currency: { adServerCurrency: 'EUR' } }); - expect( - spec.isBidRequestValid({ - ...BANNER_BID, - params: { ...BANNER_BID.params }, - }) - ).to.be.false; + describe('when params.currency is NOT specified (fallback to adServerCurrency)', function() { + it('should return true if adServerCurrency is not set', function() { + expect(spec.isBidRequestValid(BANNER_BID)).to.be.true; + }); + + it('should return true if adServerCurrency is JPY', function() { + config.setConfig({ currency: { adServerCurrency: 'JPY' } }); + expect(spec.isBidRequestValid(BANNER_BID)).to.be.true; + }); + + it('should return true if adServerCurrency is USD', function() { + config.setConfig({ currency: { adServerCurrency: 'USD' } }); + expect(spec.isBidRequestValid(BANNER_BID)).to.be.true; + }); + + it('should return false if adServerCurrency is not supported (e.g., EUR)', function() { + config.setConfig({ currency: { adServerCurrency: 'EUR' } }); + expect(spec.isBidRequestValid(BANNER_BID)).to.be.false; + }); }); }); diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index 8219ec3e8e2..9c06d717a04 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -116,6 +116,89 @@ describe('Taboola Adapter', function () { spec.onBidWon(bid); expect(server.requests[0].url).to.equals('http://win.example.com/3.4') }); + + it('should not fire nurl when deferBilling is true', function () { + const nurl = 'http://win.example.com/${AUCTION_PRICE}'; + const bid = { + requestId: 1, + cpm: 2, + originalCpm: 3.4, + creativeId: 1, + ttl: 60, + netRevenue: true, + mediaType: 'banner', + ad: '...', + width: 300, + height: 250, + nurl: nurl, + deferBilling: true + } + spec.onBidWon(bid); + expect(server.requests.length).to.equal(0); + }); + }); + + describe('onBidBillable', function () { + it('onBidBillable exist as a function', () => { + expect(spec.onBidBillable).to.exist.and.to.be.a('function'); + }); + + it('should fire burl when available', function () { + const burl = 'http://billing.example.com/${AUCTION_PRICE}'; + const nurl = 'http://win.example.com/${AUCTION_PRICE}'; + const bid = { + requestId: 1, + cpm: 2, + originalCpm: 3.4, + creativeId: 1, + ttl: 60, + netRevenue: true, + mediaType: 'banner', + ad: '...', + width: 300, + height: 250, + nurl: nurl, + burl: burl + } + spec.onBidBillable(bid); + expect(server.requests[0].url).to.equals('http://billing.example.com/3.4'); + }); + + it('should fall back to nurl when burl is not available', function () { + const nurl = 'http://win.example.com/${AUCTION_PRICE}'; + const bid = { + requestId: 1, + cpm: 2, + originalCpm: 3.4, + creativeId: 1, + ttl: 60, + netRevenue: true, + mediaType: 'banner', + ad: '...', + width: 300, + height: 250, + nurl: nurl + } + spec.onBidBillable(bid); + expect(server.requests[0].url).to.equals('http://win.example.com/3.4'); + }); + + it('should not fire anything when neither burl nor nurl is available', function () { + const bid = { + requestId: 1, + cpm: 2, + originalCpm: 3.4, + creativeId: 1, + ttl: 60, + netRevenue: true, + mediaType: 'banner', + ad: '...', + width: 300, + height: 250 + } + spec.onBidBillable(bid); + expect(server.requests.length).to.equal(0); + }); }); describe('onTimeout', function () { diff --git a/test/spec/modules/tappxBidAdapter_spec.js b/test/spec/modules/tappxBidAdapter_spec.js index d9d0004e1e0..3cd09d57a35 100644 --- a/test/spec/modules/tappxBidAdapter_spec.js +++ b/test/spec/modules/tappxBidAdapter_spec.js @@ -81,6 +81,7 @@ const c_SERVERRESPONSE_B = { cid: '01744fbb521e9fb10ffea926190effea', crid: 'a13cf884e66e7c660afec059c89d98b6', adomain: [ + 'adomain.com' ], }, ], @@ -112,6 +113,7 @@ const c_SERVERRESPONSE_V = { cid: '01744fbb521e9fb10ffea926190effea', crid: 'a13cf884e66e7c660afec059c89d98b6', adomain: [ + 'adomain.com' ], }, ], @@ -385,6 +387,16 @@ describe('Tappx bid adapter', function () { const bids = spec.interpretResponse(emptyServerResponse, c_BIDDERREQUEST_B); expect(bids).to.have.lengthOf(0); }); + + it('receive reponse with adomain', function () { + const bids_B = spec.interpretResponse(c_SERVERRESPONSE_B, c_BIDDERREQUEST_B); + const bid_B = bids_B[0]; + expect(bid_B.meta.advertiserDomains).to.deep.equal(['adomain.com']); + + const bids_V = spec.interpretResponse(c_SERVERRESPONSE_V, c_BIDDERREQUEST_V); + const bid_V = bids_V[0]; + expect(bid_V.meta.advertiserDomains).to.deep.equal(['adomain.com']); + }); }); /** diff --git a/test/spec/modules/tcfControl_spec.js b/test/spec/modules/tcfControl_spec.js index b8164b86eae..0794effaa70 100644 --- a/test/spec/modules/tcfControl_spec.js +++ b/test/spec/modules/tcfControl_spec.js @@ -404,6 +404,48 @@ describe('gdpr enforcement', function () { expectAllow(allowed, fetchBidsRule(activityParams(MODULE_TYPE_BIDDER, bidder))); }) }); + + it('should allow S2S bidder when deferS2Sbidders is true', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'basicAds', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [], + deferS2Sbidders: true + }] + } + }); + const consent = setupConsentData(); + consent.vendorData.vendor.consents = {}; + consent.vendorData.vendor.legitimateInterests = {}; + consent.vendorData.purpose.consents['2'] = true; + + const s2sBidderParams = activityParams(MODULE_TYPE_BIDDER, 's2sBidder', {isS2S: true}); + expectAllow(true, fetchBidsRule(s2sBidderParams)); + }); + + it('should not make exceptions for client bidders when deferS2Sbidders is true', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'basicAds', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [], + deferS2Sbidders: true + }] + } + }); + const consent = setupConsentData(); + consent.vendorData.vendor.consents = {}; + consent.vendorData.vendor.legitimateInterests = {}; + consent.vendorData.purpose.consents['2'] = true; + + const clientBidderParams = activityParams(MODULE_TYPE_BIDDER, 'clientBidder'); + expectAllow(false, fetchBidsRule(clientBidderParams)); + }); }); describe('reportAnalyticsRule', () => { @@ -880,7 +922,8 @@ describe('gdpr enforcement', function () { purpose: 'basicAds', enforcePurpose: true, enforceVendor: true, - vendorExceptions: [] + vendorExceptions: [], + deferS2Sbidders: false }]; beforeEach(function () { sandbox = sinon.createSandbox(); @@ -926,7 +969,8 @@ describe('gdpr enforcement', function () { purpose: 'basicAds', enforcePurpose: false, enforceVendor: true, - vendorExceptions: ['bidderA'] + vendorExceptions: ['bidderA'], + deferS2Sbidders: false } setEnforcementConfig({ gdpr: { diff --git a/test/spec/modules/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js index cec0853c114..c698473260f 100644 --- a/test/spec/modules/teadsBidAdapter_spec.js +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -3,6 +3,7 @@ import * as autoplay from 'libraries/autoplayDetection/autoplay.js'; import { spec, storage } from 'modules/teadsBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { getScreenOrientation } from 'src/utils.js'; +import {getDevicePixelRatio} from '../../../libraries/devicePixelRatio/devicePixelRatio.js'; const ENDPOINT = 'https://a.teads.tv/hb/bid-request'; const AD_SCRIPT = '"'; @@ -360,7 +361,7 @@ describe('teadsBidAdapter', () => { it('should add pixelRatio info to payload', function () { const request = spec.buildRequests(bidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); - const pixelRatio = window.top.devicePixelRatio + const pixelRatio = getDevicePixelRatio(); expect(payload.devicePixelRatio).to.exist; expect(payload.devicePixelRatio).to.deep.equal(pixelRatio); @@ -438,28 +439,6 @@ describe('teadsBidAdapter', () => { expect(payload.device).to.deep.equal(ortb2DeviceBidderRequest.ortb2.device); }); - it('should add hardwareConcurrency info to payload', function () { - const request = spec.buildRequests(bidRequests, bidderRequestDefault); - const payload = JSON.parse(request.data); - const hardwareConcurrency = window.top.navigator?.hardwareConcurrency - - if (hardwareConcurrency) { - expect(payload.hardwareConcurrency).to.exist; - expect(payload.hardwareConcurrency).to.deep.equal(hardwareConcurrency); - } else expect(payload.hardwareConcurrency).to.not.exist - }); - - it('should add deviceMemory info to payload', function () { - const request = spec.buildRequests(bidRequests, bidderRequestDefault); - const payload = JSON.parse(request.data); - const deviceMemory = window.top.navigator.deviceMemory - - if (deviceMemory) { - expect(payload.deviceMemory).to.exist; - expect(payload.deviceMemory).to.deep.equal(deviceMemory); - } else expect(payload.deviceMemory).to.not.exist; - }); - describe('pageTitle', function () { it('should add pageTitle info to payload based on document title', function () { const testText = 'This is a title'; diff --git a/test/spec/modules/terceptAnalyticsAdapter_spec.js b/test/spec/modules/terceptAnalyticsAdapter_spec.js index bcbfaf63ae8..45184941b2d 100644 --- a/test/spec/modules/terceptAnalyticsAdapter_spec.js +++ b/test/spec/modules/terceptAnalyticsAdapter_spec.js @@ -42,14 +42,8 @@ describe('tercept analytics adapter', function () { 'mediaTypes': { 'banner': { 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ] } }, @@ -65,21 +59,23 @@ describe('tercept analytics adapter', function () { } ], 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ], - 'transactionId': '6d275806-1943-4f3e-9cd5-624cbd05ad98' + 'transactionId': '6d275806-1943-4f3e-9cd5-624cbd05ad98', + 'ortb2Imp': { + 'ext': { + 'data': { + 'adserver': { + 'adslot': '/1234567/homepage-banner' + }, + 'pbadslot': 'homepage-banner-pbadslot' + } + } + } } ], - 'adUnitCodes': [ - 'div-gpt-ad-1460505748561-0' - ], + 'adUnitCodes': ['div-gpt-ad-1460505748561-0'], 'bidderRequests': [ { 'bidderCode': 'appnexus', @@ -97,28 +93,16 @@ describe('tercept analytics adapter', function () { 'mediaTypes': { 'banner': { 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ] } }, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '6d275806-1943-4f3e-9cd5-624cbd05ad98', 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ], 'bidId': '263efc09896d0c', 'bidderRequestId': '155975c76e13b1', @@ -135,9 +119,7 @@ describe('tercept analytics adapter', function () { 'referer': 'http://observer.com/integrationExamples/gpt/hello_world.html', 'reachedTop': true, 'numIframes': 0, - 'stack': [ - 'http://observer.com/integrationExamples/gpt/hello_world.html' - ] + 'stack': ['http://observer.com/integrationExamples/gpt/hello_world.html'] }, 'start': 1576823893838 }, @@ -157,28 +139,16 @@ describe('tercept analytics adapter', function () { 'mediaTypes': { 'banner': { 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ] } }, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '6d275806-1943-4f3e-9cd5-624cbd05ad98', 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ], 'bidId': '9424dea605368f', 'bidderRequestId': '181df4d465699c', @@ -195,9 +165,7 @@ describe('tercept analytics adapter', function () { 'referer': 'http://observer.com/integrationExamples/gpt/hello_world.html', 'reachedTop': true, 'numIframes': 0, - 'stack': [ - 'http://observer.com/integrationExamples/gpt/hello_world.html' - ] + 'stack': ['http://observer.com/integrationExamples/gpt/hello_world.html'] }, 'start': 1576823893838 } @@ -223,28 +191,16 @@ describe('tercept analytics adapter', function () { 'mediaTypes': { 'banner': { 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ] } }, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '6d275806-1943-4f3e-9cd5-624cbd05ad98', 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ], 'bidId': '263efc09896d0c', 'bidderRequestId': '155975c76e13b1', @@ -261,9 +217,7 @@ describe('tercept analytics adapter', function () { 'referer': 'http://observer.com/integrationExamples/gpt/hello_world.html', 'reachedTop': true, 'numIframes': 0, - 'stack': [ - 'http://observer.com/integrationExamples/gpt/hello_world.html' - ] + 'stack': ['http://observer.com/integrationExamples/gpt/hello_world.html'] }, 'start': 1576823893838 }, @@ -283,28 +237,16 @@ describe('tercept analytics adapter', function () { 'mediaTypes': { 'banner': { 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ] } }, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': 'd99d90e0-663a-459d-8c87-4c92ce6a527c', 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ], 'bidId': '9424dea605368f', 'bidderRequestId': '181df4d465699c', @@ -321,9 +263,7 @@ describe('tercept analytics adapter', function () { 'referer': 'http://observer.com/integrationExamples/gpt/hello_world.html', 'reachedTop': true, 'numIframes': 0, - 'stack': [ - 'http://observer.com/integrationExamples/gpt/hello_world.html' - ] + 'stack': ['http://observer.com/integrationExamples/gpt/hello_world.html'] }, 'start': 1576823893838 }, @@ -362,14 +302,8 @@ describe('tercept analytics adapter', function () { 'mediaTypes': { 'banner': { 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ] } }, @@ -378,14 +312,13 @@ describe('tercept analytics adapter', function () { 'sizes': [[300, 250]], 'bidId': '9424dea605368f', 'bidderRequestId': '181df4d465699c', - 'auctionId': '86e005fa-1900-4782-b6df-528500f09128', + 'auctionId': 'db377024-d866-4a24-98ac-5e430f881313', 'src': 's2s', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, 'bidderWinsCount': 0 }, - 'bidTimeout': [ - ], + 'bidTimeout': [], 'bidResponse': { 'bidderCode': 'appnexus', 'width': 300, @@ -442,14 +375,8 @@ describe('tercept analytics adapter', function () { 'mediaTypes': { 'banner': { 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ] } }, @@ -465,21 +392,23 @@ describe('tercept analytics adapter', function () { } ], 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ], - 'transactionId': '6d275806-1943-4f3e-9cd5-624cbd05ad98' + 'transactionId': '6d275806-1943-4f3e-9cd5-624cbd05ad98', + 'ortb2Imp': { + 'ext': { + 'data': { + 'adserver': { + 'adslot': '/1234567/homepage-banner' + }, + 'pbadslot': 'homepage-banner-pbadslot' + } + } + } } ], - 'adUnitCodes': [ - 'div-gpt-ad-1460505748561-0' - ], + 'adUnitCodes': ['div-gpt-ad-1460505748561-0'], 'bidderRequests': [ { 'bidderCode': 'appnexus', @@ -497,28 +426,16 @@ describe('tercept analytics adapter', function () { 'mediaTypes': { 'banner': { 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ] } }, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '6d275806-1943-4f3e-9cd5-624cbd05ad98', 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ], 'bidId': '263efc09896d0c', 'bidderRequestId': '155975c76e13b1', @@ -535,9 +452,7 @@ describe('tercept analytics adapter', function () { 'referer': 'http://observer.com/integrationExamples/gpt/hello_world.html', 'reachedTop': true, 'numIframes': 0, - 'stack': [ - 'http://observer.com/integrationExamples/gpt/hello_world.html' - ] + 'stack': ['http://observer.com/integrationExamples/gpt/hello_world.html'] }, 'start': 1576823893838 } @@ -625,28 +540,16 @@ describe('tercept analytics adapter', function () { 'mediaTypes': { 'banner': { 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ] } }, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '6d275806-1943-4f3e-9cd5-624cbd05ad98', 'sizes': [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + [300, 250], + [300, 600] ], 'bidId': '263efc09896d0c', 'bidderRequestId': '155975c76e13b1', @@ -663,9 +566,7 @@ describe('tercept analytics adapter', function () { 'referer': 'http://observer.com/integrationExamples/gpt/hello_world.html', 'reachedTop': true, 'numIframes': 0, - 'stack': [ - 'http://observer.com/integrationExamples/gpt/hello_world.html' - ] + 'stack': ['http://observer.com/integrationExamples/gpt/hello_world.html'] }, 'start': 1576823893838 }, @@ -721,222 +622,228 @@ describe('tercept analytics adapter', function () { ] } }; + const location = utils.getWindowLocation(); const expectedAfterBid = { - "bids": [ + 'bids': [ { - "bidderCode": "appnexus", - "bidId": "263efc09896d0c", - "adUnitCode": "div-gpt-ad-1460505748561-0", - "requestId": "155975c76e13b1", - "auctionId": "db377024-d866-4a24-98ac-5e430f881313", - "sizes": "300x250,300x600", - "renderStatus": 2, - "requestTimestamp": 1576823893838, - "creativeId": 96846035, - "currency": "USD", - "cpm": 0.5, - "netRevenue": true, - "mediaType": "banner", - "statusMessage": "Bid available", - "timeToRespond": 212, - "responseTimestamp": 1576823894050 + 'bidderCode': 'appnexus', + 'bidId': '263efc09896d0c', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'requestId': '155975c76e13b1', + 'auctionId': 'db377024-d866-4a24-98ac-5e430f881313', + 'sizes': '300x250,300x600', + 'renderStatus': 2, + 'requestTimestamp': 1576823893838, + 'creativeId': 96846035, + 'currency': 'USD', + 'cpm': 0.5, + 'netRevenue': true, + 'renderedSize': null, + 'width': 300, + 'height': 250, + 'mediaType': 'banner', + 'statusMessage': 'Bid available', + 'timeToRespond': 212, + 'responseTimestamp': 1576823894050, + 'renderTimestamp': null, + 'reason': null, + 'message': null, + 'host': null, + 'path': null, + 'search': null, + 'adserverAdSlot': '/1234567/homepage-banner', + 'pbAdSlot': 'homepage-banner-pbadslot', + 'ttl': 300, + 'ad': '', + 'adId': '393976d8770041', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '393976d8770041', + 'hb_pb': '0.50', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner' + }, + 'meta': { + 'advertiserId': 2529885 + } }, { - "bidderCode": "ix", - "adUnitCode": "div-gpt-ad-1460505748561-0", - "requestId": "181df4d465699c", - "auctionId": "86e005fa-1900-4782-b6df-528500f09128", - "transactionId": "d99d90e0-663a-459d-8c87-4c92ce6a527c", - "sizes": "300x250,300x600", - "renderStatus": 5, - "responseTimestamp": 1753444800000 + 'bidderCode': 'ix', + 'bidId': '9424dea605368f', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'requestId': '181df4d465699c', + 'auctionId': 'db377024-d866-4a24-98ac-5e430f881313', + 'transactionId': 'd99d90e0-663a-459d-8c87-4c92ce6a527c', + 'sizes': '300x250,300x600', + 'renderStatus': 5, + 'renderedSize': null, + 'renderTimestamp': null, + 'reason': null, + 'message': null, + 'host': null, + 'path': null, + 'search': null, + 'responseTimestamp': 1753444800000, + 'adserverAdSlot': '/1234567/homepage-banner', + 'pbAdSlot': 'homepage-banner-pbadslot', + 'meta': {} } ], - "auctionInit": { - "auctionId": "db377024-d866-4a24-98ac-5e430f881313", - "timestamp": 1576823893836, - "auctionStatus": "inProgress", - "adUnits": [ + 'auctionInit': { + 'auctionId': 'db377024-d866-4a24-98ac-5e430f881313', + 'timestamp': 1576823893836, + 'auctionStatus': 'inProgress', + 'adUnits': [ { - "code": "div-gpt-ad-1460505748561-0", - "mediaTypes": { - "banner": { - "sizes": [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + 'code': 'div-gpt-ad-1460505748561-0', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [300, 250], + [300, 600] ] } }, - "bids": [ + 'bids': [ { - "bidder": "appnexus", - "params": { - "placementId": 13144370 + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 }, - "crumbs": { - "pubcid": "ff4002c4-ce05-4a61-b4ef-45a3cd93991a" + 'crumbs': { + 'pubcid': 'ff4002c4-ce05-4a61-b4ef-45a3cd93991a' } } ], - "sizes": [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + 'sizes': [ + [300, 250], + [300, 600] ], - "transactionId": "6d275806-1943-4f3e-9cd5-624cbd05ad98" + 'transactionId': '6d275806-1943-4f3e-9cd5-624cbd05ad98', + 'ortb2Imp': { + 'ext': { + 'data': { + 'adserver': { + 'adslot': '/1234567/homepage-banner' + }, + 'pbadslot': 'homepage-banner-pbadslot' + } + } + } } ], - "adUnitCodes": [ - "div-gpt-ad-1460505748561-0" - ], - "bidderRequests": [ + 'adUnitCodes': ['div-gpt-ad-1460505748561-0'], + 'bidderRequests': [ { - "bidderCode": "appnexus", - "auctionId": "db377024-d866-4a24-98ac-5e430f881313", - "bidderRequestId": "155975c76e13b1", - "bids": [ + 'bidderCode': 'appnexus', + 'auctionId': 'db377024-d866-4a24-98ac-5e430f881313', + 'bidderRequestId': '155975c76e13b1', + 'bids': [ { - "bidder": "appnexus", - "params": { - "placementId": 13144370 + 'bidder': 'appnexus', + 'params': { + 'placementId': 13144370 }, - "crumbs": { - "pubcid": "ff4002c4-ce05-4a61-b4ef-45a3cd93991a" + 'crumbs': { + 'pubcid': 'ff4002c4-ce05-4a61-b4ef-45a3cd93991a' }, - "mediaTypes": { - "banner": { - "sizes": [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + 'mediaTypes': { + 'banner': { + 'sizes': [ + [300, 250], + [300, 600] ] } }, - "adUnitCode": "div-gpt-ad-1460505748561-0", - "transactionId": "6d275806-1943-4f3e-9cd5-624cbd05ad98", - "sizes": [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '6d275806-1943-4f3e-9cd5-624cbd05ad98', + 'sizes': [ + [300, 250], + [300, 600] ], - "bidId": "263efc09896d0c", - "bidderRequestId": "155975c76e13b1", - "auctionId": "db377024-d866-4a24-98ac-5e430f881313", - "src": "client", - "bidRequestsCount": 1, - "bidderRequestsCount": 1, - "bidderWinsCount": 0 + 'bidId': '263efc09896d0c', + 'bidderRequestId': '155975c76e13b1', + 'auctionId': 'db377024-d866-4a24-98ac-5e430f881313', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 } ], - "auctionStart": 1576823893836, - "timeout": 1000, - "refererInfo": { - "referer": "http://observer.com/integrationExamples/gpt/hello_world.html", - "reachedTop": true, - "numIframes": 0, - "stack": [ - "http://observer.com/integrationExamples/gpt/hello_world.html" - ] + 'auctionStart': 1576823893836, + 'timeout': 1000, + 'refererInfo': { + 'referer': 'http://observer.com/integrationExamples/gpt/hello_world.html', + 'reachedTop': true, + 'numIframes': 0, + 'stack': ['http://observer.com/integrationExamples/gpt/hello_world.html'] }, - "start": 1576823893838 + 'start': 1576823893838 }, { - "bidderCode": "ix", - "auctionId": "db377024-d866-4a24-98ac-5e430f881313", - "bidderRequestId": "181df4d465699c", - "bids": [ + 'bidderCode': 'ix', + 'auctionId': 'db377024-d866-4a24-98ac-5e430f881313', + 'bidderRequestId': '181df4d465699c', + 'bids': [ { - "bidder": "ix", - "params": { - "placementId": 13144370 + 'bidder': 'ix', + 'params': { + 'placementId': 13144370 }, - "crumbs": { - "pubcid": "ff4002c4-ce05-4a61-b4ef-45a3cd93991a" + 'crumbs': { + 'pubcid': 'ff4002c4-ce05-4a61-b4ef-45a3cd93991a' }, - "mediaTypes": { - "banner": { - "sizes": [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + 'mediaTypes': { + 'banner': { + 'sizes': [ + [300, 250], + [300, 600] ] } }, - "adUnitCode": "div-gpt-ad-1460505748561-0", - "transactionId": "6d275806-1943-4f3e-9cd5-624cbd05ad98", - "sizes": [ - [ - 300, - 250 - ], - [ - 300, - 600 - ] + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '6d275806-1943-4f3e-9cd5-624cbd05ad98', + 'sizes': [ + [300, 250], + [300, 600] ], - "bidId": "9424dea605368f", - "bidderRequestId": "181df4d465699c", - "auctionId": "db377024-d866-4a24-98ac-5e430f881313", - "src": "client", - "bidRequestsCount": 1, - "bidderRequestsCount": 1, - "bidderWinsCount": 0 + 'bidId': '9424dea605368f', + 'bidderRequestId': '181df4d465699c', + 'auctionId': 'db377024-d866-4a24-98ac-5e430f881313', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 } ], - "auctionStart": 1576823893836, - "timeout": 1000, - "refererInfo": { - "referer": "http://observer.com/integrationExamples/gpt/hello_world.html", - "reachedTop": true, - "numIframes": 0, - "stack": [ - "http://observer.com/integrationExamples/gpt/hello_world.html" - ] + 'auctionStart': 1576823893836, + 'timeout': 1000, + 'refererInfo': { + 'referer': 'http://observer.com/integrationExamples/gpt/hello_world.html', + 'reachedTop': true, + 'numIframes': 0, + 'stack': ['http://observer.com/integrationExamples/gpt/hello_world.html'] }, - "start": 1576823893838 + 'start': 1576823893838 } ], - "noBids": [], - "bidsReceived": [], - "winningBids": [], - "timeout": 1000, - "host": "localhost:9876", - "path": "/context.html", - "search": "" + 'noBids': [], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 1000, + 'host': 'localhost:9876', + 'path': '/context.html', + 'search': '' }, - "initOptions": { - "pubId": "1", - "pubKey": "ZXlKaGJHY2lPaUpJVXpJMU5pSjkuT==", - "hostName": "us-central1-quikr-ebay.cloudfunctions.net", - "pathName": "/prebid-analytics" + 'initOptions': { + 'pubId': '1', + 'pubKey': 'ZXlKaGJHY2lPaUpJVXpJMU5pSjkuT==', + 'hostName': 'us-central1-quikr-ebay.cloudfunctions.net', + 'pathName': '/prebid-analytics' } }; @@ -951,6 +858,8 @@ describe('tercept analytics adapter', function () { 'cpm': 0.5, 'netRevenue': true, 'renderedSize': '300x250', + 'width': 300, + 'height': 250, 'mediaType': 'banner', 'statusMessage': 'Bid available', 'status': 'rendered', @@ -958,12 +867,31 @@ describe('tercept analytics adapter', function () { 'timeToRespond': 212, 'requestTimestamp': 1576823893838, 'responseTimestamp': 1576823894050, - "host": "localhost", - "path": "/context.html", - "search": "", + 'renderTimestamp': null, + 'reason': null, + 'message': null, + 'host': 'localhost', + 'path': '/context.html', + 'search': '', + 'adserverAdSlot': '/1234567/homepage-banner', + 'pbAdSlot': 'homepage-banner-pbadslot', + 'ttl': 300, + 'ad': '', + 'adId': '393976d8770041', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '393976d8770041', + 'hb_pb': '0.50', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner' + }, + 'meta': { + 'advertiserId': 2529885 + } }, 'initOptions': initOptions - } + }; adapterManager.registerAnalyticsAdapter({ code: 'tercept', @@ -1002,19 +930,351 @@ describe('tercept analytics adapter', function () { events.emit(EVENTS.AUCTION_END, prebidEvent['auctionEnd']); expect(server.requests.length).to.equal(1); - const realAfterBid = JSON.parse(server.requests[0].requestBody); - expect(realAfterBid).to.deep.equal(expectedAfterBid); // Step 7: Send auction bid won event events.emit(EVENTS.BID_WON, prebidEvent['bidWon']); expect(server.requests.length).to.equal(2); - const winEventData = JSON.parse(server.requests[1].requestBody); - expect(winEventData).to.deep.equal(expectedAfterBidWon); }); + + it('uses correct adUnits for each auction via Map lookup', function () { + const auction1Init = { + 'auctionId': 'auction-1-id', + 'timestamp': 1576823893836, + 'auctionStatus': 'inProgress', + 'adUnits': [ + { + 'code': 'div-auction-1', + 'mediaTypes': { 'banner': { 'sizes': [[300, 250]] } }, + 'bids': [{ 'bidder': 'appnexus', 'params': { 'placementId': 111 } }], + 'sizes': [[300, 250]], + 'transactionId': 'trans-1', + 'ortb2Imp': { + 'ext': { + 'data': { + 'adserver': { 'adslot': '/auction1/slot' }, + 'pbadslot': 'auction1-pbadslot' + } + } + } + } + ], + 'adUnitCodes': ['div-auction-1'], + 'bidderRequests': [], + 'noBids': [], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 1000 + }; + + const auction2Init = { + 'auctionId': 'auction-2-id', + 'timestamp': 1576823893900, + 'auctionStatus': 'inProgress', + 'adUnits': [ + { + 'code': 'div-auction-2', + 'mediaTypes': { 'banner': { 'sizes': [[728, 90]] } }, + 'bids': [{ 'bidder': 'rubicon', 'params': { 'placementId': 222 } }], + 'sizes': [[728, 90]], + 'transactionId': 'trans-2', + 'ortb2Imp': { + 'ext': { + 'data': { + 'adserver': { 'adslot': '/auction2/slot' }, + 'pbadslot': 'auction2-pbadslot' + } + } + } + } + ], + 'adUnitCodes': ['div-auction-2'], + 'bidderRequests': [], + 'noBids': [], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 1000 + }; + + events.emit(EVENTS.AUCTION_INIT, auction1Init); + events.emit(EVENTS.AUCTION_INIT, auction2Init); + + const bidWon1 = { + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'adId': 'ad-1', + 'requestId': 'bid-1', + 'mediaType': 'banner', + 'cpm': 1.0, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': 'div-auction-1', + 'auctionId': 'auction-1-id', + 'responseTimestamp': 1576823894000, + 'requestTimestamp': 1576823893838, + 'bidder': 'appnexus', + 'timeToRespond': 164, + 'size': '300x250', + 'status': 'rendered', + 'meta': {} + }; + + const bidWon2 = { + 'bidderCode': 'rubicon', + 'width': 728, + 'height': 90, + 'adId': 'ad-2', + 'requestId': 'bid-2', + 'mediaType': 'banner', + 'cpm': 2.0, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': 'div-auction-2', + 'auctionId': 'auction-2-id', + 'responseTimestamp': 1576823894100, + 'requestTimestamp': 1576823893900, + 'bidder': 'rubicon', + 'timeToRespond': 200, + 'size': '728x90', + 'status': 'rendered', + 'meta': {} + }; + + events.emit(EVENTS.BID_WON, bidWon1); + events.emit(EVENTS.BID_WON, bidWon2); + + expect(server.requests.length).to.equal(2); + + const winData1 = JSON.parse(server.requests[0].requestBody); + expect(winData1.bidWon.adserverAdSlot).to.equal('/auction1/slot'); + expect(winData1.bidWon.pbAdSlot).to.equal('auction1-pbadslot'); + + const winData2 = JSON.parse(server.requests[1].requestBody); + expect(winData2.bidWon.adserverAdSlot).to.equal('/auction2/slot'); + expect(winData2.bidWon.pbAdSlot).to.equal('auction2-pbadslot'); + }); + + it('handles BIDDER_ERROR event', function () { + events.emit(EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); + + const bidderError = { + 'bidderCode': 'appnexus', + 'error': 'timeout', + 'bidderRequest': { + 'bidderCode': 'appnexus', + 'auctionId': 'db377024-d866-4a24-98ac-5e430f881313' + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'auctionId': 'db377024-d866-4a24-98ac-5e430f881313' + }; + + events.emit(EVENTS.BIDDER_ERROR, bidderError); + + expect(server.requests.length).to.equal(1); + const errorData = JSON.parse(server.requests[0].requestBody); + expect(errorData.bidderError).to.exist; + expect(errorData.bidderError.status).to.equal(6); + expect(errorData.bidderError.adserverAdSlot).to.equal('/1234567/homepage-banner'); + expect(errorData.bidderError.pbAdSlot).to.equal('homepage-banner-pbadslot'); + expect(errorData.bidderError.host).to.equal(window.location.hostname); + expect(errorData.bidderError.path).to.equal(window.location.pathname); + }); + + it('returns empty object for getAdSlotData when ad unit not found', function () { + const auctionInitNoOrtb2 = { + 'auctionId': 'no-ortb2-auction', + 'timestamp': 1576823893836, + 'auctionStatus': 'inProgress', + 'adUnits': [ + { + 'code': 'div-no-ortb2', + 'mediaTypes': { 'banner': { 'sizes': [[300, 250]] } }, + 'bids': [{ 'bidder': 'appnexus', 'params': { 'placementId': 999 } }], + 'sizes': [[300, 250]], + 'transactionId': 'trans-no-ortb2' + } + ], + 'adUnitCodes': ['div-no-ortb2'], + 'bidderRequests': [], + 'noBids': [], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 1000 + }; + + const bidRequest = { + 'bidderCode': 'appnexus', + 'auctionId': 'no-ortb2-auction', + 'bidderRequestId': 'req-no-ortb2', + 'bids': [{ + 'bidder': 'appnexus', + 'params': { 'placementId': 999 }, + 'mediaTypes': { 'banner': { 'sizes': [[300, 250]] } }, + 'adUnitCode': 'div-no-ortb2', + 'transactionId': 'trans-no-ortb2', + 'sizes': [[300, 250]], + 'bidId': 'bid-no-ortb2', + 'bidderRequestId': 'req-no-ortb2', + 'auctionId': 'no-ortb2-auction' + }], + 'auctionStart': 1576823893836 + }; + + const bidResponse = { + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'adId': 'ad-no-ortb2', + 'requestId': 'bid-no-ortb2', + 'mediaType': 'banner', + 'cpm': 0.5, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': 'div-no-ortb2', + 'auctionId': 'no-ortb2-auction', + 'responseTimestamp': 1576823894000, + 'bidder': 'appnexus', + 'timeToRespond': 164, + 'meta': {} + }; + + events.emit(EVENTS.AUCTION_INIT, auctionInitNoOrtb2); + events.emit(EVENTS.BID_REQUESTED, bidRequest); + events.emit(EVENTS.BID_RESPONSE, bidResponse); + events.emit(EVENTS.AUCTION_END, { auctionId: 'no-ortb2-auction' }); + + expect(server.requests.length).to.equal(1); + const auctionData = JSON.parse(server.requests[0].requestBody); + const bid = auctionData.bids.find(b => b.bidId === 'bid-no-ortb2'); + expect(bid.adserverAdSlot).to.be.undefined; + expect(bid.pbAdSlot).to.be.undefined; + }); + + it('handles AD_RENDER_SUCCEEDED event', function () { + events.emit(EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); + + const adRenderSucceeded = { + 'bid': { + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '393976d8770041', + 'requestId': '263efc09896d0c', + 'mediaType': 'banner', + 'cpm': 0.5, + 'creativeId': 96846035, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'auctionId': 'db377024-d866-4a24-98ac-5e430f881313', + 'responseTimestamp': 1576823894050, + 'requestTimestamp': 1576823893838, + 'bidder': 'appnexus', + 'timeToRespond': 212, + 'size': '300x250', + 'transactionId': '6d275806-1943-4f3e-9cd5-624cbd05ad98', + 'meta': { + 'advertiserId': 2529885 + } + }, + 'doc': {}, + 'adId': '393976d8770041' + }; + + events.emit(EVENTS.AD_RENDER_SUCCEEDED, adRenderSucceeded); + + expect(server.requests.length).to.equal(1); + const renderData = JSON.parse(server.requests[0].requestBody); + expect(renderData.adRenderSucceeded).to.exist; + expect(renderData.adRenderSucceeded.renderStatus).to.equal(7); + expect(renderData.adRenderSucceeded.renderTimestamp).to.be.a('number'); + expect(renderData.adRenderSucceeded.bidderCode).to.equal('appnexus'); + expect(renderData.adRenderSucceeded.bidId).to.equal('263efc09896d0c'); + expect(renderData.adRenderSucceeded.adUnitCode).to.equal('div-gpt-ad-1460505748561-0'); + expect(renderData.adRenderSucceeded.auctionId).to.equal('db377024-d866-4a24-98ac-5e430f881313'); + expect(renderData.adRenderSucceeded.cpm).to.equal(0.5); + expect(renderData.adRenderSucceeded.renderedSize).to.equal('300x250'); + expect(renderData.adRenderSucceeded.adserverAdSlot).to.equal('/1234567/homepage-banner'); + expect(renderData.adRenderSucceeded.pbAdSlot).to.equal('homepage-banner-pbadslot'); + expect(renderData.adRenderSucceeded.host).to.equal(window.location.hostname); + expect(renderData.adRenderSucceeded.path).to.equal(window.location.pathname); + expect(renderData.adRenderSucceeded.reason).to.be.null; + expect(renderData.adRenderSucceeded.message).to.be.null; + }); + + it('handles AD_RENDER_FAILED event', function () { + events.emit(EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); + + const adRenderFailed = { + 'bid': { + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '393976d8770041', + 'requestId': '263efc09896d0c', + 'mediaType': 'banner', + 'cpm': 0.5, + 'creativeId': 96846035, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'auctionId': 'db377024-d866-4a24-98ac-5e430f881313', + 'responseTimestamp': 1576823894050, + 'requestTimestamp': 1576823893838, + 'bidder': 'appnexus', + 'timeToRespond': 212, + 'size': '300x250', + 'transactionId': '6d275806-1943-4f3e-9cd5-624cbd05ad98', + 'meta': { + 'advertiserId': 2529885 + } + }, + 'adId': '393976d8770041', + 'reason': 'exception', + 'message': 'Error rendering ad: Cannot read property of undefined' + }; + + events.emit(EVENTS.AD_RENDER_FAILED, adRenderFailed); + + expect(server.requests.length).to.equal(1); + const renderData = JSON.parse(server.requests[0].requestBody); + expect(renderData.adRenderFailed).to.exist; + expect(renderData.adRenderFailed.renderStatus).to.equal(8); + expect(renderData.adRenderFailed.renderTimestamp).to.be.a('number'); + expect(renderData.adRenderFailed.bidderCode).to.equal('appnexus'); + expect(renderData.adRenderFailed.bidId).to.equal('263efc09896d0c'); + expect(renderData.adRenderFailed.adUnitCode).to.equal('div-gpt-ad-1460505748561-0'); + expect(renderData.adRenderFailed.auctionId).to.equal('db377024-d866-4a24-98ac-5e430f881313'); + expect(renderData.adRenderFailed.cpm).to.equal(0.5); + expect(renderData.adRenderFailed.reason).to.equal('exception'); + expect(renderData.adRenderFailed.message).to.equal('Error rendering ad: Cannot read property of undefined'); + expect(renderData.adRenderFailed.adserverAdSlot).to.equal('/1234567/homepage-banner'); + expect(renderData.adRenderFailed.pbAdSlot).to.equal('homepage-banner-pbadslot'); + expect(renderData.adRenderFailed.host).to.equal(window.location.hostname); + expect(renderData.adRenderFailed.path).to.equal(window.location.pathname); + }); + + it('includes null render fields in bidWon for consistency', function () { + events.emit(EVENTS.AUCTION_INIT, prebidEvent['auctionInit']); + events.emit(EVENTS.BID_WON, prebidEvent['bidWon']); + + expect(server.requests.length).to.equal(1); + const winData = JSON.parse(server.requests[0].requestBody); + expect(winData.bidWon.renderTimestamp).to.be.null; + expect(winData.bidWon.reason).to.be.null; + expect(winData.bidWon.message).to.be.null; + }); }); }); diff --git a/test/spec/modules/timeoutRtdProvider_spec.js b/test/spec/modules/timeoutRtdProvider_spec.js index b4231c3db7c..4776c52440e 100644 --- a/test/spec/modules/timeoutRtdProvider_spec.js +++ b/test/spec/modules/timeoutRtdProvider_spec.js @@ -3,206 +3,6 @@ import { expect } from 'chai'; import * as ajax from 'src/ajax.js'; import * as prebidGlobal from 'src/prebidGlobal.js'; -const DEFAULT_USER_AGENT = window.navigator.userAgent; -const DEFAULT_CONNECTION = window.navigator.connection; - -const PC_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246'; -const MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'; -const TABLET_USER_AGENT = 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148'; - -function resetUserAgent() { - window.navigator.__defineGetter__('userAgent', () => DEFAULT_USER_AGENT); -}; - -function setUserAgent(userAgent) { - window.navigator.__defineGetter__('userAgent', () => userAgent); -} - -function resetConnection() { - window.navigator.__defineGetter__('connection', () => DEFAULT_CONNECTION); -} -function setConnectionType(connectionType) { - window.navigator.__defineGetter__('connection', () => { return {'type': connectionType} }); -} - -describe('getDeviceType', () => { - afterEach(() => { - resetUserAgent(); - }); - - [ - // deviceType, userAgent, deviceTypeNum - ['pc', PC_USER_AGENT, 2], - ['mobile', MOBILE_USER_AGENT, 4], - ['tablet', TABLET_USER_AGENT, 5], - ].forEach(function(args) { - const [deviceType, userAgent, deviceTypeNum] = args; - it(`should be able to recognize ${deviceType} devices`, () => { - setUserAgent(userAgent); - const res = timeoutRtdFunctions.getDeviceType(); - expect(res).to.equal(deviceTypeNum) - }) - }) -}); - -describe('getConnectionSpeed', () => { - afterEach(() => { - resetConnection(); - }); - [ - // connectionType, connectionSpeed - ['slow-2g', 'slow'], - ['2g', 'slow'], - ['3g', 'medium'], - ['bluetooth', 'fast'], - ['cellular', 'fast'], - ['ethernet', 'fast'], - ['wifi', 'fast'], - ['wimax', 'fast'], - ['4g', 'fast'], - ['not known', 'unknown'], - [undefined, 'unknown'], - ].forEach(function(args) { - const [connectionType, connectionSpeed] = args; - it(`should be able to categorize connection speed when the connection type is ${connectionType}`, () => { - setConnectionType(connectionType); - const res = timeoutRtdFunctions.getConnectionSpeed(); - expect(res).to.equal(connectionSpeed) - }) - }) -}); - -describe('Timeout modifier calculations', () => { - let sandbox; - beforeEach(() => { - sandbox = sinon.createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it('should be able to detect video ad units', () => { - let adUnits = [] - let res = timeoutRtdFunctions.checkVideo(adUnits); - expect(res).to.be.false; - - adUnits = [{ - mediaTypes: { - video: [] - } - }]; - res = timeoutRtdFunctions.checkVideo(adUnits); - expect(res).to.be.true; - - adUnits = [{ - mediaTypes: { - banner: [] - } - }]; - res = timeoutRtdFunctions.checkVideo(adUnits); - expect(res).to.be.false; - }); - - it('should calculate the timeout modifier for video', () => { - sandbox.stub(timeoutRtdFunctions, 'checkVideo').returns(true); - const rules = { - includesVideo: { - 'true': 200, - 'false': 50 - } - } - const res = timeoutRtdFunctions.calculateTimeoutModifier([], rules); - expect(res).to.equal(200) - }); - - it('should calculate the timeout modifier for connectionSpeed', () => { - sandbox.stub(timeoutRtdFunctions, 'getConnectionSpeed').returns('slow'); - const rules = { - connectionSpeed: { - 'slow': 200, - 'medium': 100, - 'fast': 50 - } - } - const res = timeoutRtdFunctions.calculateTimeoutModifier([], rules); - expect(res).to.equal(200); - }); - - it('should calculate the timeout modifier for deviceType', () => { - sandbox.stub(timeoutRtdFunctions, 'getDeviceType').returns(4); - const rules = { - deviceType: { - '2': 50, - '4': 100, - '5': 200 - }, - } - const res = timeoutRtdFunctions.calculateTimeoutModifier([], rules); - expect(res).to.equal(100) - }); - - it('should calculate the timeout modifier for ranged numAdunits', () => { - const rules = { - numAdUnits: { - '1-5': 100, - '6-10': 200, - '11-15': 300, - } - } - const adUnits = [1, 2, 3, 4, 5, 6]; - const res = timeoutRtdFunctions.calculateTimeoutModifier(adUnits, rules); - expect(res).to.equal(200) - }); - - it('should calculate the timeout modifier for exact numAdunits', () => { - const rules = { - numAdUnits: { - '1': 100, - '2': 200, - '3': 300, - '4-5': 400, - } - } - const adUnits = [1, 2]; - const res = timeoutRtdFunctions.calculateTimeoutModifier(adUnits, rules); - expect(res).to.equal(200); - }); - - it('should add up all the modifiers when all the rules are present', () => { - sandbox.stub(timeoutRtdFunctions, 'getConnectionSpeed').returns('slow'); - sandbox.stub(timeoutRtdFunctions, 'getDeviceType').returns(4); - const rules = { - connectionSpeed: { - 'slow': 200, - 'medium': 100, - 'fast': 50 - }, - deviceType: { - '2': 50, - '4': 100, - '5': 200 - }, - includesVideo: { - 'true': 200, - 'false': 50 - }, - numAdUnits: { - '1': 100, - '2': 200, - '3': 300, - '4-5': 400, - } - } - const res = timeoutRtdFunctions.calculateTimeoutModifier([{ - mediaTypes: { - video: [] - } - }], rules); - expect(res).to.equal(600); - }); -}); - describe('Timeout RTD submodule', () => { let sandbox; beforeEach(() => { diff --git a/test/spec/modules/toponBidAdapter_spec.js b/test/spec/modules/toponBidAdapter_spec.js new file mode 100644 index 00000000000..2922398ef16 --- /dev/null +++ b/test/spec/modules/toponBidAdapter_spec.js @@ -0,0 +1,216 @@ +import { expect } from "chai"; +import { spec } from "modules/toponBidAdapter.js"; +import * as utils from "src/utils.js"; +const USER_SYNC_URL = "https://pb.anyrtb.com/pb/page/prebidUserSyncs.html"; +const USER_SYNC_IMG_URL = "https://cm.anyrtb.com/cm/sdk_sync"; + +describe("TopOn Adapter", function () { + const PREBID_VERSION = "$prebid.version$"; + const BIDDER_CODE = "topon"; + + let bannerBid = { + bidder: BIDDER_CODE, + params: { + pubid: "pub-uuid", + }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + }; + + const validBidRequests = [bannerBid]; + const bidderRequest = { + bids: [bannerBid], + }; + + const bannerResponse = { + bid: [ + { + id: "6e976fc683e543d892160ee7d6f057d8", + impid: "1fabbf3c-b5e4-4b7d-9956-8112f92c1076", + price: 7.906274762781043, + nurl: "https://127.0.0.1:1381/prebid_tk?...", + burl: "https://127.0.0.1:1381/prebid_tk?...", + lurl: "https://127.0.0.1:1381/prebid_tk?...", + adm: `
✅ TopOn Mock Ad
300x250 🚫
`, + adid: "Ad538d326a-47f1-4c22-80f0-67684a713898", + cid: "110", + crid: "Creative32666aba-b5d3-4074-9ad1-d1702e9ba22b", + exp: 1800, + ext: {}, + mtype: 1, + }, + ], + }; + + const response = { + body: { + cur: "USD", + id: "aa2653ff-bd37-4fef-8085-2e444347af8c", + seatbid: [bannerResponse], + }, + }; + + it("should properly expose spec attributes", function () { + expect(spec.code).to.equal(BIDDER_CODE); + expect(spec.supportedMediaTypes).to.exist.and.to.be.an("array"); + expect(spec.isBidRequestValid).to.be.a("function"); + expect(spec.buildRequests).to.be.a("function"); + expect(spec.interpretResponse).to.be.a("function"); + }); + + describe("Bid validations", () => { + it("should return true if publisherId is present in params", () => { + const isValid = spec.isBidRequestValid(validBidRequests[0]); + expect(isValid).to.equal(true); + }); + + it("should return false if publisherId is missing", () => { + const bid = utils.deepClone(validBidRequests[0]); + delete bid.params.pubid; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equal(false); + }); + + it("should return false if publisherId is not of type string", () => { + const bid = utils.deepClone(validBidRequests[0]); + bid.params.pubid = 10000; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equal(false); + }); + }); + + describe("Requests", () => { + it("should correctly build an ORTB Bid Request", () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + + expect(request).to.be.an("object"); + expect(request.method).to.equal("POST"); + expect(request.data).to.exist; + expect(request.data).to.be.an("object"); + expect(request.data.id).to.be.an("string"); + expect(request.data.id).to.not.be.empty; + }); + + it("should include prebid flag in request", () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + + expect(request.data.ext).to.have.property("prebid"); + expect(request.data.ext.prebid).to.have.property("channel"); + expect(request.data.ext.prebid.channel).to.deep.equal({ + version: PREBID_VERSION, + source: "pbjs", + }); + expect(request.data.source.ext.prebid).to.equal(1); + }); + }); + + describe("Response", () => { + it("should parse banner adm and set bidResponse.ad, width, and height", () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + response.body.seatbid[0].bid[0].impid = request.data.imp[0].id; + const bidResponses = spec.interpretResponse(response, request); + + expect(bidResponses).to.be.an("array"); + expect(bidResponses[0]).to.exist; + expect(bidResponses[0].ad).to.exist; + expect(bidResponses[0].mediaType).to.equal("banner"); + expect(bidResponses[0].width).to.equal(300); + expect(bidResponses[0].height).to.equal(250); + }); + }); + + describe("GetUserSyncs", function () { + it("should return correct sync URLs when iframeEnabled is true", function () { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: true, + }; + + spec.buildRequests(validBidRequests, bidderRequest); + const result = spec.getUserSyncs(syncOptions, [], {}); + + expect(result).to.be.an("array"); + expect(result[0].type).to.equal("iframe"); + expect(result[0].url).to.include(USER_SYNC_URL); + expect(result[0].url).to.include("pubid=tpnpub-uuid"); + }); + + it("should return correct sync URLs when pixelEnabled is true", function () { + const syncOptions = { + iframeEnabled: false, + pixelEnabled: true, + }; + + spec.buildRequests(validBidRequests, bidderRequest); + const result = spec.getUserSyncs(syncOptions, [], {}); + + expect(result).to.be.an("array"); + expect(result[0].type).to.equal("image"); + expect(result[0].url).to.include(USER_SYNC_IMG_URL); + expect(result[0].url).to.include("pubid=tpnpub-uuid"); + }); + + it("should respect gdpr consent data", function () { + const gdprConsent = { + gdprApplies: true, + consentString: "test-consent-string", + }; + + spec.buildRequests(validBidRequests, bidderRequest); + const result = spec.getUserSyncs( + { iframeEnabled: true }, + [], + gdprConsent + ); + expect(result[0].url).to.include("gdpr=1"); + expect(result[0].url).to.include("consent=test-consent-string"); + }); + + it("should handle US Privacy consent", function () { + const uspConsent = "1YNN"; + + spec.buildRequests(validBidRequests, bidderRequest); + const result = spec.getUserSyncs( + { iframeEnabled: true }, + [], + {}, + uspConsent + ); + + expect(result[0].url).to.include("us_privacy=1YNN"); + }); + + it("should handle GPP", function () { + const gppConsent = { + applicableSections: [7], + gppString: "test-consent-string", + }; + + spec.buildRequests(validBidRequests, bidderRequest); + const result = spec.getUserSyncs( + { iframeEnabled: true }, + [], + {}, + "", + gppConsent + ); + + expect(result[0].url).to.include("gpp=test-consent-string"); + expect(result[0].url).to.include("gpp_sid=7"); + }); + + it("should return empty array when sync is not enabled", function () { + const syncOptions = { + iframeEnabled: false, + pixelEnabled: false, + }; + + const result = spec.getUserSyncs(syncOptions, [], {}); + + expect(result).to.be.an("array").that.is.empty; + }); + }); +}); diff --git a/test/spec/modules/trustxBidAdapter_spec.js b/test/spec/modules/trustxBidAdapter_spec.js new file mode 100644 index 00000000000..ac572027900 --- /dev/null +++ b/test/spec/modules/trustxBidAdapter_spec.js @@ -0,0 +1,1112 @@ +import {expect} from 'chai'; +import {spec} from 'modules/trustxBidAdapter.js'; +import {BANNER, VIDEO} from 'src/mediaTypes.js'; +import sinon from 'sinon'; +import {config} from 'src/config.js'; + +const getBannerRequest = () => { + return { + bidderCode: 'trustx', + auctionId: 'ca09c8cd-3824-4322-9dfe-d5b62b51c81c', + bidderRequestId: 'trustx-request-1', + bids: [ + { + bidder: 'trustx', + params: { + uid: '987654', + bidfloor: 5.25, + }, + auctionId: 'auction-id-45fe-9823-123456789abc', + placementCode: 'div-gpt-ad-trustx-test', + mediaTypes: { + banner: { + sizes: [ + [ 300, 250 ], + ] + } + }, + bidId: 'trustx-bid-12345', + bidderRequestId: 'trustx-request-1', + } + ], + start: 1615982436070, + auctionStart: 1615982436069, + timeout: 2000 + } +}; + +const getVideoRequest = () => { + return { + bidderCode: 'trustx', + auctionId: 'd2b62784-f134-4896-a87e-a233c3371413', + bidderRequestId: 'trustx-video-request-1', + bids: [{ + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'trustx', + sizes: [640, 480], + bidId: 'trustx-video-bid-1', + adUnitCode: 'video-placement-1', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 789, + rewarded: 0, + placement: 1, + plcmt: 1, + hp: 1, + inventoryid: 456 + }, + site: { + id: 1234, + page: 'https://trustx-test.com', + referrer: 'http://trustx-referrer.com' + }, + publisher_id: 'trustx-publisher-id', + bidfloor: 7.25, + } + }, { + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'trustx', + sizes: [640, 480], + bidId: 'trustx-video-bid-2', + adUnitCode: 'video-placement-2', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 790, + rewarded: 0, + placement: 1, + plcmt: 1, + hp: 1, + inventoryid: 457 + }, + site: { + id: 1235, + page: 'https://trustx-test2.com', + referrer: 'http://trustx-referrer2.com' + }, + publisher_id: 'trustx-publisher-id', + bidfloor: 8.50, + } + }], + auctionStart: 1615982456880, + timeout: 3500, + start: 1615982456884, + doneCbCallCount: 0, + refererInfo: { + numIframes: 1, + reachedTop: true, + referer: 'trustx-test.com' + } + }; +}; + +const getBidderResponse = () => { + return { + headers: null, + body: { + id: 'trustx-response-id-1', + seatbid: [ + { + bid: [ + { + id: 'trustx-bid-12345', + impid: 'trustx-bid-12345', + price: 3.22, + adm: '', + adid: '987654321', + adomain: [ + 'https://trustx-advertiser.com' + ], + iurl: 'https://trustx-campaign.com/creative.jpg', + cid: '12345', + crid: 'trustx-creative-234', + cat: [], + w: 300, + h: 250, + ext: { + prebid: { + type: 'banner' + }, + bidder: { + trustx: { + brand_id: 123456, + auction_id: 987654321098765, + bidder_id: 5, + bid_ad_type: 0 + } + } + } + } + ], + seat: 'trustx' + } + ], + ext: { + usersync: { + sync1: { + status: 'none', + syncs: [ + { + url: 'https://sync1.trustx.org/sync', + type: 'iframe' + } + ] + }, + sync2: { + status: 'none', + syncs: [ + { + url: 'https://sync2.trustx.org/sync', + type: 'pixel' + } + ] + } + }, + responsetimemillis: { + trustx: 95 + } + } + } + }; +} + +describe('trustxBidAdapter', function() { + let videoBidRequest; + + const VIDEO_REQUEST = { + 'bidderCode': 'trustx', + 'auctionId': 'd2b62784-f134-4896-a87e-a233c3371413', + 'bidderRequestId': 'trustx-video-request-1', + 'bids': videoBidRequest, + 'auctionStart': 1615982456880, + 'timeout': 3000, + 'start': 1615982456884, + 'doneCbCallCount': 0, + 'refererInfo': { + 'numIframes': 1, + 'reachedTop': true, + 'referer': 'trustx-test.com' + } + }; + + beforeEach(function () { + videoBidRequest = { + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'trustx', + sizes: [640, 480], + bidId: 'trustx-video-bid-1', + adUnitCode: 'video-placement-1', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 789, + rewarded: 0, + placement: 1, + plcmt: 1, + hp: 1, + inventoryid: 456 + }, + site: { + id: 1234, + page: 'https://trustx-test.com', + referrer: 'http://trustx-referrer.com' + }, + publisher_id: 'trustx-publisher-id', + bidfloor: 0 + } + }; + }); + + describe('isValidRequest', function() { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('should accept request with uid/secid', function () { + bidderRequest.bids[0].params = { + uid: '123', + mediaTypes: { banner: { sizes: [[300, 250]] } } + }; + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.true; + }); + + it('should accept request with secid', function () { + bidderRequest.bids[0].params = { + secid: '456', + publisher_id: 'pub-1', + mediaTypes: { banner: { sizes: [[300, 250]] } } + }; + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.true; + }); + + it('reject requests without params', function () { + bidderRequest.bids[0].params = {}; + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.false; + }); + + it('returns false when banner mediaType does not exist', function () { + bidderRequest.bids[0].mediaTypes = {} + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.false; + }); + }); + + describe('buildRequests', function() { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('should return expected request object', function() { + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(bidRequest.url).equal('https://ads.trustx.org/pbhb'); + expect(bidRequest.method).equal('POST'); + }); + }); + + context('banner validation', function () { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('returns true when banner sizes are defined', function () { + const bid = { + bidder: 'trustx', + mediaTypes: { + banner: { + sizes: [[250, 300]] + } + }, + params: { + uid: 'trustx-placement-1', + } + }; + + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.true; + }); + + it('returns false when banner sizes are invalid', function () { + const invalidSizes = [ + undefined, + '3:2', + 456, + 'invalid' + ]; + + invalidSizes.forEach((sizes) => { + const bid = { + bidder: 'trustx', + mediaTypes: { + banner: { + sizes + } + }, + params: { + uid: 'trustx-placement-1', + } + }; + + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + }); + + context('video validation', function () { + beforeEach(function () { + // Basic Valid BidRequest + this.bid = { + bidder: 'trustx', + mediaTypes: { + video: { + playerSize: [[300, 250]], + context: 'instream', + mimes: ['video/mp4', 'video/webm'], + protocols: [2, 3] + } + }, + params: { + uid: 'trustx-placement-1', + } + }; + }); + + it('should return true (skip validations) when test = true', function () { + this.bid.params = { + test: true + }; + expect(spec.isBidRequestValid(this.bid)).to.equal(true); + }); + + it('returns false when video context is not defined', function () { + delete this.bid.mediaTypes.video.context; + + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + + it('returns false when video playserSize is invalid', function () { + const invalidSizes = [ + undefined, + '1:1', + 456, + 'invalid' + ]; + + invalidSizes.forEach((playerSize) => { + this.bid.mediaTypes.video.playerSize = playerSize; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + }); + + it('returns false when video mimes is invalid', function () { + const invalidMimes = [ + undefined, + 'invalid', + 1, + [] + ] + + invalidMimes.forEach((mimes) => { + this.bid.mediaTypes.video.mimes = mimes; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + + it('returns false when video protocols is invalid', function () { + const invalidProtocols = [ + undefined, + 'invalid', + 1, + [] + ] + + invalidProtocols.forEach((protocols) => { + this.bid.mediaTypes.video.protocols = protocols; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + + it('should accept outstream context', function () { + this.bid.mediaTypes.video.context = 'outstream'; + expect(spec.isBidRequestValid(this.bid)).to.be.true; + }); + }); + + describe('buildRequests', function () { + let bidderBannerRequest; + let bidRequestsWithMediaTypes; + let mockBidderRequest; + + beforeEach(function() { + bidderBannerRequest = getBannerRequest(); + + mockBidderRequest = {refererInfo: {}}; + + bidRequestsWithMediaTypes = [{ + bidder: 'trustx', + params: { + publisher_id: 'trustx-publisher-id', + }, + adUnitCode: '/adunit-test/trustx-path', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'trustx-test-bid-1', + bidderRequestId: 'trustx-test-request-1', + auctionId: 'trustx-test-auction-1', + transactionId: 'trustx-test-transaction-1', + ortb2Imp: { + ext: { + ae: 3 + } + } + }, { + bidder: 'trustx', + params: { + publisher_id: 'trustx-publisher-id', + }, + adUnitCode: 'trustx-adunit', + mediaTypes: { + video: { + playerSize: [640, 480], + placement: 1, + plcmt: 1, + } + }, + bidId: 'trustx-test-bid-2', + bidderRequestId: 'trustx-test-request-2', + auctionId: 'trustx-test-auction-2', + transactionId: 'trustx-test-transaction-2' + }]; + }); + + context('when mediaType is banner', function () { + it('creates request data', function () { + let request = spec.buildRequests(bidderBannerRequest.bids, bidderBannerRequest) + + expect(request).to.exist.and.to.be.a('object'); + const payload = request.data; + expect(payload.imp[0]).to.have.property('id', bidderBannerRequest.bids[0].bidId); + }); + + it('should combine multiple bid requests into a single request', function () { + // NOTE: This test verifies that trustx adapter does NOT use multi-request logic. + // Trustx adapter = returns single request with all imp objects (standard OpenRTB) + // + // IMPORTANT: Trustx adapter DOES support multi-bid (multiple bids in response for one imp), + // but does NOT support multi-request (multiple requests instead of one). + const multipleBidRequests = [ + { + bidder: 'trustx', + params: { uid: 'uid-1' }, + adUnitCode: 'adunit-1', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + bidId: 'bid-1', + bidderRequestId: 'request-1', + auctionId: 'auction-1' + }, + { + bidder: 'trustx', + params: { uid: 'uid-2' }, + adUnitCode: 'adunit-2', + mediaTypes: { banner: { sizes: [[728, 90]] } }, + bidId: 'bid-2', + bidderRequestId: 'request-1', + auctionId: 'auction-1' + }, + { + bidder: 'trustx', + params: { uid: 'uid-3' }, + adUnitCode: 'adunit-3', + mediaTypes: { banner: { sizes: [[970, 250]] } }, + bidId: 'bid-3', + bidderRequestId: 'request-1', + auctionId: 'auction-1' + } + ]; + + const request = spec.buildRequests(multipleBidRequests, mockBidderRequest); + + // Trustx adapter should return a SINGLE request object + expect(request).to.be.an('object'); + expect(request).to.not.be.an('array'); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://ads.trustx.org/pbhb'); // No placement_id in URL + + // All imp objects should be in the same request + const payload = request.data; + expect(payload.imp).to.be.an('array'); + expect(payload.imp).to.have.length(3); + expect(payload.imp[0].id).to.equal('bid-1'); + expect(payload.imp[1].id).to.equal('bid-2'); + expect(payload.imp[2].id).to.equal('bid-3'); + expect(payload.imp[0].tagid).to.equal('uid-1'); + expect(payload.imp[1].tagid).to.equal('uid-2'); + expect(payload.imp[2].tagid).to.equal('uid-3'); + }); + + it('should determine media type from mtype field for banner', function () { + const customBidderResponse = Object.assign({}, getBidderResponse()); + customBidderResponse.body = Object.assign({}, getBidderResponse().body); + + if (customBidderResponse.body.seatbid && + customBidderResponse.body.seatbid[0] && + customBidderResponse.body.seatbid[0].bid && + customBidderResponse.body.seatbid[0].bid[0]) { + // Add mtype to the bid + customBidderResponse.body.seatbid[0].bid[0].mtype = 1; // Banner type + } + + const bidRequest = spec.buildRequests(bidderBannerRequest.bids, bidderBannerRequest); + const bids = spec.interpretResponse(customBidderResponse, bidRequest); + expect(bids[0].mediaType).to.equal('banner'); + }); + }); + + if (FEATURES.VIDEO) { + context('video', function () { + it('should create a POST request for every bid', function () { + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(requests.method).to.equal('POST'); + expect(requests.url.trim()).to.equal(spec.ENDPOINT); + }); + + it('should attach request data', function () { + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const data = requests.data; + const [width, height] = videoBidRequest.sizes; + const VERSION = '1.0.0'; + + expect(data.imp[1].video.w).to.equal(width); + expect(data.imp[1].video.h).to.equal(height); + expect(data.imp[1].bidfloor).to.equal(videoBidRequest.params.bidfloor); + expect(data.imp[1]['video']['placement']).to.equal(videoBidRequest.params.video['placement']); + expect(data.imp[1]['video']['plcmt']).to.equal(videoBidRequest.params.video['plcmt']); + expect(data.ext.prebidver).to.equal('$prebid.version$'); + expect(data.ext.adapterver).to.equal(spec.VERSION); + }); + + it('should attach End 2 End test data', function () { + bidRequestsWithMediaTypes[1].params.test = true; + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const data = requests.data; + expect(data.imp[1].bidfloor).to.equal(0); + expect(data.imp[1].video.w).to.equal(640); + expect(data.imp[1].video.h).to.equal(480); + }); + }); + } + + context('privacy regulations', function() { + it('should include USP consent data in request', function() { + const uspConsent = '1YNN'; + const bidderRequestWithUsp = Object.assign({}, mockBidderRequest, { uspConsent }); + const requests = spec.buildRequests(bidRequestsWithMediaTypes, bidderRequestWithUsp); + const data = requests.data; + + expect(data.regs.ext).to.have.property('us_privacy', '1YNN'); + }); + + it('should include GPP consent data from gppConsent in request', function() { + const gppConsent = { + gppString: 'GPP_CONSENT_STRING', + applicableSections: [1, 2, 3] + }; + + const bidderRequestWithGpp = Object.assign({}, mockBidderRequest, { gppConsent }); + const requests = spec.buildRequests(bidRequestsWithMediaTypes, bidderRequestWithGpp); + const data = requests.data; + + expect(data.regs).to.have.property('gpp', 'GPP_CONSENT_STRING'); + expect(data.regs.gpp_sid).to.deep.equal([1, 2, 3]); + }); + + it('should include GPP consent data from ortb2 in request', function() { + const ortb2 = { + regs: { + gpp: 'GPP_STRING_FROM_ORTB2', + gpp_sid: [1, 2] + } + }; + + const bidderRequestWithOrtb2Gpp = Object.assign({}, mockBidderRequest, { ortb2 }); + const requests = spec.buildRequests(bidRequestsWithMediaTypes, bidderRequestWithOrtb2Gpp); + const data = requests.data; + + expect(data.regs).to.have.property('gpp', 'GPP_STRING_FROM_ORTB2'); + expect(data.regs.gpp_sid).to.deep.equal([1, 2]); + }); + + it('should prioritize gppConsent over ortb2 for GPP consent data', function() { + const gppConsent = { + gppString: 'GPP_CONSENT_STRING', + applicableSections: [1, 2, 3] + }; + + const ortb2 = { + regs: { + gpp: 'GPP_STRING_FROM_ORTB2', + gpp_sid: [1, 2] + } + }; + + const bidderRequestWithBothGpp = Object.assign({}, mockBidderRequest, { gppConsent, ortb2 }); + const requests = spec.buildRequests(bidRequestsWithMediaTypes, bidderRequestWithBothGpp); + const data = requests.data; + + expect(data.regs).to.have.property('gpp', 'GPP_CONSENT_STRING'); + expect(data.regs.gpp_sid).to.deep.equal([1, 2, 3]); + }); + + it('should include COPPA flag in request when set to true', function() { + // Mock the config.getConfig function to return true for coppa + sinon.stub(config, 'getConfig').withArgs('coppa').returns(true); + + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const data = requests.data; + + expect(data.regs).to.have.property('coppa', 1); + + // Restore the stub + config.getConfig.restore(); + }); + }); + }); + + describe('interpretResponse', function() { + context('when mediaType is banner', function() { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getBannerRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + + it('handles empty response', function () { + const EMPTY_RESP = Object.assign({}, bidderResponse, {'body': {}}); + const bids = spec.interpretResponse(EMPTY_RESP, bidRequest); + + expect(bids).to.be.empty; + }); + + it('have bids', function () { + let bids = spec.interpretResponse(bidderResponse, bidRequest); + expect(bids).to.be.an('array').that.is.not.empty; + validateBidOnIndex(0); + + function validateBidOnIndex(index) { + expect(bids[index]).to.have.property('currency', 'USD'); + expect(bids[index]).to.have.property('requestId', getBidderResponse().body.seatbid[0].bid[index].impid); + expect(bids[index]).to.have.property('cpm', getBidderResponse().body.seatbid[0].bid[index].price); + expect(bids[index]).to.have.property('width', getBidderResponse().body.seatbid[0].bid[index].w); + expect(bids[index]).to.have.property('height', getBidderResponse().body.seatbid[0].bid[index].h); + expect(bids[index]).to.have.property('ad', getBidderResponse().body.seatbid[0].bid[index].adm); + expect(bids[index]).to.have.property('creativeId', getBidderResponse().body.seatbid[0].bid[index].crid); + expect(bids[index].meta).to.have.property('advertiserDomains'); + expect(bids[index]).to.have.property('ttl', 360); + expect(bids[index]).to.have.property('netRevenue', false); + } + }); + + it('should determine media type from mtype field for banner', function () { + const customBidderResponse = Object.assign({}, getBidderResponse()); + customBidderResponse.body = Object.assign({}, getBidderResponse().body); + + if (customBidderResponse.body.seatbid && + customBidderResponse.body.seatbid[0] && + customBidderResponse.body.seatbid[0].bid && + customBidderResponse.body.seatbid[0].bid[0]) { + // Add mtype to the bid + customBidderResponse.body.seatbid[0].bid[0].mtype = 1; // Banner type + } + + const bids = spec.interpretResponse(customBidderResponse, bidRequest); + expect(bids[0].mediaType).to.equal('banner'); + }); + + it('should support multi-bid (multiple bids for one imp object) - Prebid v10 feature', function () { + // Multi-bid: Server can return multiple bids for a single imp object + // This is supported by ortbConverter which trustx adapter uses + const multiBidResponse = { + headers: null, + body: { + id: 'trustx-response-multi-bid', + seatbid: [ + { + bid: [ + { + id: 'bid-1', + impid: 'trustx-bid-12345', // Same impid - multiple bids for one imp + price: 2.50, + adm: '
Bid 1 Creative
', + adid: 'ad-1', + crid: 'creative-1', + w: 300, + h: 250, + mtype: 1, // Banner + ext: { + prebid: { type: 'banner' } + } + }, + { + id: 'bid-2', + impid: 'trustx-bid-12345', // Same impid - multiple bids for one imp + price: 3.00, + adm: '
Bid 2 Creative
', + adid: 'ad-2', + crid: 'creative-2', + w: 300, + h: 250, + mtype: 1, // Banner + ext: { + prebid: { type: 'banner' } + } + }, + { + id: 'bid-3', + impid: 'trustx-bid-12345', // Same impid - multiple bids for one imp + price: 2.75, + adm: '
Bid 3 Creative
', + adid: 'ad-3', + crid: 'creative-3', + w: 300, + h: 250, + mtype: 1, // Banner + ext: { + prebid: { type: 'banner' } + } + } + ], + seat: 'trustx' + } + ] + } + }; + + const bids = spec.interpretResponse(multiBidResponse, bidRequest); + + // Trustx adapter should return all bids (multi-bid support via ortbConverter) + expect(bids).to.be.an('array'); + expect(bids).to.have.length(3); // All 3 bids should be returned + + // Verify each bid + expect(bids[0].requestId).to.equal('trustx-bid-12345'); + expect(bids[0].cpm).to.equal(2.50); + expect(bids[0].ad).to.equal('
Bid 1 Creative
'); + expect(bids[0].creativeId).to.equal('creative-1'); + + expect(bids[1].requestId).to.equal('trustx-bid-12345'); + expect(bids[1].cpm).to.equal(3.00); + expect(bids[1].ad).to.equal('
Bid 2 Creative
'); + expect(bids[1].creativeId).to.equal('creative-2'); + + expect(bids[2].requestId).to.equal('trustx-bid-12345'); + expect(bids[2].cpm).to.equal(2.75); + expect(bids[2].ad).to.equal('
Bid 3 Creative
'); + expect(bids[2].creativeId).to.equal('creative-3'); + }); + + it('should handle response with ext.userid (userid is saved to localStorage by adapter)', function () { + const userId = '42d55fac-da65-4468-b854-06f40cfe3852'; + const impId = bidRequest.data.imp[0].id; + const responseWithUserId = { + headers: null, + body: { + id: 'trustx-response-with-userid', + seatbid: [{ + bid: [{ + id: 'bid-1', + impid: impId, + price: 0.5, + adm: '
Test Ad
', + w: 300, + h: 250, + mtype: 1 + }], + seat: 'trustx' + }], + ext: { + userid: userId + } + } + }; + + // Test that interpretResponse handles ext.userid without errors + // (actual localStorage saving is tested at integration level) + const bids = spec.interpretResponse(responseWithUserId, bidRequest); + + expect(bids).to.be.an('array').that.is.not.empty; + expect(bids[0].cpm).to.equal(0.5); + // ext.userid is processed internally by adapter and saved to localStorage + // if localStorageWriteAllowed is true and localStorage is enabled + }); + + it('should handle response with nurl and burl tracking URLs', function () { + const impId = bidRequest.data.imp[0].id; + const nurl = 'https://trackers.trustx.org/event?brid=xxx&e=nurl&cpm=${AUCTION_PRICE}'; + const burl = 'https://trackers.trustx.org/event?brid=xxx&e=burl&cpm=${AUCTION_PRICE}'; + const responseWithTracking = { + headers: null, + body: { + id: 'trustx-response-tracking', + seatbid: [{ + bid: [{ + id: 'bid-1', + impid: impId, + price: 0.0413, + adm: '
Test Ad
', + nurl: nurl, + burl: burl, + adid: 'z0bgu851', + w: 728, + h: 90, + mtype: 1 + }], + seat: 'trustx' + }] + } + }; + + const bids = spec.interpretResponse(responseWithTracking, bidRequest); + + expect(bids).to.be.an('array').that.is.not.empty; + // burl is directly mapped to bidResponse.burl by ortbConverter + expect(bids[0].burl).to.equal(burl); + // nurl is processed by banner processor: if adm exists, nurl is embedded as tracking pixel in ad + // if no adm, nurl becomes adUrl. In this case, we have adm, so nurl is in ad as tracking pixel + expect(bids[0].ad).to.include(nurl); // nurl should be embedded in ad as tracking pixel + expect(bids[0].ad).to.include('
Test Ad
'); // original adm should still be there + }); + + it('should handle response with adomain and cat (categories)', function () { + const impId = bidRequest.data.imp[0].id; + const responseWithCategories = { + headers: null, + body: { + id: 'trustx-response-categories', + seatbid: [{ + bid: [{ + id: 'bid-1', + impid: impId, + price: 0.5, + adm: '
Test Ad
', + adid: 'z0bgu851', + adomain: ['adl.org'], + cat: ['IAB6'], + w: 728, + h: 90, + mtype: 1 + }], + seat: 'trustx' + }] + } + }; + + const bids = spec.interpretResponse(responseWithCategories, bidRequest); + + expect(bids).to.be.an('array').that.is.not.empty; + expect(bids[0].meta).to.exist; + expect(bids[0].meta.advertiserDomains).to.deep.equal(['adl.org']); + expect(bids[0].meta.primaryCatId).to.equal('IAB6'); + }); + }); + + context('when mediaType is video', function () { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getVideoRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + + it('handles empty response', function () { + const EMPTY_RESP = Object.assign({}, bidderResponse, {'body': {}}); + const bids = spec.interpretResponse(EMPTY_RESP, bidRequest); + + expect(bids).to.be.empty; + }); + + it('should return no bids if the response "nurl" and "adm" are missing', function () { + const SERVER_RESP = Object.assign({}, bidderResponse, {'body': { + seatbid: [{ + bid: [{ + price: 8.01 + }] + }] + }}); + const bids = spec.interpretResponse(SERVER_RESP, bidRequest); + expect(bids.length).to.equal(0); + }); + + it('should return no bids if the response "price" is missing', function () { + const SERVER_RESP = Object.assign({}, bidderResponse, {'body': { + seatbid: [{ + bid: [{ + adm: '' + }] + }] + }}); + const bids = spec.interpretResponse(SERVER_RESP, bidRequest); + expect(bids.length).to.equal(0); + }); + + it('should determine media type from mtype field for video', function () { + const SERVER_RESP = Object.assign({}, bidderResponse, { + 'body': { + seatbid: [{ + bid: [{ + id: 'trustx-video-bid-1', + impid: 'trustx-video-bid-1', + price: 10.00, + adm: '', + adid: '987654321', + adomain: ['trustx-advertiser.com'], + iurl: 'https://trustx-campaign.com/creative.jpg', + cid: '12345', + crid: 'trustx-creative-234', + w: 1920, + h: 1080, + mtype: 2, // Video type + ext: { + prebid: { + type: 'video' + } + } + }] + }] + } + }); + + const bids = spec.interpretResponse(SERVER_RESP, bidRequest); + expect(bids[0].mediaType).to.equal('video'); + }); + }); + }); + + describe('getUserSyncs', function () { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getVideoRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + + it('handles no parameters', function () { + let opts = spec.getUserSyncs({}); + expect(opts).to.be.an('array').that.is.empty; + }); + it('returns non if sync is not allowed', function () { + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + + expect(opts).to.be.an('array').that.is.empty; + }); + + it('iframe sync enabled should return results', function () { + let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [bidderResponse]); + + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal(bidderResponse.body.ext.usersync['sync1'].syncs[0].url); + }); + + it('pixel sync enabled should return results', function () { + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [bidderResponse]); + + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal(bidderResponse.body.ext.usersync['sync2'].syncs[0].url); + }); + + it('all sync enabled should prioritize iframe', function () { + let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [bidderResponse]); + + expect(opts.length).to.equal(1); + }); + + it('should handle real-world usersync with multiple providers (ttd, mediaforce, bidswitch, trustx)', function () { + const realWorldResponse = { + headers: null, + body: { + id: '40998eeaaba56068', + seatbid: [{ + bid: [{ + id: '435ddfcec96f2ab', + impid: '435ddfcec96f2ab', + price: 0.0413, + adm: '
Test Ad
', + w: 728, + h: 90, + mtype: 1 + }], + seat: '15' + }], + ext: { + usersync: { + ttd: { + syncs: [ + { + url: 'https://match.adsrvr.org/track/cmf/generic?ttd_tpi=1&ttd_pid=lewrkpm', + type: 'pixel' + }, + { + url: 'https://match.adsrvr.org/track/cmf/generic?ttd_tpi=1&ttd_pid=q4jldgr', + type: 'pixel' + } + ] + }, + mediaforce: { + syncs: [ + { + url: 'https://rtb.mfadsrvr.com/sync?ssp=trustx', + type: 'pixel' + } + ] + }, + bidswitch: { + syncs: [ + { + url: 'https://x.bidswitch.net/sync?ssp=trustx&user_id=42d55fac-da65-4468-b854-06f40cfe3852', + type: 'pixel' + } + ] + }, + trustx: { + syncs: [ + { + url: 'https://sync.trustx.org/usync?gdpr=&gdpr_consent=&us_privacy=1---&gpp=&gpp_sid=&publisher_id=101452&source=pbjs-response', + type: 'pixel' + }, + { + url: 'https://static.cdn.trustx.org/x/user_sync.html?gdpr=&gdpr_consent=&us_privacy=1---&gpp=&gpp_sid=&publisher_id=101452&source=pbjs-response', + type: 'iframe' + } + ] + } + }, + userid: '42d55fac-da65-4468-b854-06f40cfe3852' + } + } + }; + + // Test with pixel enabled + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [realWorldResponse]); + + // Should return all pixel syncs from all providers + expect(opts).to.be.an('array'); + expect(opts.length).to.equal(5); // 2 ttd + 1 mediaforce + 1 bidswitch + 1 trustx pixel + expect(opts.every(s => s.type === 'image')).to.be.true; + expect(opts.some(s => s.url.includes('adsrvr.org'))).to.be.true; + expect(opts.some(s => s.url.includes('mfadsrvr.com'))).to.be.true; + expect(opts.some(s => s.url.includes('bidswitch.net'))).to.be.true; + expect(opts.some(s => s.url.includes('sync.trustx.org'))).to.be.true; + + // Test with iframe enabled + opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [realWorldResponse]); + + // Should return only iframe syncs + expect(opts).to.be.an('array'); + expect(opts.length).to.equal(1); // 1 trustx iframe + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.include('static.cdn.trustx.org'); + }); + }); +}); diff --git a/test/spec/modules/uid2IdSystem_spec.js b/test/spec/modules/uid2IdSystem_spec.js index a90972d4163..f8980bd3961 100644 --- a/test/spec/modules/uid2IdSystem_spec.js +++ b/test/spec/modules/uid2IdSystem_spec.js @@ -35,7 +35,7 @@ const makeUid2IdentityContainer = (token) => ({uid2: {id: token}}); const makeUid2OptoutContainer = (token) => ({uid2: {optout: true}}); let useLocalStorage = false; const makePrebidConfig = (params = null, extraSettings = {}, debug = false) => ({ - userSync: { auctionDelay: auctionDelayMs, userIds: [{name: 'uid2', params: {storage: useLocalStorage ? 'localStorage' : 'cookie', ...params}}] }, debug, ...extraSettings + userSync: { auctionDelay: extraSettings.auctionDelay ?? auctionDelayMs, ...(extraSettings.syncDelay !== undefined && {syncDelay: extraSettings.syncDelay}), userIds: [{name: 'uid2', params: {storage: useLocalStorage ? 'localStorage' : 'cookie', ...params}}] }, debug }); const makeOriginalIdentity = (identity, salt = 1) => ({ identity: utils.cyrb53Hash(identity, salt), diff --git a/test/spec/modules/uniquest_widgetBidAdapter_spec.js b/test/spec/modules/uniquest_widgetBidAdapter_spec.js new file mode 100644 index 00000000000..55a06a976bc --- /dev/null +++ b/test/spec/modules/uniquest_widgetBidAdapter_spec.js @@ -0,0 +1,102 @@ +import { expect } from 'chai'; +import { spec } from 'modules/uniquest_widgetBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +const ENDPOINT = 'https://adpb.ust-ad.com/hb/prebid/widgets'; + +describe('uniquest_widgetBidAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + const request = { + bidder: 'uniquest_widget', + params: { + wid: 'wid_0001', + }, + }; + expect(spec.isBidRequestValid(request)).to.equal(true) + }) + + it('should return false when required params are not passed', function () { + expect(spec.isBidRequestValid({})).to.equal(false) + expect(spec.isBidRequestValid({ wid: '' })).to.equal(false) + }) + }) + + describe('buildRequest', function () { + const bids = [ + { + bidder: 'uniquest_widget', + params: { + wid: 'wid_0001', + }, + adUnitCode: 'adunit-code', + sizes: [ + [1, 1], + ], + bidId: '359d7a594535852', + bidderRequestId: '247f62f777e5e4', + } + ]; + const bidderRequest = { + timeout: 1500, + } + it('sends bid request to ENDPOINT via GET', function () { + const requests = spec.buildRequests(bids, bidderRequest); + expect(requests[0].url).to.equal(ENDPOINT); + expect(requests[0].method).to.equal('GET'); + expect(requests[0].data).to.equal('bid=359d7a594535852&wid=wid_0001&widths=1&heights=1&timeout=1500&') + }) + }) + + describe('interpretResponse', function() { + it('should return a valid bid response', function () { + const serverResponse = { + request_id: '347f62f777e5e4', + cpm: 12.3, + currency: 'JPY', + width: 1, + height: 1, + bid_id: 'bid_0001', + deal_id: '', + net_revenue: false, + ttl: 300, + ad: '
', + media_type: 'banner', + meta: { + advertiser_domains: ['advertiser.com'], + }, + }; + const expectResponse = [{ + requestId: '347f62f777e5e4', + cpm: 12.3, + currency: 'JPY', + width: 1, + height: 1, + ad: '
', + creativeId: 'bid_0001', + netRevenue: false, + mediaType: 'banner', + ttl: 300, + meta: { + advertiserDomains: ['advertiser.com'], + } + }]; + const result = spec.interpretResponse({ body: serverResponse }, {}); + expect(result).to.have.lengthOf(1); + expect(result).to.deep.have.same.members(expectResponse); + }) + + it('should return an empty array to indicate no valid bids', function () { + const result = spec.interpretResponse({ body: {} }, {}) + expect(result).is.an('array').is.empty; + }) + }) +}) diff --git a/test/spec/modules/wurflRtdProvider_spec.js b/test/spec/modules/wurflRtdProvider_spec.js index fa6ea2e642f..c446142db1a 100644 --- a/test/spec/modules/wurflRtdProvider_spec.js +++ b/test/spec/modules/wurflRtdProvider_spec.js @@ -1,24 +1,41 @@ import { - bidderData, - enrichBidderRequest, - lowEntropyData, wurflSubmodule, - makeOrtb2DeviceType, - toNumber, + storage } from 'modules/wurflRtdProvider'; import * as ajaxModule from 'src/ajax'; import { loadExternalScriptStub } from 'test/mocks/adloaderStub.js'; +import * as prebidGlobalModule from 'src/prebidGlobal.js'; +import { guardOrtb2Fragments } from 'libraries/objectGuard/ortbGuard.js'; +import { config } from 'src/config.js'; describe('wurflRtdProvider', function () { describe('wurflSubmodule', function () { const altHost = 'http://example.local/wurfl.js'; + // Global cleanup to ensure debug config doesn't leak between tests + afterEach(function () { + config.resetConfig(); + }); + const wurfl_pbjs = { - low_entropy_caps: ['is_mobile', 'complete_device_name', 'form_factor'], - caps: ['advertised_browser', 'advertised_browser_version', 'advertised_device_os', 'advertised_device_os_version', 'ajax_support_javascript', 'brand_name', 'complete_device_name', 'density_class', 'form_factor', 'is_android', 'is_app_webview', 'is_connected_tv', 'is_full_desktop', 'is_ios', 'is_mobile', 'is_ott', 'is_phone', 'is_robot', 'is_smartphone', 'is_smarttv', 'is_tablet', 'manufacturer_name', 'marketing_name', 'max_image_height', 'max_image_width', 'model_name', 'physical_screen_height', 'physical_screen_width', 'pixel_density', 'pointing_method', 'resolution_height', 'resolution_width'], - authorized_bidders: { - bidder1: [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31], - bidder2: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 25, 28, 30, 31] + caps: ['wurfl_id', 'advertised_browser', 'advertised_browser_version', 'advertised_device_os', 'advertised_device_os_version', 'ajax_support_javascript', 'brand_name', 'complete_device_name', 'density_class', 'form_factor', 'is_android', 'is_app_webview', 'is_connected_tv', 'is_full_desktop', 'is_ios', 'is_mobile', 'is_ott', 'is_phone', 'is_robot', 'is_smartphone', 'is_smarttv', 'is_tablet', 'manufacturer_name', 'marketing_name', 'max_image_height', 'max_image_width', 'model_name', 'physical_screen_height', 'physical_screen_width', 'pixel_density', 'pointing_method', 'resolution_height', 'resolution_width'], + over_quota: 0, + sampling_rate: 100, + global: { + basic_set: { + cap_indices: [0, 9, 15, 16, 17, 18, 32] + }, + publisher: { + cap_indices: [1, 2, 3, 4, 5] + } + }, + bidders: { + bidder1: { + cap_indices: [6, 7, 8, 10, 11, 26, 27] + }, + bidder2: { + cap_indices: [12, 13, 14, 19, 20, 21, 22] + } } } const WURFL = { @@ -58,10 +75,12 @@ describe('wurflRtdProvider', function () { }; // expected analytics values - const expectedStatsURL = 'https://prebid.wurflcloud.com/v1/prebid/stats'; + const expectedStatsURL = 'https://stats.prebid.wurflcloud.com/v2/prebid/stats'; const expectedData = JSON.stringify({ bidders: ['bidder1', 'bidder2'] }); let sandbox; + // originalUserAgentData to restore after tests + let originalUAData; beforeEach(function () { sandbox = sinon.createSandbox(); @@ -69,12 +88,19 @@ describe('wurflRtdProvider', function () { init: new Promise(function (resolve, reject) { resolve({ WURFL, wurfl_pbjs }) }), complete: new Promise(function (resolve, reject) { resolve({ WURFL, wurfl_pbjs }) }), }; + originalUAData = window.navigator.userAgentData; + // Initialize module with clean state for each test + wurflSubmodule.init({ params: {} }); }); afterEach(() => { // Restore the original functions sandbox.restore(); window.WURFLPromises = undefined; + Object.defineProperty(window.navigator, 'userAgentData', { + value: originalUAData, + configurable: true, + }); }); // Bid request config @@ -94,396 +120,2426 @@ describe('wurflRtdProvider', function () { } }; - it('initialises the WURFL RTD provider', function () { - expect(wurflSubmodule.init()).to.be.true; - }); + // Client Hints tests + describe('Client Hints support', () => { + it('should collect and send client hints when available', (done) => { + const clock = sinon.useFakeTimers(); + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; - it('should enrich the bid request data', (done) => { - const expectedURL = new URL(altHost); - expectedURL.searchParams.set('debug', true); - expectedURL.searchParams.set('mode', 'prebid'); - expectedURL.searchParams.set('wurfl_id', true); + // Mock Client Hints + const mockClientHints = { + architecture: 'arm', + bitness: '64', + model: 'Pixel 5', + platformVersion: '13.0.0', + uaFullVersion: '130.0.6723.58', + fullVersionList: [ + { brand: 'Chromium', version: '130.0.6723.58' } + ] + }; - const callback = () => { - const v = { - bidder1: { - device: { - make: 'Google', - model: 'Nexus 5', - devicetype: 1, - os: 'Android', - osv: '6.0', - hwv: 'Nexus 5', - h: 1920, - w: 1080, - ppi: 443, - pxratio: 3.0, - js: 1, - ext: { - wurfl: { - advertised_browser: 'Chrome Mobile', - advertised_browser_version: '130.0.0.0', - advertised_device_os: 'Android', - advertised_device_os_version: '6.0', - ajax_support_javascript: !0, - brand_name: 'Google', - complete_device_name: 'Google Nexus 5', - density_class: '3.0', - form_factor: 'Feature Phone', - is_app_webview: !1, - is_connected_tv: !1, - is_full_desktop: !1, - is_mobile: !0, - is_ott: !1, - is_phone: !0, - is_robot: !1, - is_smartphone: !1, - is_smarttv: !1, - is_tablet: !1, - manufacturer_name: 'LG', - marketing_name: '', - max_image_height: 640, - max_image_width: 360, - model_name: 'Nexus 5', - physical_screen_height: 110, - physical_screen_width: 62, - pixel_density: 443, - pointing_method: 'touchscreen', - resolution_height: 1920, - resolution_width: 1080, - wurfl_id: 'lg_nexus5_ver1', - }, - }, - }, - }, - bidder2: { - device: { - make: 'Google', - model: 'Nexus 5', - devicetype: 1, - os: 'Android', - osv: '6.0', - hwv: 'Nexus 5', - h: 1920, - w: 1080, - ppi: 443, - pxratio: 3.0, - js: 1, - ext: { - wurfl: { - advertised_device_os: 'Android', - advertised_device_os_version: '6.0', - ajax_support_javascript: !0, - brand_name: 'Google', - complete_device_name: 'Google Nexus 5', - density_class: '3.0', - form_factor: 'Feature Phone', - is_android: !0, - is_app_webview: !1, - is_connected_tv: !1, - is_full_desktop: !1, - is_ios: !1, - is_mobile: !0, - is_ott: !1, - is_phone: !0, - is_tablet: !1, - manufacturer_name: 'LG', - model_name: 'Nexus 5', - pixel_density: 443, - resolution_height: 1920, - resolution_width: 1080, - wurfl_id: 'lg_nexus5_ver1', - }, - }, - }, - }, - bidder3: { - device: { - make: 'Google', - model: 'Nexus 5', - ext: { - wurfl: { - complete_device_name: 'Google Nexus 5', - form_factor: 'Feature Phone', - is_mobile: !0, - model_name: 'Nexus 5', - brand_name: 'Google', - wurfl_id: 'lg_nexus5_ver1', - }, - }, - }, - }, + const getHighEntropyValuesStub = sandbox.stub().resolves(mockClientHints); + Object.defineProperty(navigator, 'userAgentData', { + value: { getHighEntropyValues: getHighEntropyValuesStub }, + configurable: true, + writable: true + }); + + // Empty cache to trigger async load + sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const callback = async () => { + // Verify client hints were requested + expect(getHighEntropyValuesStub.calledOnce).to.be.true; + expect(getHighEntropyValuesStub.calledWith( + ['architecture', 'bitness', 'model', 'platformVersion', 'uaFullVersion', 'fullVersionList'] + )).to.be.true; + + try { + // Use tickAsync to properly handle promise microtasks + await clock.tickAsync(1); + + // Now verify WURFL.js was loaded with client hints in URL + expect(loadExternalScriptStub.called).to.be.true; + const scriptUrl = loadExternalScriptStub.getCall(0).args[0]; + + const url = new URL(scriptUrl); + const uachParam = url.searchParams.get('uach'); + expect(uachParam).to.not.be.null; + + const parsedHints = JSON.parse(uachParam); + expect(parsedHints).to.deep.equal(mockClientHints); + + clock.restore(); + done(); + } catch (err) { + clock.restore(); + done(err); + } }; - expect(reqBidsConfigObj.ortb2Fragments.bidder).to.deep.equal(v); - done(); - }; - const config = { - params: { - altHost: altHost, - debug: true, - } - }; - const userConsent = {}; + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }) + it('should load WURFL.js without client hints when not available', (done) => { + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; - wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, userConsent); - expect(loadExternalScriptStub.calledOnce).to.be.true; - const loadExternalScriptCall = loadExternalScriptStub.getCall(0); - expect(loadExternalScriptCall.args[0]).to.equal(expectedURL.toString()); - expect(loadExternalScriptCall.args[2]).to.equal('wurfl'); - }); + // No client hints available + Object.defineProperty(navigator, 'userAgentData', { + value: undefined, + configurable: true, + writable: true + }); - it('onAuctionEndEvent: should send analytics data using navigator.sendBeacon, if available', () => { - const auctionDetails = {}; - const config = {}; - const userConsent = {}; + // Empty cache to trigger async load + sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); - const sendBeaconStub = sandbox.stub(navigator, 'sendBeacon'); + const callback = () => { + // Verify WURFL.js was loaded without uach parameter + expect(loadExternalScriptStub.calledOnce).to.be.true; + const scriptUrl = loadExternalScriptStub.getCall(0).args[0]; - // Call the function - wurflSubmodule.onAuctionEndEvent(auctionDetails, config, userConsent); + const url = new URL(scriptUrl); + const uachParam = url.searchParams.get('uach'); + expect(uachParam).to.be.null; + + done(); + }; - // Assertions - expect(sendBeaconStub.calledOnce).to.be.true; - expect(sendBeaconStub.calledWithExactly(expectedStatsURL, expectedData)).to.be.true; + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); }); - it('onAuctionEndEvent: should send analytics data using fetch as fallback, if navigator.sendBeacon is not available', () => { - const auctionDetails = {}; - const config = {}; - const userConsent = {}; + // TTL handling tests + describe('TTL handling', () => { + it('should use valid (not expired) cached data without triggering async load', (done) => { + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; - const sendBeaconStub = sandbox.stub(navigator, 'sendBeacon').value(undefined); - const windowFetchStub = sandbox.stub(window, 'fetch'); - const fetchAjaxStub = sandbox.stub(ajaxModule, 'fetch'); + // Setup cache with valid TTL (expires in future) + const futureExpiry = Date.now() + 1000000; // expires in future + const cachedData = { + WURFL, + wurfl_pbjs: { ...wurfl_pbjs, ttl: 2592000 }, + expire_at: futureExpiry + }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); - // Call the function - wurflSubmodule.onAuctionEndEvent(auctionDetails, config, userConsent); + const callback = () => { + // Verify global FPD enrichment happened (not over quota) + expect(reqBidsConfigObj.ortb2Fragments.global.device).to.deep.include({ + make: 'Google', + model: 'Nexus 5', + devicetype: 4 + }); - // Assertions - expect(sendBeaconStub.called).to.be.false; + // Verify no async load was triggered (cache is valid) + expect(loadExternalScriptStub.called).to.be.false; - expect(fetchAjaxStub.calledOnce).to.be.true; - const fetchAjaxCall = fetchAjaxStub.getCall(0); - expect(fetchAjaxCall.args[0]).to.equal(expectedStatsURL); - expect(fetchAjaxCall.args[1].method).to.equal('POST'); - expect(fetchAjaxCall.args[1].body).to.equal(expectedData); - expect(fetchAjaxCall.args[1].mode).to.equal('no-cors'); - }); - }); + done(); + }; - describe('bidderData', () => { - it('should return the WURFL data for a bidder', () => { - const wjsData = { - capability1: 'value1', - capability2: 'value2', - capability3: 'value3', - }; - const caps = ['capability1', 'capability2', 'capability3']; - const filter = [0, 2]; + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); - const result = bidderData(wjsData, caps, filter); + it('should use expired cached data and trigger async refresh (without Client Hints)', (done) => { + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; - expect(result).to.deep.equal({ - capability1: 'value1', - capability3: 'value3', + Object.defineProperty(navigator, 'userAgentData', { + value: undefined, + configurable: true, + writable: true + }); + // Setup cache with expired TTL + const pastExpiry = Date.now() - 1000; // expired 1 second ago + const cachedData = { + WURFL, + wurfl_pbjs: { ...wurfl_pbjs, ttl: 2592000 }, + expire_at: pastExpiry + }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const callback = () => { + // Verify expired cache data is still used for enrichment + expect(reqBidsConfigObj.ortb2Fragments.global.device).to.deep.include({ + make: 'Google', + model: 'Nexus 5', + devicetype: 4 + }); + + // Verify bidders were enriched + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder1).to.exist; + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder2).to.exist; + + // Verify async load WAS triggered for refresh (cache expired) + expect(loadExternalScriptStub.calledOnce).to.be.true; + + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); }); }); - it('should return an empty object if the filter is empty', () => { - const wjsData = { - capability1: 'value1', - capability2: 'value2', - capability3: 'value3', - }; - const caps = ['capability1', 'capability3']; - const filter = []; + // Debug mode initialization tests + describe('Debug mode', () => { + afterEach(() => { + // Clean up window object after each test + delete window.WurflRtdDebug; + // Reset global config + config.resetConfig(); + }); - const result = bidderData(wjsData, caps, filter); + it('should not create window.WurflRtdDebug when global debug=false', () => { + config.setConfig({ debug: false }); + const moduleConfig = { params: {} }; + wurflSubmodule.init(moduleConfig); + expect(window.WurflRtdDebug).to.be.undefined; + }); - expect(result).to.deep.equal({}); - }); - }); + it('should not create window.WurflRtdDebug when global debug is not configured', () => { + config.resetConfig(); + const moduleConfig = { params: {} }; + wurflSubmodule.init(moduleConfig); + expect(window.WurflRtdDebug).to.be.undefined; + }); - describe('lowEntropyData', () => { - it('should return the correct low entropy data for Apple devices', () => { - const wjsData = { - complete_device_name: 'Apple iPhone X', - form_factor: 'Smartphone', - is_mobile: !0, - brand_name: 'Apple', - model_name: 'iPhone X', - }; - const lowEntropyCaps = ['complete_device_name', 'form_factor', 'is_mobile']; - const expectedData = { - complete_device_name: 'Apple iPhone', - form_factor: 'Smartphone', - is_mobile: !0, - brand_name: 'Apple', - model_name: 'iPhone', - }; - const result = lowEntropyData(wjsData, lowEntropyCaps); - expect(result).to.deep.equal(expectedData); + it('should create window.WurflRtdDebug when global debug=true', () => { + config.setConfig({ debug: true }); + const moduleConfig = { params: {} }; + wurflSubmodule.init(moduleConfig); + expect(window.WurflRtdDebug).to.exist; + expect(window.WurflRtdDebug.dataSource).to.equal('unknown'); + expect(window.WurflRtdDebug.cacheExpired).to.be.false; + }); }); - it('should return the correct low entropy data for Android devices', () => { - const wjsData = { - complete_device_name: 'Samsung SM-G981B (Galaxy S20 5G)', - form_factor: 'Smartphone', - is_mobile: !0, - }; - const lowEntropyCaps = ['complete_device_name', 'form_factor', 'is_mobile']; - const expectedData = { - complete_device_name: 'Samsung SM-G981B (Galaxy S20 5G)', - form_factor: 'Smartphone', - is_mobile: !0, - }; - const result = lowEntropyData(wjsData, lowEntropyCaps); - expect(result).to.deep.equal(expectedData); + it('initialises the WURFL RTD provider', function () { + expect(wurflSubmodule.init()).to.be.true; }); - it('should return an empty object if the lowEntropyCaps array is empty', () => { - const wjsData = { - complete_device_name: 'Samsung SM-G981B (Galaxy S20 5G)', - form_factor: 'Smartphone', - is_mobile: !0, - }; - const lowEntropyCaps = []; - const expectedData = {}; - const result = lowEntropyData(wjsData, lowEntropyCaps); - expect(result).to.deep.equal(expectedData); - }); - }); + describe('A/B testing', () => { + it('should return true when A/B testing is disabled', () => { + const config = { params: { abTest: false } }; + expect(wurflSubmodule.init(config)).to.be.true; + }); - describe('enrichBidderRequest', () => { - it('should enrich the bidder request with WURFL data', () => { - const reqBidsConfigObj = { - ortb2Fragments: { - global: { - device: {}, - }, - bidder: { - exampleBidder: { - device: { - ua: 'user-agent', + it('should return true when A/B testing is not configured', () => { + const config = { params: {} }; + expect(wurflSubmodule.init(config)).to.be.true; + }); + + it('should return true for users in treatment group (random < abSplit)', () => { + sandbox.stub(Math, 'random').returns(0.25); // 0.25 < 0.5 = treatment + const config = { params: { abTest: true, abName: 'test_sept', abSplit: 0.5 } }; + expect(wurflSubmodule.init(config)).to.be.true; + }); + + it('should return true for users in control group (random >= abSplit)', () => { + sandbox.stub(Math, 'random').returns(0.75); // 0.75 >= 0.5 = control + const config = { params: { abTest: true, abName: 'test_sept', abSplit: 0.5 } }; + expect(wurflSubmodule.init(config)).to.be.true; + }); + + it('should use default abSplit of 0.5 when not specified', () => { + sandbox.stub(Math, 'random').returns(0.40); // 0.40 < 0.5 = treatment + const config = { params: { abTest: true, abName: 'test_sept' } }; + expect(wurflSubmodule.init(config)).to.be.true; + }); + + it('should handle abSplit of 0 (all control)', () => { + sandbox.stub(Math, 'random').returns(0.01); // split <= 0 = control + const config = { params: { abTest: true, abName: 'test_sept', abSplit: 0 } }; + expect(wurflSubmodule.init(config)).to.be.true; + }); + + it('should handle abSplit of 1 (all treatment)', () => { + sandbox.stub(Math, 'random').returns(0.99); // split >= 1 = treatment + const config = { params: { abTest: true, abName: 'test_sept', abSplit: 1 } }; + expect(wurflSubmodule.init(config)).to.be.true; + }); + + it('should skip enrichment for control group in getBidRequestData', (done) => { + sandbox.stub(Math, 'random').returns(0.75); // Control group + const config = { params: { abTest: true, abName: 'test_sept', abSplit: 0.5 } }; + + // Initialize with A/B test config + wurflSubmodule.init(config); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + // Control group should not enrich + expect(reqBidsConfigObj.ortb2Fragments.global.device).to.deep.equal({}); + expect(reqBidsConfigObj.ortb2Fragments.bidder).to.deep.equal({}); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, {}); + }); + + it('should send beacon with ab_name and ab_variant for treatment group', (done) => { + sandbox.stub(Math, 'random').returns(0.25); // Treatment group + const config = { params: { abTest: true, abName: 'test_sept', abSplit: 0.5 } }; + + // Initialize with A/B test config + wurflSubmodule.init(config); + + const cachedData = { WURFL, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon').returns(true); + + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => [] + }); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [{ bidder: 'bidder1' }] } - } - } - } - }; - const bidderCode = 'exampleBidder'; - const wjsData = { - capability1: 'value1', - capability2: 'value2' - }; + ] + }; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, null); + + expect(sendBeaconStub.calledOnce).to.be.true; + const beaconCall = sendBeaconStub.getCall(0); + const payload = JSON.parse(beaconCall.args[1]); + expect(payload).to.have.property('ab_name', 'test_sept'); + expect(payload).to.have.property('ab_variant', 'treatment'); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, {}); + }); + + it('should send beacon with ab_name and ab_variant for control group', (done) => { + sandbox.stub(Math, 'random').returns(0.75); // Control group + const config = { params: { abTest: true, abName: 'test_sept', abSplit: 0.5 } }; + + // Initialize with A/B test config + wurflSubmodule.init(config); + + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon').returns(true); + + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => [] + }); - enrichBidderRequest(reqBidsConfigObj, bidderCode, wjsData); + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; - expect(reqBidsConfigObj.ortb2Fragments.bidder).to.deep.equal({ - exampleBidder: { - device: { - ua: 'user-agent', - ext: { - wurfl: { - capability1: 'value1', - capability2: 'value2' + const callback = () => { + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [{ bidder: 'bidder1' }] } - } - } - } + ] + }; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, null); + + expect(sendBeaconStub.calledOnce).to.be.true; + const beaconCall = sendBeaconStub.getCall(0); + const payload = JSON.parse(beaconCall.args[1]); + expect(payload).to.have.property('ab_name', 'test_sept'); + expect(payload).to.have.property('ab_variant', 'control'); + expect(payload).to.have.property('enrichment', 'none'); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, {}); }); - }); - }); - describe('makeOrtb2DeviceType', function () { - it('should return 1 when wurflData is_mobile and is_phone is true', function () { - const wurflData = { is_mobile: true, is_phone: true, is_tablet: false }; - const result = makeOrtb2DeviceType(wurflData); - expect(result).to.equal(1); - }); + describe('ABTestManager behavior', () => { + it('should disable A/B test when abTest is false', () => { + const config = { params: { abTest: false } }; + wurflSubmodule.init(config); + // A/B test disabled, so enrichment should proceed normally + expect(wurflSubmodule.init(config)).to.be.true; + }); - it('should return 1 when wurflData is_mobile and is_tablet is true', function () { - const wurflData = { is_mobile: true, is_phone: false, is_tablet: true }; - const result = makeOrtb2DeviceType(wurflData); - expect(result).to.equal(1); - }); + it('should assign control group when split is 0', (done) => { + sandbox.stub(Math, 'random').returns(0.01); + const config = { params: { abTest: true, abName: 'test_split', abSplit: 0, abExcludeLCE: false } }; + wurflSubmodule.init(config); - it('should return 6 when wurflData is_mobile but is_phone and is_tablet are false', function () { - const wurflData = { is_mobile: true, is_phone: false, is_tablet: false }; - const result = makeOrtb2DeviceType(wurflData); - expect(result).to.equal(6); - }); + const cachedData = { WURFL, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); - it('should return 2 when wurflData is_full_desktop is true', function () { - const wurflData = { is_full_desktop: true }; - const result = makeOrtb2DeviceType(wurflData); - expect(result).to.equal(2); - }); + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; - it('should return 3 when wurflData is_connected_tv is true', function () { - const wurflData = { is_connected_tv: true }; - const result = makeOrtb2DeviceType(wurflData); - expect(result).to.equal(3); - }); + const callback = () => { + // Control group should skip enrichment + expect(reqBidsConfigObj.ortb2Fragments.global.device).to.deep.equal({}); + done(); + }; - it('should return 4 when wurflData is_phone is true and is_mobile is false or undefined', function () { - const wurflData = { is_phone: true }; - const result = makeOrtb2DeviceType(wurflData); - expect(result).to.equal(4); - }); + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, {}); + }); - it('should return 5 when wurflData is_tablet is true and is_mobile is false or undefined', function () { - const wurflData = { is_tablet: true }; - const result = makeOrtb2DeviceType(wurflData); - expect(result).to.equal(5); - }); + it('should assign treatment group when split is 1', (done) => { + sandbox.stub(Math, 'random').returns(0.99); + const config = { params: { abTest: true, abName: 'test_split', abSplit: 1, abExcludeLCE: false } }; + wurflSubmodule.init(config); - it('should return 7 when wurflData is_ott is true', function () { - const wurflData = { is_ott: true }; - const result = makeOrtb2DeviceType(wurflData); - expect(result).to.equal(7); - }); + const cachedData = { WURFL, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); - it('should return undefined when wurflData is_mobile is true but is_phone and is_tablet are missing', function () { - const wurflData = { is_mobile: true }; - const result = makeOrtb2DeviceType(wurflData); - expect(result).to.be.undefined; - }); + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; - it('should return undefined when no conditions are met', function () { - const wurflData = {}; - const result = makeOrtb2DeviceType(wurflData); - expect(result).to.be.undefined; - }); - }); + const callback = () => { + // Treatment group should enrich + expect(reqBidsConfigObj.ortb2Fragments.global.device).to.not.deep.equal({}); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, {}); + }); + + it('should use default abName when not provided', (done) => { + sandbox.stub(Math, 'random').returns(0.25); + const config = { params: { abTest: true, abSplit: 0.5, abExcludeLCE: false } }; + wurflSubmodule.init(config); + + const cachedData = { WURFL, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon').returns(true); + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => [] + }); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [{ bidder: 'bidder1' }] + } + ] + }; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, null); + + const payload = JSON.parse(sendBeaconStub.getCall(0).args[1]); + expect(payload).to.have.property('ab_name', 'unknown'); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, {}); + }); + + it('should exclude LCE from A/B test when abExcludeLCE is true (control group)', (done) => { + sandbox.stub(Math, 'random').returns(0.75); // Control group + const config = { params: { abTest: true, abName: 'test_lce', abSplit: 0.5, abExcludeLCE: true } }; + wurflSubmodule.init(config); + + // Trigger LCE (no cache) + sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon').returns(true); + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => [] + }); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + // Control group should still enrich with LCE when excluded + expect(reqBidsConfigObj.ortb2Fragments.global.device).to.have.property('js', 1); + + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [{ bidder: 'bidder1' }] + } + ] + }; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, null); + + const payload = JSON.parse(sendBeaconStub.getCall(0).args[1]); + // Beacon should NOT include ab_name and ab_variant when LCE excluded + expect(payload).to.not.have.property('ab_name'); + expect(payload).to.not.have.property('ab_variant'); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, {}); + }); + + it('should exclude LCE from A/B test when abExcludeLCE is true (treatment group)', (done) => { + sandbox.stub(Math, 'random').returns(0.25); // Treatment group + const config = { params: { abTest: true, abName: 'test_lce', abSplit: 0.5, abExcludeLCE: true } }; + wurflSubmodule.init(config); + + // Trigger LCE (no cache) + sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon').returns(true); + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => [] + }); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + // Treatment group should enrich with LCE + expect(reqBidsConfigObj.ortb2Fragments.global.device).to.have.property('js', 1); + + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [{ bidder: 'bidder1' }] + } + ] + }; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, null); + + const payload = JSON.parse(sendBeaconStub.getCall(0).args[1]); + // Beacon should NOT include ab_name and ab_variant when LCE excluded + expect(payload).to.not.have.property('ab_name'); + expect(payload).to.not.have.property('ab_variant'); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, {}); + }); + + it('should include WURFL in A/B test when abExcludeLCE is true (control group)', (done) => { + sandbox.stub(Math, 'random').returns(0.75); // Control group + const config = { params: { abTest: true, abName: 'test_wurfl', abSplit: 0.5, abExcludeLCE: true } }; + wurflSubmodule.init(config); + + // Provide WURFL cache + const cachedData = { WURFL, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon').returns(true); + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => [] + }); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + // Control group should skip enrichment + expect(reqBidsConfigObj.ortb2Fragments.global.device).to.deep.equal({}); + + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [{ bidder: 'bidder1' }] + } + ] + }; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, null); + + const payload = JSON.parse(sendBeaconStub.getCall(0).args[1]); + // Beacon should include ab_name and ab_variant for WURFL + expect(payload).to.have.property('ab_name', 'test_wurfl'); + expect(payload).to.have.property('ab_variant', 'control'); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, {}); + }); + + it('should include LCE in A/B test when abExcludeLCE is false (control group)', (done) => { + sandbox.stub(Math, 'random').returns(0.75); // Control group + const config = { params: { abTest: true, abName: 'test_include_lce', abSplit: 0.5, abExcludeLCE: false } }; + wurflSubmodule.init(config); + + // Trigger LCE (no cache) + sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon').returns(true); + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => [] + }); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + // Control group should skip enrichment even with LCE + expect(reqBidsConfigObj.ortb2Fragments.global.device).to.deep.equal({}); + + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [{ bidder: 'bidder1' }] + } + ] + }; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, null); + + const payload = JSON.parse(sendBeaconStub.getCall(0).args[1]); + // Beacon should include ab_name and ab_variant + expect(payload).to.have.property('ab_name', 'test_include_lce'); + expect(payload).to.have.property('ab_variant', 'control'); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, {}); + }); + + it('should include LCE in A/B test when abExcludeLCE is false (treatment group)', (done) => { + sandbox.stub(Math, 'random').returns(0.25); // Treatment group + const config = { params: { abTest: true, abName: 'test_include_lce', abSplit: 0.5, abExcludeLCE: false } }; + wurflSubmodule.init(config); + + // Trigger LCE (no cache) + sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon').returns(true); + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => [] + }); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + // Treatment group should enrich with LCE + expect(reqBidsConfigObj.ortb2Fragments.global.device).to.have.property('js', 1); + + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [{ bidder: 'bidder1' }] + } + ] + }; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, null); + + const payload = JSON.parse(sendBeaconStub.getCall(0).args[1]); + // Beacon should include ab_name and ab_variant + expect(payload).to.have.property('ab_name', 'test_include_lce'); + expect(payload).to.have.property('ab_variant', 'treatment'); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, {}); + }); + + it('should default abExcludeLCE to true', (done) => { + sandbox.stub(Math, 'random').returns(0.75); // Control group + const config = { params: { abTest: true, abName: 'test_default', abSplit: 0.5 } }; // No abExcludeLCE specified + wurflSubmodule.init(config); + + // Trigger LCE (no cache) + sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon').returns(true); + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => [] + }); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + // Should behave like abExcludeLCE: true (control enriches with LCE) + expect(reqBidsConfigObj.ortb2Fragments.global.device).to.have.property('js', 1); + + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [{ bidder: 'bidder1' }] + } + ] + }; - describe('toNumber', function () { - it('converts valid numbers', function () { - expect(toNumber(42)).to.equal(42); - expect(toNumber(3.14)).to.equal(3.14); - expect(toNumber('100')).to.equal(100); - expect(toNumber('3.14')).to.equal(3.14); - expect(toNumber(' 50 ')).to.equal(50); + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, null); + + const payload = JSON.parse(sendBeaconStub.getCall(0).args[1]); + // Beacon should NOT include ab_name and ab_variant (default is true) + expect(payload).to.not.have.property('ab_name'); + expect(payload).to.not.have.property('ab_variant'); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, {}); + }); + }); }); - it('converts booleans correctly', function () { - expect(toNumber(true)).to.equal(1); - expect(toNumber(false)).to.equal(0); + it('should enrich multiple bidders with cached WURFL data (not over quota)', (done) => { + // Reset reqBidsConfigObj to clean state + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + // Setup localStorage with cached WURFL data + const cachedData = { WURFL, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const callback = () => { + // Verify global FPD has device data (not over quota) + expect(reqBidsConfigObj.ortb2Fragments.global.device).to.deep.include({ + make: 'Google', + model: 'Nexus 5', + devicetype: 4, + os: 'Android', + osv: '6.0', + hwv: 'Nexus 5', + h: 1920, + w: 1080, + ppi: 443, + pxratio: 3.0, + js: 1 + }); + + // Verify global has ext.wurfl with basic+pub capabilities (new behavior) + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext).to.exist; + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext.wurfl).to.exist; + + // Calculate expected basic+pub caps + const basicIndices = wurfl_pbjs.global.basic_set.cap_indices; + const pubIndices = wurfl_pbjs.global.publisher.cap_indices; + const allBasicPubIndices = [...new Set([...basicIndices, ...pubIndices])]; + const expectedBasicPubCaps = {}; + allBasicPubIndices.forEach(index => { + const capName = wurfl_pbjs.caps[index]; + if (capName && capName in WURFL) { + expectedBasicPubCaps[capName] = WURFL[capName]; + } + }); + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext.wurfl).to.deep.equal(expectedBasicPubCaps); + + // Under quota, authorized bidders: should get only bidder-specific caps (delta) + const bidder1Indices = wurfl_pbjs.bidders.bidder1.cap_indices; + const expectedBidder1Caps = {}; + bidder1Indices.forEach(index => { + const capName = wurfl_pbjs.caps[index]; + if (capName && capName in WURFL) { + expectedBidder1Caps[capName] = WURFL[capName]; + } + }); + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder1.device.ext.wurfl).to.deep.equal(expectedBidder1Caps); + + const bidder2Indices = wurfl_pbjs.bidders.bidder2.cap_indices; + const expectedBidder2Caps = {}; + bidder2Indices.forEach(index => { + const capName = wurfl_pbjs.caps[index]; + if (capName && capName in WURFL) { + expectedBidder2Caps[capName] = WURFL[capName]; + } + }); + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder2.device.ext.wurfl).to.deep.equal(expectedBidder2Caps); + + // bidder3 is NOT authorized, should get empty object (inherits from global) + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder3).to.not.exist; + + done(); + }; + + const config = { params: {} }; + const userConsent = {}; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, userConsent); }); - it('handles special cases', function () { - expect(toNumber(null)).to.be.undefined; - expect(toNumber('')).to.be.undefined; + it('should use LCE data when cache is empty and load WURFL.js async', (done) => { + // Reset reqBidsConfigObj to clean state + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + // Setup empty cache + sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + // Set global debug flag + config.setConfig({ debug: true }); + + const expectedURL = new URL(altHost); + expectedURL.searchParams.set('debug', 'true'); + expectedURL.searchParams.set('mode', 'prebid2'); + expectedURL.searchParams.set('bidders', 'bidder1,bidder2,bidder3'); + + const callback = () => { + // Verify global FPD has LCE device data + expect(reqBidsConfigObj.ortb2Fragments.global.device).to.exist; + expect(reqBidsConfigObj.ortb2Fragments.global.device.js).to.equal(1); + + // Verify ext.wurfl.is_robot is set + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext).to.exist; + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext.wurfl).to.exist; + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext.wurfl.is_robot).to.be.false; + + // No bidder enrichment should occur without cached WURFL data + expect(reqBidsConfigObj.ortb2Fragments.bidder).to.deep.equal({}); + + done(); + }; + + const moduleConfig = { + params: { + altHost: altHost, + } + }; + const userConsent = {}; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, moduleConfig, userConsent); + + // Verify WURFL.js is loaded async for future requests + expect(loadExternalScriptStub.calledOnce).to.be.true; + const loadExternalScriptCall = loadExternalScriptStub.getCall(0); + expect(loadExternalScriptCall.args[0]).to.equal(expectedURL.toString()); + expect(loadExternalScriptCall.args[2]).to.equal('wurfl'); }); - it('returns undefined for non-numeric values', function () { - expect(toNumber('abc')).to.be.undefined; - expect(toNumber(undefined)).to.be.undefined; - expect(toNumber(NaN)).to.be.undefined; - expect(toNumber({})).to.be.undefined; - expect(toNumber([1, 2, 3])).to.be.undefined; - // WURFL.js cannot return [] so it is safe to not handle and return undefined - expect(toNumber([])).to.equal(0); + it('should not include device.w and device.h in LCE enrichment (removed in v2.3.0 - fingerprinting APIs)', (done) => { + // Reset reqBidsConfigObj to clean state + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + // Setup empty cache to trigger LCE + sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + // Mock a typical desktop Chrome user agent to get consistent device detection + const originalUserAgent = navigator.userAgent; + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', + configurable: true, + writable: true + }); + + const callback = () => { + const device = reqBidsConfigObj.ortb2Fragments.global.device; + + // Verify device object exists + expect(device).to.exist; + + // CRITICAL: Verify device.w and device.h are NOT present + // These were removed in v2.3.0 due to fingerprinting API concerns (screen.availWidth, screen.width/height) + expect(device).to.not.have.property('w'); + expect(device).to.not.have.property('h'); + + // Verify other ORTB2_DEVICE_FIELDS properties ARE populated when available + // From ORTB2_DEVICE_FIELDS: ['make', 'model', 'devicetype', 'os', 'osv', 'hwv', 'h', 'w', 'ppi', 'pxratio', 'js'] + expect(device.js).to.equal(1); // Always present + + // These should be present based on UA detection + expect(device.make).to.be.a('string').and.not.be.empty; + expect(device.devicetype).to.be.a('number'); // ORTB2_DEVICE_TYPE.PERSONAL_COMPUTER (2) + expect(device.os).to.be.a('string').and.not.be.empty; + + // osv, model, hwv may be present depending on UA + if (device.osv !== undefined) { + expect(device.osv).to.be.a('string'); + } + if (device.model !== undefined) { + expect(device.model).to.be.a('string'); + } + if (device.hwv !== undefined) { + expect(device.hwv).to.be.a('string'); + } + + // pxratio uses OS-based hardcoded values (v2.4.0+), not window.devicePixelRatio (fingerprinting API) + if (device.pxratio !== undefined) { + expect(device.pxratio).to.be.a('number'); + } + + // ppi is not typically populated by LCE (would come from WURFL server-side data) + // Just verify it doesn't exist or is undefined in LCE mode + expect(device.ppi).to.be.undefined; + + // Verify ext.wurfl.is_robot is set + expect(device.ext).to.exist; + expect(device.ext.wurfl).to.exist; + expect(device.ext.wurfl.is_robot).to.be.a('boolean'); + + // Restore original userAgent + Object.defineProperty(navigator, 'userAgent', { + value: originalUserAgent, + configurable: true, + writable: true + }); + + done(); + }; + + const moduleConfig = { params: {} }; + const userConsent = {}; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, moduleConfig, userConsent); + }); + + describe('LCE bot detection', () => { + let originalUserAgent; + + beforeEach(() => { + // Setup empty cache to trigger LCE + sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + // Save original userAgent + originalUserAgent = navigator.userAgent; + }); + + afterEach(() => { + // Restore original userAgent + Object.defineProperty(navigator, 'userAgent', { + value: originalUserAgent, + configurable: true, + writable: true + }); + }); + + it('should detect Googlebot and set is_robot to true', (done) => { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', + configurable: true, + writable: true + }); + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext).to.exist; + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext.wurfl).to.exist; + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext.wurfl.is_robot).to.be.true; + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should detect BingPreview and set is_robot to true', (done) => { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534+ (KHTML, like Gecko) BingPreview/1.0b', + configurable: true, + writable: true + }); + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext.wurfl.is_robot).to.be.true; + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should detect Yahoo! Slurp and set is_robot to true', (done) => { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)', + configurable: true, + writable: true + }); + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext.wurfl.is_robot).to.be.true; + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should detect +http bot token and set is_robot to true', (done) => { + Object.defineProperty(navigator, 'userAgent', { + value: 'SomeBot/1.0 (+http://example.com/bot)', + configurable: true, + writable: true + }); + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext.wurfl.is_robot).to.be.true; + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should set is_robot to false for regular Chrome user agent', (done) => { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', + configurable: true, + writable: true + }); + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext.wurfl.is_robot).to.be.false; + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should set is_robot to false for regular mobile Safari user agent', (done) => { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', + configurable: true, + writable: true + }); + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext.wurfl.is_robot).to.be.false; + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + }); + + describe('LCE pxratio (OS-based device pixel ratio)', () => { + let originalUserAgent; + + beforeEach(() => { + // Setup empty cache to trigger LCE + sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + // Save original userAgent + originalUserAgent = navigator.userAgent; + }); + + afterEach(() => { + // Restore original userAgent + Object.defineProperty(navigator, 'userAgent', { + value: originalUserAgent, + configurable: true, + writable: true + }); + }); + + it('should set pxratio to 2.0 for Android devices', (done) => { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36', + configurable: true, + writable: true + }); + + const callback = () => { + const device = reqBidsConfigObj.ortb2Fragments.global.device; + expect(device.pxratio).to.equal(2.0); + expect(device.os).to.equal('Android'); + expect(device.devicetype).to.equal(4); // PHONE + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should set pxratio to 3.0 for iOS (iPhone) devices', (done) => { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', + configurable: true, + writable: true + }); + + const callback = () => { + const device = reqBidsConfigObj.ortb2Fragments.global.device; + expect(device.pxratio).to.equal(3.0); + expect(device.os).to.equal('iOS'); + expect(device.devicetype).to.equal(4); // PHONE + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should set pxratio to 2.0 for iPadOS (iPad) devices', (done) => { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1', + configurable: true, + writable: true + }); + + const callback = () => { + const device = reqBidsConfigObj.ortb2Fragments.global.device; + expect(device.pxratio).to.equal(2.0); + expect(device.os).to.equal('iPadOS'); + expect(device.devicetype).to.equal(5); // TABLET + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should set pxratio to 1.0 for desktop/other devices (default)', (done) => { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', + configurable: true, + writable: true + }); + + const callback = () => { + const device = reqBidsConfigObj.ortb2Fragments.global.device; + expect(device.pxratio).to.equal(1.0); + expect(device.os).to.equal('Windows'); + expect(device.devicetype).to.equal(2); // PERSONAL_COMPUTER + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should set pxratio to 1.0 for macOS devices (default)', (done) => { + Object.defineProperty(navigator, 'userAgent', { + value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', + configurable: true, + writable: true + }); + + const callback = () => { + const device = reqBidsConfigObj.ortb2Fragments.global.device; + expect(device.pxratio).to.equal(1.0); + expect(device.os).to.equal('macOS'); + expect(device.devicetype).to.equal(2); // PERSONAL_COMPUTER + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + }); + + it('should enrich only bidders when over quota', (done) => { + // Reset reqBidsConfigObj to clean state + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + // Setup localStorage with cached WURFL data (over quota) + const wurfl_pbjs_over_quota = { + ...wurfl_pbjs, + over_quota: 1 + }; + const cachedData = { WURFL, wurfl_pbjs: wurfl_pbjs_over_quota }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const callback = () => { + // Verify global FPD does NOT have device data (over quota) + expect(reqBidsConfigObj.ortb2Fragments.global.device).to.deep.equal({}); + + // Over quota, authorized bidders: should get basic + bidder-specific (NO pub) + // bidder1 should get device fields + ext.wurfl with basic + bidder1-specific + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder1.device).to.deep.include({ + make: 'Google', + model: 'Nexus 5', + devicetype: 4, + os: 'Android', + osv: '6.0', + hwv: 'Nexus 5', + h: 1920, + w: 1080, + ppi: 443, + pxratio: 3.0, + js: 1 + }); + const basicIndices = wurfl_pbjs_over_quota.global.basic_set.cap_indices; + const bidder1Indices = wurfl_pbjs_over_quota.bidders.bidder1.cap_indices; + const allBidder1Indices = [...new Set([...basicIndices, ...bidder1Indices])]; + const expectedBidder1AllCaps = {}; + allBidder1Indices.forEach(index => { + const capName = wurfl_pbjs_over_quota.caps[index]; + if (capName && capName in WURFL) { + expectedBidder1AllCaps[capName] = WURFL[capName]; + } + }); + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder1.device.ext.wurfl).to.deep.equal(expectedBidder1AllCaps); + + // bidder2 should get device fields + ext.wurfl with basic + bidder2-specific + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder2.device).to.deep.include({ + make: 'Google', + model: 'Nexus 5', + devicetype: 4, + os: 'Android', + osv: '6.0', + hwv: 'Nexus 5', + h: 1920, + w: 1080, + ppi: 443, + pxratio: 3.0, + js: 1 + }); + const bidder2Indices = wurfl_pbjs_over_quota.bidders.bidder2.cap_indices; + const allBidder2Indices = [...new Set([...basicIndices, ...bidder2Indices])]; + const expectedBidder2AllCaps = {}; + allBidder2Indices.forEach(index => { + const capName = wurfl_pbjs_over_quota.caps[index]; + if (capName && capName in WURFL) { + expectedBidder2AllCaps[capName] = WURFL[capName]; + } + }); + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder2.device.ext.wurfl).to.deep.equal(expectedBidder2AllCaps); + + // bidder3 is NOT authorized, should get nothing + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder3).to.be.undefined; + + done(); + }; + + const config = { params: {} }; + const userConsent = {}; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, userConsent); + }); + + it('should initialize ortb2Fragments.bidder when undefined and enrich authorized bidders (over quota)', (done) => { + // Test the fix for ortb2Fragments.bidder being undefined + reqBidsConfigObj.ortb2Fragments.global.device = {}; + // Explicitly set bidder to undefined to simulate the race condition + reqBidsConfigObj.ortb2Fragments.bidder = undefined; + + // Setup localStorage with cached WURFL data (over quota) + const wurfl_pbjs_over_quota = { + ...wurfl_pbjs, + over_quota: 1 + }; + const cachedData = { WURFL, wurfl_pbjs: wurfl_pbjs_over_quota }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const callback = () => { + // Verify ortb2Fragments.bidder was properly initialized + expect(reqBidsConfigObj.ortb2Fragments.bidder).to.exist; + expect(reqBidsConfigObj.ortb2Fragments.bidder).to.be.an('object'); + + // Verify global FPD does NOT have device data (over quota) + expect(reqBidsConfigObj.ortb2Fragments.global.device).to.deep.equal({}); + + // Over quota, authorized bidders: should get basic + pub + bidder-specific caps (ALL) + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder1).to.exist; + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder1.device).to.deep.include({ + make: 'Google', + model: 'Nexus 5', + devicetype: 4, + os: 'Android', + osv: '6.0', + hwv: 'Nexus 5', + h: 1920, + w: 1080, + ppi: 443, + pxratio: 3.0, + js: 1 + }); + const basicIndices = wurfl_pbjs_over_quota.global.basic_set.cap_indices; + const bidder1Indices = wurfl_pbjs_over_quota.bidders.bidder1.cap_indices; + const allBidder1Indices = [...new Set([...basicIndices, ...bidder1Indices])]; + const expectedBidder1AllCaps = {}; + allBidder1Indices.forEach(index => { + const capName = wurfl_pbjs_over_quota.caps[index]; + if (capName && capName in WURFL) { + expectedBidder1AllCaps[capName] = WURFL[capName]; + } + }); + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder1.device.ext.wurfl).to.deep.equal(expectedBidder1AllCaps); + + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder2).to.exist; + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder2.device).to.deep.include({ + make: 'Google', + model: 'Nexus 5', + devicetype: 4 + }); + const bidder2Indices = wurfl_pbjs_over_quota.bidders.bidder2.cap_indices; + const allBidder2Indices = [...new Set([...basicIndices, ...bidder2Indices])]; + const expectedBidder2AllCaps = {}; + allBidder2Indices.forEach(index => { + const capName = wurfl_pbjs_over_quota.caps[index]; + if (capName && capName in WURFL) { + expectedBidder2AllCaps[capName] = WURFL[capName]; + } + }); + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder2.device.ext.wurfl).to.deep.equal(expectedBidder2AllCaps); + + // bidder3 is NOT authorized, should get nothing + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder3).to.be.undefined; + + done(); + }; + + const config = { params: {} }; + const userConsent = {}; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, userConsent); + }); + + it('should work with guardOrtb2Fragments Proxy (Prebid 10.x compatibility)', (done) => { + // Simulate Prebid 10.x where rtdModule wraps ortb2Fragments with guardOrtb2Fragments + const plainFragments = { + global: { device: {} }, + bidder: {} + }; + + const plainReqBidsConfigObj = { + adUnits: [{ + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' } + ] + }], + ortb2Fragments: plainFragments + }; + + // Setup localStorage with cached WURFL data (over quota) + const wurfl_pbjs_over_quota = { + ...wurfl_pbjs, + over_quota: 1 + }; + const cachedData = { WURFL, wurfl_pbjs: wurfl_pbjs_over_quota }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + // Wrap with guard (like rtdModule does in production) + const guardedFragments = guardOrtb2Fragments(plainFragments, {}); + const guardedReqBidsConfigObj = { ...plainReqBidsConfigObj, ortb2Fragments: guardedFragments }; + + const callback = () => { + // Over quota, authorized bidders: should get basic + pub + bidder-specific caps (ALL) + expect(plainFragments.bidder.bidder1).to.exist; + expect(plainFragments.bidder.bidder1.device).to.exist; + expect(plainFragments.bidder.bidder1.device.ext).to.exist; + + const basicIndices = wurfl_pbjs_over_quota.global.basic_set.cap_indices; + const bidder1Indices = wurfl_pbjs_over_quota.bidders.bidder1.cap_indices; + const allBidder1Indices = [...new Set([...basicIndices, ...bidder1Indices])]; + const expectedBidder1AllCaps = {}; + allBidder1Indices.forEach(index => { + const capName = wurfl_pbjs_over_quota.caps[index]; + if (capName && capName in WURFL) { + expectedBidder1AllCaps[capName] = WURFL[capName]; + } + }); + expect(plainFragments.bidder.bidder1.device.ext.wurfl).to.deep.equal(expectedBidder1AllCaps); + + // Verify FPD is present + expect(plainFragments.bidder.bidder1.device).to.deep.include({ + make: 'Google', + model: 'Nexus 5', + devicetype: 4, + os: 'Android', + osv: '6.0' + }); + + // Verify bidder2 (authorized) also got enriched + expect(plainFragments.bidder.bidder2).to.exist; + const bidder2Indices = wurfl_pbjs_over_quota.bidders.bidder2.cap_indices; + const allBidder2Indices = [...new Set([...basicIndices, ...bidder2Indices])]; + const expectedBidder2AllCaps = {}; + allBidder2Indices.forEach(index => { + const capName = wurfl_pbjs_over_quota.caps[index]; + if (capName && capName in WURFL) { + expectedBidder2AllCaps[capName] = WURFL[capName]; + } + }); + expect(plainFragments.bidder.bidder2.device.ext.wurfl).to.deep.equal(expectedBidder2AllCaps); + + done(); + }; + + const config = { params: {} }; + const userConsent = {}; + + wurflSubmodule.getBidRequestData(guardedReqBidsConfigObj, callback, config, userConsent); + }); + + it('should pass basic+pub caps via global and authorized bidders get full caps when under quota', (done) => { + // Reset reqBidsConfigObj to clean state + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + // Setup localStorage with cached WURFL data (NOT over quota) + const cachedData = { WURFL, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const callback = () => { + // Verify global FPD has device data (not over quota) + expect(reqBidsConfigObj.ortb2Fragments.global.device).to.deep.include({ + make: 'Google', + model: 'Nexus 5', + devicetype: 4 + }); + + // Calculate expected caps for basic + pub (no bidder-specific) + const basicIndices = wurfl_pbjs.global.basic_set.cap_indices; + const pubIndices = wurfl_pbjs.global.publisher.cap_indices; + const allBasicPubIndices = [...new Set([...basicIndices, ...pubIndices])]; + + const expectedBasicPubCaps = {}; + allBasicPubIndices.forEach(index => { + const capName = wurfl_pbjs.caps[index]; + if (capName && capName in WURFL) { + expectedBasicPubCaps[capName] = WURFL[capName]; + } + }); + + // Verify global has ext.wurfl with basic+pub caps (new behavior) + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext).to.exist; + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext.wurfl).to.deep.equal(expectedBasicPubCaps); + + // Under quota, authorized bidders: should get only bidder-specific caps (delta) + const bidder1Indices = wurfl_pbjs.bidders.bidder1.cap_indices; + const expectedBidder1Caps = {}; + bidder1Indices.forEach(index => { + const capName = wurfl_pbjs.caps[index]; + if (capName && capName in WURFL) { + expectedBidder1Caps[capName] = WURFL[capName]; + } + }); + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder1.device.ext.wurfl).to.deep.equal(expectedBidder1Caps); + + const bidder2Indices = wurfl_pbjs.bidders.bidder2.cap_indices; + const expectedBidder2Caps = {}; + bidder2Indices.forEach(index => { + const capName = wurfl_pbjs.caps[index]; + if (capName && capName in WURFL) { + expectedBidder2Caps[capName] = WURFL[capName]; + } + }); + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder2.device.ext.wurfl).to.deep.equal(expectedBidder2Caps); + + // bidder3 is NOT authorized, should get NOTHING (inherits from global.device.ext.wurfl) + expect(reqBidsConfigObj.ortb2Fragments.bidder.bidder3).to.not.exist; + + // Verify the caps calculation: basic+pub union in global + const globalCapCount = Object.keys(reqBidsConfigObj.ortb2Fragments.global.device.ext.wurfl).length; + expect(globalCapCount).to.equal(allBasicPubIndices.length); + + done(); + }; + + const config = { params: {} }; + const userConsent = {}; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, userConsent); + }); + + it('should enrich global.device.ext.wurfl when under quota (verifies GlobalExt)', (done) => { + // This test verifies that GlobalExt() is called and global enrichment works + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const cachedData = { WURFL, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const callback = () => { + // Calculate expected basic+pub caps + const basicIndices = wurfl_pbjs.global.basic_set.cap_indices; + const pubIndices = wurfl_pbjs.global.publisher.cap_indices; + const allBasicPubIndices = [...new Set([...basicIndices, ...pubIndices])]; + const expectedBasicPubCaps = {}; + allBasicPubIndices.forEach(index => { + const capName = wurfl_pbjs.caps[index]; + if (capName && capName in WURFL) { + expectedBasicPubCaps[capName] = WURFL[capName]; + } + }); + + // Verify GlobalExt() populated global.device.ext.wurfl with basic+pub + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext).to.exist; + expect(reqBidsConfigObj.ortb2Fragments.global.device.ext.wurfl).to.deep.equal(expectedBasicPubCaps); + + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('onAuctionEndEvent: should send analytics data using navigator.sendBeacon, if available', (done) => { + // Reset reqBidsConfigObj to clean state + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + // Setup localStorage with cached WURFL data to populate enrichedBidders + const cachedData = { WURFL, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const sendBeaconStub = sandbox.stub(navigator, 'sendBeacon').returns(true); + + // Mock getGlobal().getHighestCpmBids() + const mockHighestCpmBids = [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1' } + ]; + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => mockHighestCpmBids + }); + + const callback = () => { + // Build auctionDetails with bidsReceived and adUnits + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' }, + { requestId: 'req2', bidderCode: 'bidder2', adUnitCode: 'ad1', cpm: 1.2, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' } + ] + } + ] + }; + const config = { params: {} }; + const userConsent = {}; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, userConsent); + + // Assertions + expect(sendBeaconStub.calledOnce).to.be.true; + const beaconCall = sendBeaconStub.getCall(0); + expect(beaconCall.args[0]).to.equal(expectedStatsURL); + + // Parse and verify payload structure + const payload = JSON.parse(beaconCall.args[1]); + expect(payload).to.have.property('version'); + expect(payload).to.have.property('domain'); + expect(payload).to.have.property('path'); + expect(payload).to.have.property('sampling_rate', 100); + expect(payload).to.have.property('enrichment', 'wurfl_pub'); + expect(payload).to.have.property('wurfl_id', 'lg_nexus5_ver1'); + expect(payload).to.have.property('over_quota', 0); + expect(payload).to.have.property('consent_class', 0); + expect(payload).to.have.property('ad_units'); + expect(payload.ad_units).to.be.an('array').with.lengthOf(1); + expect(payload.ad_units[0].ad_unit_code).to.equal('ad1'); + expect(payload.ad_units[0].bidders).to.be.an('array').with.lengthOf(2); + expect(payload.ad_units[0].bidders[0]).to.deep.include({ + bidder: 'bidder1', + bdr_enrich: 'wurfl_ssp', + cpm: 1.5, + currency: 'USD', + won: true + }); + expect(payload.ad_units[0].bidders[1]).to.deep.include({ + bidder: 'bidder2', + bdr_enrich: 'wurfl_ssp', + cpm: 1.2, + currency: 'USD', + won: false + }); + + done(); + }; + + const config = { params: {} }; + const userConsent = {}; + + // First enrich bidders to populate enrichedBidders Set + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, userConsent); + }); + + it('onAuctionEndEvent: should send analytics data using fetch as fallback, if navigator.sendBeacon is not available', (done) => { + // Reset reqBidsConfigObj to clean state + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + // Setup localStorage with cached WURFL data to populate enrichedBidders + const cachedData = { WURFL, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon').returns(false); + const fetchAjaxStub = sandbox.stub(ajaxModule, 'fetch').returns(Promise.resolve()); + + // Mock getGlobal().getHighestCpmBids() + const mockHighestCpmBids = [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1' } + ]; + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => mockHighestCpmBids + }); + + const callback = () => { + // Build auctionDetails with bidsReceived and adUnits + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' }, + { requestId: 'req2', bidderCode: 'bidder2', adUnitCode: 'ad1', cpm: 1.2, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' } + ] + } + ] + }; + const config = { params: {} }; + const userConsent = {}; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, userConsent); + + // Assertions + expect(sendBeaconStub.calledOnce).to.be.true; + + expect(fetchAjaxStub.calledOnce).to.be.true; + const fetchAjaxCall = fetchAjaxStub.getCall(0); + expect(fetchAjaxCall.args[0]).to.equal(expectedStatsURL); + expect(fetchAjaxCall.args[1].method).to.equal('POST'); + expect(fetchAjaxCall.args[1].mode).to.equal('no-cors'); + + // Parse and verify payload structure + const payload = JSON.parse(fetchAjaxCall.args[1].body); + expect(payload).to.have.property('domain'); + expect(payload).to.have.property('path'); + expect(payload).to.have.property('sampling_rate', 100); + expect(payload).to.have.property('enrichment', 'wurfl_pub'); + expect(payload).to.have.property('wurfl_id', 'lg_nexus5_ver1'); + expect(payload).to.have.property('over_quota', 0); + expect(payload).to.have.property('consent_class', 0); + expect(payload).to.have.property('ad_units'); + expect(payload.ad_units).to.be.an('array').with.lengthOf(1); + + done(); + }; + + const config = { params: {} }; + const userConsent = {}; + + // First enrich bidders to populate enrichedBidders Set + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, userConsent); + }); + + describe('consent classification', () => { + beforeEach(function () { + // Setup localStorage with cached WURFL data + const cachedData = { WURFL, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + // Mock getGlobal().getHighestCpmBids() + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => [] + }); + + // Reset reqBidsConfigObj + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + }); + + const testConsentClass = (description, userConsent, expectedClass, done) => { + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon').returns(true); + + const callback = () => { + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [{ bidder: 'bidder1' }] + } + ] + }; + const config = { params: {} }; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, userConsent); + + expect(sendBeaconStub.calledOnce).to.be.true; + const beaconCall = sendBeaconStub.getCall(0); + const payload = JSON.parse(beaconCall.args[1]); + expect(payload).to.have.property('consent_class', expectedClass); + done(); + }; + + const config = { params: {} }; + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, {}); + }; + + it('should return NO consent (0) when userConsent is null', (done) => { + testConsentClass('null userConsent', null, 0, done); + }); + + it('should return NO consent (0) when userConsent is empty object', (done) => { + testConsentClass('empty object', {}, 0, done); + }); + + it('should return NO consent (0) when COPPA is enabled', (done) => { + testConsentClass('COPPA enabled', { coppa: true }, 0, done); + }); + + it('should return NO consent (0) when USP opt-out (1Y)', (done) => { + testConsentClass('USP opt-out', { usp: '1YYN' }, 0, done); + }); + + it('should return NO consent (0) when GDPR applies but no purposes granted', (done) => { + const userConsent = { + gdpr: { + gdprApplies: true, + vendorData: { + purpose: { + consents: {}, + legitimateInterests: {} + } + } + } + }; + testConsentClass('GDPR no purposes', userConsent, 0, done); + }); + + it('should return FULL consent (2) when no GDPR object (non-GDPR region)', (done) => { + testConsentClass('no GDPR object', { usp: '1NNN' }, 2, done); + }); + + it('should return FULL consent (2) when GDPR does not apply', (done) => { + const userConsent = { + gdpr: { + gdprApplies: false + } + }; + testConsentClass('GDPR not applicable', userConsent, 2, done); + }); + + it('should return FULL consent (2) when all 3 GDPR purposes granted via consents', (done) => { + const userConsent = { + gdpr: { + gdprApplies: true, + vendorData: { + purpose: { + consents: { 7: true, 8: true, 10: true } + } + } + } + }; + testConsentClass('all purposes via consents', userConsent, 2, done); + }); + + it('should return FULL consent (2) when all 3 GDPR purposes granted via legitimateInterests', (done) => { + const userConsent = { + gdpr: { + gdprApplies: true, + vendorData: { + purpose: { + legitimateInterests: { 7: true, 8: true, 10: true } + } + } + } + }; + testConsentClass('all purposes via LI', userConsent, 2, done); + }); + + it('should return FULL consent (2) when all 3 GDPR purposes granted via mixed consents and LI', (done) => { + const userConsent = { + gdpr: { + gdprApplies: true, + vendorData: { + purpose: { + consents: { 7: true, 10: true }, + legitimateInterests: { 8: true } + } + } + } + }; + testConsentClass('mixed consents and LI', userConsent, 2, done); + }); + + it('should return PARTIAL consent (1) when only 1 GDPR purpose granted', (done) => { + const userConsent = { + gdpr: { + gdprApplies: true, + vendorData: { + purpose: { + consents: { 7: true } + } + } + } + }; + testConsentClass('1 purpose granted', userConsent, 1, done); + }); + + it('should return PARTIAL consent (1) when 2 GDPR purposes granted', (done) => { + const userConsent = { + gdpr: { + gdprApplies: true, + vendorData: { + purpose: { + consents: { 7: true }, + legitimateInterests: { 8: true } + } + } + } + }; + testConsentClass('2 purposes granted', userConsent, 1, done); + }); + }); + + describe('sampling rate', () => { + it('should not send beacon when sampling_rate is 0', (done) => { + // Setup WURFL data with sampling_rate: 0 + const wurfl_pbjs_zero_sampling = { ...wurfl_pbjs, sampling_rate: 0 }; + const cachedData = { WURFL, wurfl_pbjs: wurfl_pbjs_zero_sampling }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon'); + const fetchStub = sandbox.stub(ajaxModule, 'fetch').returns(Promise.resolve()); + + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => [] + }); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [{ bidder: 'bidder1' }] + } + ] + }; + const config = { params: {} }; + const userConsent = null; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, userConsent); + + // Beacon should NOT be sent due to sampling_rate: 0 + expect(sendBeaconStub.called).to.be.false; + expect(fetchStub.called).to.be.false; + done(); + }; + + const config = { params: {} }; + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, {}); + }); + + it('should send beacon when sampling_rate is 100', (done) => { + // Setup WURFL data with sampling_rate: 100 + const wurfl_pbjs_full_sampling = { ...wurfl_pbjs, sampling_rate: 100 }; + const cachedData = { WURFL, wurfl_pbjs: wurfl_pbjs_full_sampling }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon').returns(true); + + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => [] + }); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [{ bidder: 'bidder1' }] + } + ] + }; + const config = { params: {} }; + const userConsent = null; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, userConsent); + + // Beacon should be sent + expect(sendBeaconStub.calledOnce).to.be.true; + const beaconCall = sendBeaconStub.getCall(0); + const payload = JSON.parse(beaconCall.args[1]); + expect(payload).to.have.property('sampling_rate', 100); + done(); + }; + + const config = { params: {} }; + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, {}); + }); + + it('should use default sampling_rate (100) for LCE and send beacon', (done) => { + // No cached data - will use LCE + sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon').returns(true); + + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => [] + }); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [{ bidder: 'bidder1' }] + } + ] + }; + const config = { params: {} }; + const userConsent = null; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, userConsent); + + // Beacon should be sent with default sampling_rate + expect(sendBeaconStub.calledOnce).to.be.true; + const beaconCall = sendBeaconStub.getCall(0); + const payload = JSON.parse(beaconCall.args[1]); + expect(payload).to.have.property('sampling_rate', 100); + // Enrichment type can be 'lce' or 'lcefailed' depending on what data is available + expect(payload.enrichment).to.be.oneOf(['lce', 'lcefailed']); + done(); + }; + + const config = { params: {} }; + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, {}); + }); + }); + + describe('onAuctionEndEvent: overquota beacon enrichment', () => { + beforeEach(() => { + // Mock getGlobal().getHighestCpmBids() + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => [] + }); + + // Reset reqBidsConfigObj + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + }); + + it('should report wurfl_ssp for authorized bidders and none for unauthorized when overquota', (done) => { + // Setup overquota scenario + const wurfl_pbjs_over_quota = { + ...wurfl_pbjs, + over_quota: 1 + }; + const cachedData = { WURFL, wurfl_pbjs: wurfl_pbjs_over_quota }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon').returns(true); + + const callback = () => { + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' }, + { requestId: 'req2', bidderCode: 'bidder2', adUnitCode: 'ad1', cpm: 1.2, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [ + { bidder: 'bidder1' }, // authorized + { bidder: 'bidder2' }, // authorized + { bidder: 'bidder3' } // NOT authorized + ] + } + ] + }; + const config = { params: {} }; + const userConsent = {}; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, userConsent); + + expect(sendBeaconStub.calledOnce).to.be.true; + const beaconCall = sendBeaconStub.getCall(0); + const payload = JSON.parse(beaconCall.args[1]); + + // Verify overall enrichment is none when overquota (publisher not enriched) + expect(payload).to.have.property('enrichment', 'none'); + expect(payload).to.have.property('over_quota', 1); + + // Verify per-bidder enrichment + expect(payload.ad_units).to.be.an('array').with.lengthOf(1); + expect(payload.ad_units[0].bidders).to.be.an('array').with.lengthOf(3); + + // bidder1 and bidder2 are authorized - should report wurfl_ssp + expect(payload.ad_units[0].bidders[0]).to.deep.include({ + bidder: 'bidder1', + bdr_enrich: 'wurfl_ssp', + cpm: 1.5, + currency: 'USD', + won: false + }); + expect(payload.ad_units[0].bidders[1]).to.deep.include({ + bidder: 'bidder2', + bdr_enrich: 'wurfl_ssp', + cpm: 1.2, + currency: 'USD', + won: false + }); + + // bidder3 is NOT authorized and overquota - should report none + expect(payload.ad_units[0].bidders[2]).to.deep.include({ + bidder: 'bidder3', + bdr_enrich: 'none', + won: false + }); + + done(); + }; + + const config = { params: {} }; + const userConsent = {}; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, userConsent); + }); + + it('should report wurfl_ssp for authorized and wurfl_pub for unauthorized when not overquota', (done) => { + // Setup NOT overquota scenario + const cachedData = { WURFL, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon').returns(true); + + const callback = () => { + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' }, + { requestId: 'req3', bidderCode: 'bidder3', adUnitCode: 'ad1', cpm: 1.0, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [ + { bidder: 'bidder1' }, // authorized + { bidder: 'bidder3' } // NOT authorized + ] + } + ] + }; + const config = { params: {} }; + const userConsent = {}; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, config, userConsent); + + expect(sendBeaconStub.calledOnce).to.be.true; + const beaconCall = sendBeaconStub.getCall(0); + const payload = JSON.parse(beaconCall.args[1]); + + // Verify overall enrichment is wurfl_pub when not overquota + expect(payload).to.have.property('enrichment', 'wurfl_pub'); + expect(payload).to.have.property('over_quota', 0); + + // Verify per-bidder enrichment + expect(payload.ad_units).to.be.an('array').with.lengthOf(1); + expect(payload.ad_units[0].bidders).to.be.an('array').with.lengthOf(2); + + // bidder1 is authorized - should always report wurfl_ssp + expect(payload.ad_units[0].bidders[0]).to.deep.include({ + bidder: 'bidder1', + bdr_enrich: 'wurfl_ssp', + cpm: 1.5, + currency: 'USD', + won: false + }); + + // bidder3 is NOT authorized but not overquota - should report wurfl_pub + expect(payload.ad_units[0].bidders[1]).to.deep.include({ + bidder: 'bidder3', + bdr_enrich: 'wurfl_pub', + cpm: 1.0, + currency: 'USD', + won: false + }); + + done(); + }; + + const config = { params: {} }; + const userConsent = {}; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, config, userConsent); + }); + }); + + describe('device type mapping', () => { + it('should map is_ott priority over form_factor', (done) => { + const wurflWithOtt = { ...WURFL, is_ott: true, form_factor: 'Desktop' }; + const cachedData = { WURFL: wurflWithOtt, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.devicetype).to.equal(7); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should map is_console priority over form_factor', (done) => { + const wurflWithConsole = { ...WURFL, is_console: true, form_factor: 'Desktop' }; + const cachedData = { WURFL: wurflWithConsole, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.devicetype).to.equal(6); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should map physical_form_factor out_of_home_device', (done) => { + const wurflWithOOH = { ...WURFL, physical_form_factor: 'out_of_home_device', form_factor: 'Desktop' }; + const cachedData = { WURFL: wurflWithOOH, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.devicetype).to.equal(8); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should map form_factor Desktop to PERSONAL_COMPUTER', (done) => { + const wurflDesktop = { ...WURFL, form_factor: 'Desktop' }; + const cachedData = { WURFL: wurflDesktop, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.devicetype).to.equal(2); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should map form_factor Smartphone to PHONE', (done) => { + const wurflSmartphone = { ...WURFL, form_factor: 'Smartphone' }; + const cachedData = { WURFL: wurflSmartphone, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.devicetype).to.equal(4); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should map form_factor Tablet to TABLET', (done) => { + const wurflTablet = { ...WURFL, form_factor: 'Tablet' }; + const cachedData = { WURFL: wurflTablet, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.devicetype).to.equal(5); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should map form_factor Smart-TV to CONNECTED_TV', (done) => { + const wurflSmartTV = { ...WURFL, form_factor: 'Smart-TV' }; + const cachedData = { WURFL: wurflSmartTV, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.devicetype).to.equal(3); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should map form_factor Other Non-Mobile to CONNECTED_DEVICE', (done) => { + const wurflOtherNonMobile = { ...WURFL, form_factor: 'Other Non-Mobile' }; + const cachedData = { WURFL: wurflOtherNonMobile, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.devicetype).to.equal(6); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should map form_factor Other Mobile to MOBILE_OR_TABLET', (done) => { + const wurflOtherMobile = { ...WURFL, form_factor: 'Other Mobile' }; + const cachedData = { WURFL: wurflOtherMobile, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.devicetype).to.equal(1); + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should return undefined when form_factor is missing', (done) => { + const wurflNoFormFactor = { ...WURFL }; + delete wurflNoFormFactor.form_factor; + const cachedData = { WURFL: wurflNoFormFactor, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.devicetype).to.be.undefined; + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + + it('should return undefined for unknown form_factor', (done) => { + const wurflUnknownFormFactor = { ...WURFL, form_factor: 'UnknownDevice' }; + const cachedData = { WURFL: wurflUnknownFormFactor, wurfl_pbjs }; + sandbox.stub(storage, 'getDataFromLocalStorage').returns(JSON.stringify(cachedData)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + + const callback = () => { + expect(reqBidsConfigObj.ortb2Fragments.global.device.devicetype).to.be.undefined; + done(); + }; + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + }); + }); + + describe('LCE Error Handling', function () { + beforeEach(function () { + // Setup empty cache to force LCE + sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'hasLocalStorage').returns(true); + + reqBidsConfigObj.ortb2Fragments.global.device = {}; + reqBidsConfigObj.ortb2Fragments.bidder = {}; + }); + + it('should set LCE_ERROR enrichment type when LCE device detection throws error', (done) => { + const sendBeaconStub = sandbox.stub(ajaxModule, 'sendBeacon').returns(true); + + sandbox.stub(prebidGlobalModule, 'getGlobal').returns({ + getHighestCpmBids: () => [] + }); + + // Import the WurflLCEDevice to stub it + const wurflRtdProvider = require('modules/wurflRtdProvider.js'); + + const callback = () => { + const device = reqBidsConfigObj.ortb2Fragments.global.device; + + // Should have minimal fallback data + expect(device.js).to.equal(1); + + // UA-dependent fields should not be set when error occurs + expect(device.devicetype).to.be.undefined; + expect(device.os).to.be.undefined; + + // Trigger auction to verify enrichment type in beacon + const auctionDetails = { + bidsReceived: [ + { requestId: 'req1', bidderCode: 'bidder1', adUnitCode: 'ad1', cpm: 1.5, currency: 'USD' } + ], + adUnits: [ + { + code: 'ad1', + bids: [{ bidder: 'bidder1' }] + } + ] + }; + + wurflSubmodule.onAuctionEndEvent(auctionDetails, { params: {} }, null); + + // Check beacon was sent with lcefailed enrichment type + expect(sendBeaconStub.calledOnce).to.be.true; + const beaconCall = sendBeaconStub.getCall(0); + const payload = JSON.parse(beaconCall.args[1]); + expect(payload).to.have.property('enrichment', 'lcefailed'); + + done(); + }; + + // Stub _getDeviceInfo to throw an error + const originalGetDeviceInfo = window.navigator.userAgent; + Object.defineProperty(window.navigator, 'userAgent', { + get: () => { + throw new Error('User agent access failed'); + }, + configurable: true + }); + + wurflSubmodule.getBidRequestData(reqBidsConfigObj, callback, { params: {} }, {}); + + // Restore + Object.defineProperty(window.navigator, 'userAgent', { + value: originalGetDeviceInfo, + configurable: true + }); + }); }); }); }); diff --git a/test/spec/modules/yahooAdsBidAdapter_spec.js b/test/spec/modules/yahooAdsBidAdapter_spec.js index ab4f3eb5b8e..ad1e428aafd 100644 --- a/test/spec/modules/yahooAdsBidAdapter_spec.js +++ b/test/spec/modules/yahooAdsBidAdapter_spec.js @@ -694,7 +694,8 @@ describe('Yahoo Advertising Bid Adapter:', () => { const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; const user = data.user; expect(user[param]).to.be.a('object'); - expect(user[param]).to.be.deep.include({[param]: {a: '123', b: '456'}}); + // Properties from ortb2.user.ext should be merged into user.ext, not nested + expect(user[param]).to.be.deep.include({a: '123', b: '456'}); }); }); diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index 4f4454c17ae..dc4651d2e1f 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -533,11 +533,6 @@ describe('YieldmoAdapter', function () { expect(utils.deepAccess(videoBid, 'params.video')['plcmt']).to.equal(1); }); - it('should add start delay if plcmt value is not 1', function () { - const videoBid = mockVideoBid({}, {}, { plcmt: 2 }); - expect(build([videoBid])[0].data.imp[0].video.startdelay).to.equal(0); - }); - it('should override mediaTypes.video.mimes prop if params.video.mimes is present', function () { utils.deepAccess(videoBid, 'mediaTypes.video')['mimes'] = ['video/mp4']; utils.deepAccess(videoBid, 'params.video')['mimes'] = ['video/mkv']; diff --git a/test/spec/ortbConverter/pbsExtensions/params_spec.js b/test/spec/ortbConverter/pbsExtensions/params_spec.js index d1b36c18b49..bad5307b9af 100644 --- a/test/spec/ortbConverter/pbsExtensions/params_spec.js +++ b/test/spec/ortbConverter/pbsExtensions/params_spec.js @@ -1,20 +1,9 @@ import {setImpBidParams} from '../../../../libraries/pbsExtensions/processors/params.js'; describe('pbjs -> ortb bid params to imp[].ext.prebid.BIDDER', () => { - let bidderRegistry, index, adUnit; - beforeEach(() => { - bidderRegistry = {}; - adUnit = {code: 'mockAdUnit'}; - index = { - getAdUnit() { - return adUnit; - } - } - }); - - function setParams(bidRequest, context, deps = {}) { + function setParams(bidRequest = {}) { const imp = {}; - setImpBidParams(imp, bidRequest, context, Object.assign({bidderRegistry, index}, deps)) + setImpBidParams(imp, bidRequest) return imp; } diff --git a/test/spec/unit/core/storageManager_spec.js b/test/spec/unit/core/storageManager_spec.js index 686464b8b5c..b0a9adcd3a1 100644 --- a/test/spec/unit/core/storageManager_spec.js +++ b/test/spec/unit/core/storageManager_spec.js @@ -1,4 +1,5 @@ import { + canSetCookie, deviceAccessRule, getCoreStorageManager, newStorageManager, @@ -20,6 +21,7 @@ import { ACTIVITY_PARAM_STORAGE_TYPE } from '../../../../src/activities/params.js'; import {activityParams} from '../../../../src/activities/activityParams.js'; +import {registerActivityControl} from '../../../../src/activities/rules.js'; describe('storage manager', function() { before(() => { @@ -319,3 +321,38 @@ describe('storage manager', function() { }); }); }); + +describe('canSetCookie', () => { + let allow, unregisterACRule; + beforeEach(() => { + allow = true; + unregisterACRule = registerActivityControl(ACTIVITY_ACCESS_DEVICE, 'test', (params) => { + if (params.component === 'prebid.storage') { + return {allow}; + } + }) + }); + afterEach(() => { + unregisterACRule(); + canSetCookie.clear(); + }) + + it('should return true when allowed', () => { + expect(canSetCookie()).to.be.true; + }); + it('should not leave stray cookies', () => { + const previousCookies = document.cookie; + canSetCookie(); + expect(previousCookies).to.eql(document.cookie); + }); + it('should return false when not allowed', () => { + allow = false; + expect(canSetCookie()).to.be.false; + }); + + it('should cache results', () => { + expect(canSetCookie()).to.be.true; + allow = false; + expect(canSetCookie()).to.be.true; + }) +}) diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 3a439f7eb04..0ae2f29aa9b 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -27,6 +27,7 @@ import {deepAccess, deepSetValue, generateUUID} from '../../../src/utils.js'; import {getCreativeRenderer} from '../../../src/creativeRenderers.js'; import {BID_STATUS, EVENTS, GRANULARITY_OPTIONS, PB_LOCATOR, TARGETING_KEYS} from 'src/constants.js'; import {getBidToRender} from '../../../src/adRendering.js'; +import {getGlobal} from '../../../src/prebidGlobal.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -1640,6 +1641,117 @@ describe('Unit: Prebid Module', function () { sinon.assert.called(spec.onTimeout); }); + describe('requestBids event', () => { + beforeEach(() => { + sandbox.stub(events, 'emit'); + }); + + it('should be emitted with request', async () => { + const request = { + adUnits + } + await runAuction(request); + sinon.assert.calledWith(events.emit, EVENTS.REQUEST_BIDS, request); + }); + + it('should provide a request object when not supplied to requestBids()', async () => { + getGlobal().addAdUnits(adUnits); + try { + await runAuction(); + sinon.assert.calledWith(events.emit, EVENTS.REQUEST_BIDS, sinon.match({ + adUnits + })); + } finally { + adUnits.map(au => au.code).forEach(getGlobal().removeAdUnit) + } + }); + + it('should not leak internal state', async () => { + const request = { + adUnits + }; + await runAuction(Object.assign({}, request)); + expect(events.emit.args[0][1].metrics).to.not.exist; + }); + + describe('ad unit filter', () => { + let au, request; + + function requestBidsHook(next, req) { + request = req; + next(req); + } + before(() => { + pbjsModule.requestBids.before(requestBidsHook, 999); + }) + after(() => { + pbjsModule.requestBids.getHooks({hook: requestBidsHook}).remove(); + }) + + beforeEach(() => { + request = null; + au = { + ...adUnits[0], + code: 'au' + } + adUnits.push(au); + }); + it('should filter adUnits by code', async () => { + await runAuction({ + adUnits, + adUnitCodes: ['au'] + }); + sinon.assert.calledWith(events.emit, EVENTS.REQUEST_BIDS, sinon.match({ + adUnits: [au], + })); + }); + it('should still pass unfiltered ad units to requestBids', () => { + runAuction({ + adUnits: adUnits.slice(), + adUnitCodes: ['au'] + }); + expect(request.adUnits).to.have.deep.members(adUnits); + }); + + it('should allow event handlers to add ad units', () => { + const extraAu = { + ...adUnits[0], + code: 'extra' + } + events.emit.callsFake((evt, request) => { + request.adUnits.push(extraAu) + }); + runAuction({ + adUnits: adUnits.slice(), + adUnitCodes: ['au'] + }); + expect(request.adUnits).to.have.deep.members([...adUnits, extraAu]); + }); + + it('should allow event handlers to remove ad units', () => { + events.emit.callsFake((evt, request) => { + request.adUnits = []; + }); + runAuction({ + adUnits: adUnits.slice(), + adUnitCodes: ['au'] + }); + expect(request.adUnits).to.eql([adUnits[0]]); + }); + + it('should NOT allow event handlers to modify adUnitCodes', () => { + events.emit.callsFake((evt, request) => { + request.adUnitCodes = ['other'] + }); + runAuction({ + adUnits, + adUnitCodes: ['au'] + }); + expect(request.adUnitCodes).to.eql(['au']); + }) + }); + }) + it('should execute `onSetTargeting` after setTargetingForGPTAsync', async function () { const bidId = 1; const auctionId = 1; diff --git a/test/spec/unit/secureCreatives_spec.js b/test/spec/unit/secureCreatives_spec.js index 24c7542b31d..6f48a0364a2 100644 --- a/test/spec/unit/secureCreatives_spec.js +++ b/test/spec/unit/secureCreatives_spec.js @@ -1,4 +1,4 @@ -import {getReplier, receiveMessage, resizeRemoteCreative} from 'src/secureCreatives.js'; +import {getReplier, receiveMessage, resizeAnchor, resizeRemoteCreative} from 'src/secureCreatives.js'; import * as utils from 'src/utils.js'; import {getAdUnits, getBidRequests, getBidResponses} from 'test/fixtures/fixtures.js'; import {auctionManager} from 'src/auctionManager.js'; @@ -631,4 +631,65 @@ describe('secureCreatives', () => { sinon.assert.notCalled(document.getElementById); }) }) + + describe('resizeAnchor', () => { + let ins, clock; + beforeEach(() => { + clock = sinon.useFakeTimers(); + ins = { + style: { + width: 'auto', + height: 'auto' + } + } + }); + afterEach(() => { + clock.restore(); + }) + function setSize(width = '300px', height = '250px') { + ins.style.width = width; + ins.style.height = height; + } + it('should not change dimensions until they have been set externally', () => { + const pm = resizeAnchor(ins, 100, 200); + clock.tick(200); + expect(ins.style).to.eql({width: 'auto', height: 'auto'}); + setSize(); + clock.tick(200); + return pm.then(() => { + expect(ins.style.width).to.eql('100px'); + expect(ins.style.height).to.eql('200px'); + }) + }) + it('should quit trying if dimensions are never set externally', () => { + const pm = resizeAnchor(ins, 100, 200); + clock.tick(5000); + return pm + .then(() => { sinon.assert.fail('should have thrown') }) + .catch(err => { + expect(err.message).to.eql('Could not resize anchor') + }) + }); + it('should not choke when initial width/ height are null', () => { + ins.style = {}; + const pm = resizeAnchor(ins, 100, 200); + clock.tick(200); + setSize(); + clock.tick(200); + return pm.then(() => { + expect(ins.style.width).to.eql('100px'); + expect(ins.style.height).to.eql('200px'); + }) + }); + + it('should not resize dimensions that are set to 100%', () => { + const pm = resizeAnchor(ins, 100, 200); + setSize('100%', '250px'); + clock.tick(200); + return pm.then(() => { + expect(ins.style.width).to.eql('100%'); + expect(ins.style.height).to.eql('200px'); + }); + }) + }) }); diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index 1efdc5621f6..8ed2efa3b9f 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -1446,7 +1446,7 @@ describe('getWinDimensions', () => { }); it('should clear cache once per 20ms', () => { - const resetWinDimensionsSpy = sinon.spy(winDimensions.internal, 'reset'); + const resetWinDimensionsSpy = sinon.spy(winDimensions.internal.winDimensions, 'reset'); expect(getWinDimensions().innerHeight).to.exist; clock.tick(1); expect(getWinDimensions().innerHeight).to.exist; diff --git a/test/test_deps.js b/test/test_deps.js index e35e813a574..7047e775d9c 100644 --- a/test/test_deps.js +++ b/test/test_deps.js @@ -39,6 +39,8 @@ sinon.useFakeXMLHttpRequest = fakeXhr.useFakeXMLHttpRequest.bind(fakeXhr); sinon.createFakeServer = fakeServer.create.bind(fakeServer); sinon.createFakeServerWithClock = fakeServerWithClock.create.bind(fakeServerWithClock); +localStorage.clear(); + require('test/helpers/global_hooks.js'); require('test/helpers/consentData.js'); require('test/helpers/prebidGlobal.js'); diff --git a/wdio.conf.js b/wdio.conf.js index d23fecd0b15..53ccd216b69 100644 --- a/wdio.conf.js +++ b/wdio.conf.js @@ -1,4 +1,5 @@ const shared = require('./wdio.shared.conf.js'); +const process = require('process'); const browsers = Object.fromEntries( Object.entries(require('./browsers.json')) @@ -28,7 +29,7 @@ function getCapabilities() { osVersion: browser.os_version, networkLogs: true, consoleLogs: 'verbose', - buildName: `Prebidjs E2E (${browser.browser} ${browser.browser_version}) ${new Date().toLocaleString()}` + buildName: process.env.BROWSERSTACK_BUILD_NAME }, acceptInsecureCerts: true, }); diff --git a/wdio.local.conf.js b/wdio.local.conf.js index 772448472bf..74c7ac3a3ee 100644 --- a/wdio.local.conf.js +++ b/wdio.local.conf.js @@ -1,4 +1,5 @@ const shared = require('./wdio.shared.conf.js'); +const process = require('process'); exports.config = { ...shared.config, @@ -9,5 +10,21 @@ exports.config = { args: ['headless', 'disable-gpu'], }, }, - ], + { + browserName: 'firefox', + 'moz:firefoxOptions': { + args: ['-headless'] + } + }, + { + browserName: 'msedge', + 'ms:edgeOptions': { + args: ['--headless'] + } + }, + { + browserName: 'safari technology preview' + } + ].filter((cap) => cap.browserName === (process.env.BROWSER ?? 'chrome')), + maxInstancesPerCapability: 1 };