Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e165898
Bump axios from 1.16.0 to 1.18.1 (#15372)
dependabot[bot] Jul 22, 2026
f960adf
Bump fast-xml-parser from 5.9.3 to 5.10.1 (#15378)
dependabot[bot] Jul 23, 2026
ac2c542
Bump fast-uri from 3.1.2 to 3.1.4 (#15376)
dependabot[bot] Jul 23, 2026
61db77c
Bump body-parser from 1.20.5 to 1.20.6 (#15375)
dependabot[bot] Jul 23, 2026
9b2b81a
Bump tar from 7.5.16 to 7.5.21 (#15377)
dependabot[bot] Jul 23, 2026
3cab5f1
Core: fpd validation library (#15333)
mkomorski Jul 23, 2026
5e132be
Core: remove Topics API handling (#15369)
patmmccann Jul 23, 2026
a99eb55
gamAdServerVideo: Add GPP consent information to the DFP video url (#…
robin-crazygames Jul 25, 2026
b12a85f
New Module: DAA AdChoices Signal (#15138)
rvidakovic Jul 25, 2026
4d7a4fe
Module EightPod Adapter: update bid and analytics adapters (#15253)
ad8pod Jul 25, 2026
50e1858
WURFL RTD: report configurable caps in beacon via wurfl_caps (#15293)
lucor-sm Jul 27, 2026
fa6edc9
realTimeData module: Allow post-install (#15116)
olafbuitelaar Jul 27, 2026
9a1de95
Copper6 adapter: change utility suite (#14991)
anna-y-perion Jul 28, 2026
eaf1dfb
adQuery ID System: resolve qid synchronously without backend round-tr…
adzida-adquery Jul 28, 2026
53915de
Vidazoo utils: add event callbacks (#15335)
anna-y-perion Jul 28, 2026
53795e3
Revert "Vidazoo utils: add event callbacks (#15335)" (#15400)
patmmccann Jul 28, 2026
44f01a7
Tests: demonstrate tracking pixel URL HTML insertion isn't dangerous …
patmmccann Jul 28, 2026
af7cd9b
Prebid 11.26.0 release - reautomated
Aug 14, 2026
bece749
Rename pbjs.setConfig to oajs.setConfig in new AdChoices module docs
khang-vu-ttd Aug 14, 2026
1ead283
Remove orphaned copper6sspBidAdapter.d.ts
khang-vu-ttd Aug 14, 2026
734a51f
Tests: fix permutiveCombined_spec.js expiry timing fragility
khang-vu-ttd Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion libraries/dfpUtils/dfpUtils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { gdprDataHandler } from '../../src/consentHandler.js';
import { gdprDataHandler, gppDataHandler } from '../../src/consentHandler.js';

/** Safe defaults which work on pretty much all video calls. */
export const DEFAULT_DFP_PARAMS = {
Expand All @@ -24,3 +24,13 @@ export function gdprParams() {
}
return params;
}

export function gppParams() {
const gppConsent = gppDataHandler.getConsentData();
const params = {};
if (gppConsent) {
if (gppConsent.gppString) { params.gpp = gppConsent.gppString; }
if (gppConsent.applicableSections) { params.gpp_sid = gppConsent.applicableSections.join(','); }
}
return params;
}
File renamed without changes.
10 changes: 10 additions & 0 deletions libraries/fpdUtils/pubcidOptout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { StorageManager } from '../../src/storageManager.js';

export const PUBCID_OPTOUT_KEY = '_pubcid_optout';

export function hasPubcidOptout(storage: StorageManager): boolean {
return Boolean(
(storage.cookiesAreEnabled() && storage.getCookie(PUBCID_OPTOUT_KEY)) ||
(storage.hasLocalStorage() && storage.getDataFromLocalStorage(PUBCID_OPTOUT_KEY))
);
}
205 changes: 205 additions & 0 deletions libraries/fpdUtils/validateFpd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { ORTB_MAP } from './ortbMap.js';

/**
* Utility functions the validator depends on. These are expected to be the
* corresponding exports from `src/utils.js`, injected by the caller so that
* this library stays decoupled from core.
*/
export type FpdValidatorDeps = {
logWarn: (...args: any[]) => void;
isNumber: (val: unknown) => val is number;
isEmpty: (val: unknown) => boolean;
deepAccess: (obj: any, path: string) => any;
/**
* Deep clone, used to avoid mutating the caller's data when `filter` is false.
* Required when `filter` is false; unused otherwise.
*/
deepClone?: <T>(obj: T) => T;
};

export type FpdValidatorOptions = {
/**
* Whether the validator removes invalid data from its input. This only affects
* the wording of the warnings: `true` (the default) reports data as "Filtered";
* `false` reports it as "Invalid", for callers that inspect without altering the data.
*/
filter?: boolean;
};

/**
* Build an ortb2 first-party-data validator.
* @param deps utility functions from `src/utils.js`
* @param deps.logWarn warning logger
* @param deps.isNumber number type guard
* @param deps.isEmpty empty-value check
* @param deps.deepAccess dotted-path accessor
* @param deps.deepClone deep clone (used only when `filter` is false)
* @param options validator options
* @param options.filter whether invalid data is removed (controls warning wording and whether the input is modified)
* @returns `validateFpd` and `filterArrayData` bound to the injected utilities
*/
export function fpdValidator({ logWarn, isNumber, isEmpty, deepAccess, deepClone }: FpdValidatorDeps, { filter = true }: FpdValidatorOptions = {}) {
const label = filter ? 'Filtered' : 'Invalid';
function isEmptyData(data) {
let check = true;

if (typeof data === 'object' && !isEmpty(data)) {
check = false;
} else if (typeof data !== 'object' && (isNumber(data) || data)) {
check = false;
}

return check;
}

function getRequiredData(obj, required, parent, i) {
let check = true;

required.forEach(key => {
if (!obj[key] || isEmptyData(obj[key])) {
check = false;
logWarn(`${label} ${parent}[] value at index ${i} in ortb2 data: missing required property ${key}`);
}
});

return check;
}

function typeValidation(data, mapping) {
let check = false;

switch (mapping.type) {
case 'string':
if (typeof data === 'string') check = true;
break;
case 'number':
if (typeof data === 'number' && isFinite(data)) check = true;
break;
case 'object':
if (typeof data === 'object') {
if ((Array.isArray(data) && mapping.isArray) || (!Array.isArray(data) && !mapping.isArray)) check = true;
}
break;
}

return check;
}

function filterArrayData(arr, child, path, parent, optout = false) {
arr = arr.filter((index, i) => {
const check = typeValidation(index, { type: child.type, isArray: child.isArray });

if (check && Array.isArray(index) === Boolean(child.isArray)) {
return true;
}

logWarn(`${label} ${parent}[] value at index ${i} in ortb2 data: expected type ${child.type}`);
return false;
}).filter((index, i) => {
let requiredCheck = true;
const mapping = deepAccess(ORTB_MAP, path);

if (mapping && mapping.required) requiredCheck = getRequiredData(index, mapping.required, parent, i);

if (requiredCheck) return true;
return false;
}).reduce((result, value, i) => {
let typeBool = false;
const mapping = deepAccess(ORTB_MAP, path);

switch (child.type) {
case 'string':
result.push(value);
typeBool = true;
break;
case 'object':
if (mapping && mapping.children) {
const validObject = validate(value, path + '.children.', parent + '.', optout);
if (Object.keys(validObject).length) {
const requiredCheck = getRequiredData(validObject, mapping.required, parent, i);

if (requiredCheck) {
result.push(validObject);
typeBool = true;
}
}
} else {
result.push(value);
typeBool = true;

Check warning on line 128 in libraries/fpdUtils/validateFpd.ts

View workflow job for this annotation

GitHub Actions / Coverage

127-128 lines are not covered with tests
}
break;
}

if (!typeBool) logWarn(`${label} ${parent}[] value at index ${i} in ortb2 data: expected type ${child.type}`);

return result;
}, []);

return arr;
}

function validate(fpd, path = '', parent = '', optout = false) {
if (!fpd) return {};

const validObject = Object.assign({}, Object.keys(fpd).filter(key => {
const mapping = deepAccess(ORTB_MAP, path + key);

if (!mapping || !mapping.invalid) return key;

logWarn(`${label} ${parent}${key} property in ortb2 data: invalid property`);
return false;
}).filter(key => {
const mapping = deepAccess(ORTB_MAP, path + key);
const typeBool = (mapping) ? typeValidation(fpd[key], { type: mapping.type, isArray: mapping.isArray }) : true;

if (typeBool || !mapping) return key;

logWarn(`${label} ${parent}${key} property in ortb2 data: expected type ${(mapping.isArray) ? 'array' : mapping.type}`);
return false;
}).reduce((result, key) => {
const mapping = deepAccess(ORTB_MAP, path + key);

if (mapping) {
if (mapping.optoutApplies && optout) {
logWarn(`${label} ${parent}${key} data: pubcid optout found`);
return result;
}

const modified = (mapping.type === 'object' && !mapping.isArray)
? validate(fpd[key], path + key + '.children.', parent + key + '.', optout)
: (mapping.isArray && mapping.childType)
? filterArrayData(fpd[key], { type: mapping.childType, isArray: mapping.childisArray }, path + key, parent + key, optout) : fpd[key];

(!isEmptyData(modified)) ? result[key] = modified
: logWarn(`${label} ${parent}${key} property in ortb2 data: empty data found`);
} else {
result[key] = fpd[key];
}

return result;
}, {}));

return validObject;
}

/**
* Validate ortb2 first-party data.
* When `filter` is true, returns a copy with invalid data removed.
* When `filter` is false, the input is left untouched (validation runs against a
* clone purely to emit warnings) and the original object is returned unchanged.
* @throws when `filter` is false but no `deepClone` was provided, as the input
* cannot be inspected without risking mutation.
*/
function validateFpd(fpd, path = '', parent = '', optout = false) {
if (!filter) {
if (deepClone == null) {
throw new Error('fpdValidator: a deepClone dependency is required when filter is false');
}
validate(deepClone(fpd), path, parent, optout);
return fpd;
}
return validate(fpd, path, parent, optout);
}

return { validateFpd, filterArrayData };
}
2 changes: 1 addition & 1 deletion libraries/gamUtils/gamUtils.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { DEFAULT_DFP_PARAMS as DEFAULT_GAM_PARAMS, DFP_ENDPOINT as GAM_ENDPOINT, gdprParams } from '../dfpUtils/dfpUtils.js';
export { DEFAULT_DFP_PARAMS as DEFAULT_GAM_PARAMS, DFP_ENDPOINT as GAM_ENDPOINT, gdprParams, gppParams } from '../dfpUtils/dfpUtils.js';
4 changes: 3 additions & 1 deletion libraries/vidazooUtils/bidderUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
setStorageItem(storage, key, nextValue, timestamp);
return nextValue;
} catch (e) {
return 0;

Check warning on line 127 in libraries/vidazooUtils/bidderUtils.js

View workflow job for this annotation

GitHub Actions / Coverage

127 line is not covered with tests
}
}

Expand Down Expand Up @@ -338,7 +338,6 @@
url: encodeURIComponent(topWindowUrl),
uqs: getTopWindowQueryParams(),
cb: Date.now(),
bidFloor: bidFloor,
bidId: bidId,
referrer: bidderRequest.refererInfo.ref,
adUnitCode: adUnitCode,
Expand Down Expand Up @@ -368,6 +367,9 @@
...uniqueRequestData
};

if (bidFloor) {
data.bidFloor = bidFloor;
}
// backward compatible userId generators
if (bid.userIdAsEids?.length > 0) {
appendUserIdsAsEidsToRequestPayload(data, bid.userIdAsEids);
Expand Down
6 changes: 4 additions & 2 deletions libraries/vidazooUtils/vidazooTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ export interface VidazooBaseBidderParams {
/**
* The minimum bid value desired. Adapter will not respond with bids lower than this value
*/
bidFloor: number;
bidFloor?: number;
/**
* Placement id on platform.
*/

placementId?: number;
/**
* Custom parameters for the request
*/
ext?: Ext;
/**
* Subdomain define subdomain in the bid request URL
Expand Down
87 changes: 87 additions & 0 deletions modules/adChoices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Overview

Module Name: AdChoices Signal Module
Module Type: Consent Module
Maintainer: prebid@aboutads.info

# Description

This module reads the [DAA (Digital Advertising Alliance) AdChoices Signal](https://github.com/Digital-Advertising-Alliance/DAA-Choice-Tools/blob/main/AdChoices%20Signal/AdChoices%20Signal%20Specification.md)
and conveys it in the OpenRTB bid stream as the community extension
`regs.ext.adchoices`, as described in Appendix 5 of the specification.

The AdChoices Signal is a base64url-encoded string that expresses a user's
interest-based advertising preferences. In the browser it can be read from the
DAA's [Protect My Choices (PMC)](https://github.com/Digital-Advertising-Alliance/DAA-Choice-Tools/blob/main/Protect%20My%20Choices/PMC2%20Overview.md)
extension, which exposes the signal via a `window.postMessage` protocol. When a
user does not have the extension installed, no signal is read and nothing is
added to the bid stream.

Publishers who obtain the signal by other means (for example, reading the
`X-AdChoices` request header on their server) can supply it directly through the
module's `signal` configuration option.

# Integration

Build the module into your Prebid.js package:

```bash
gulp build --modules=adChoices
```

# Configuration

The module works with no configuration. To supply a static signal or to opt into
delaying auctions while the signal is read, use the `adChoices` config namespace:

```javascript
oajs.setConfig({
adChoices: {
// Optional: a statically supplied AdChoices Signal. Takes precedence over a
// value read from the browser extension.
signal: 'AAEAA... (base64url signal)',

// Optional: max milliseconds to delay the first auction while waiting for the
// signal from the extension. Default 0 (non-blocking).
timeout: 0
}
});
```

| Param | Scope | Type | Description |
|---|---|---|---|
| `signal` | optional | string | A statically supplied AdChoices Signal. When set, it is used as-is and takes precedence over any value read from the Protect My Choices extension. |
| `timeout` | optional | integer | Max milliseconds to delay auctions while waiting for the signal from the extension. Defaults to `0` (non-blocking) so that users without the extension are not delayed. When set to a positive value, the first auction is delayed up to this many ms; the delay window starts when an auction begins waiting and applies once, so later auctions are not re-delayed. |

# What changes in the bid request

When a signal is available it is added to every outgoing bid request at
`regs.ext.adchoices`:

```json
{
"regs": {
"ext": {
"adchoices": "<AdChoices Signal string>"
}
}
}
```

# How the signal is read

When included, the module automatically begins listening for the signal from the
Protect My Choices extension using the documented message protocol:

1. The extension posts an `ExtensionLoaded` message when it is ready.
2. The module requests the preferences by posting `{ type: "GetAdPreferences" }`.
3. The extension responds with an `AdPreferences` message whose `data` field
contains the AdChoices Signal string.

The module also proactively sends a `GetAdPreferences` request on startup in case
the `ExtensionLoaded` message fired before the listener was attached.

Note: page JavaScript cannot read the `X-AdChoices` (Chrome) / `Cookie2` (Safari)
headers that the extension injects into outbound requests — those are intended for
server-side consumption. In the browser, the postMessage protocol (or the `signal`
config option) is the supported way to obtain the value.
Loading
Loading