Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions crates/trusted-server-core/src/integrations/prebid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -921,7 +921,7 @@ pub fn register(
.with_proxy(integration.clone())
.with_attribute_rewriter(integration.clone())
.with_head_injector(integration)
.without_js()
.with_deferred_js()
.build(),
))
}
Expand Down Expand Up @@ -2937,8 +2937,8 @@ passphrase = "test-secret-key-32-bytes-minimum"
"External prebid bundle route should be injected"
);
assert!(
!processed.contains("tsjs-prebid.min.js"),
"Embedded deferred prebid bundle should not be injected"
processed.contains("tsjs-prebid.min.js"),
"Deferred tsjs prebid shim should be injected"
);
}

Expand Down
22 changes: 11 additions & 11 deletions crates/trusted-server-core/src/integrations/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1949,7 +1949,7 @@ mod tests {
}

#[test]
fn js_module_ids_exclude_prebid_and_include_core_js_only_modules() {
fn js_module_ids_defer_prebid_and_include_core_js_only_modules() {
let settings = crate::test_support::tests::create_test_settings();
let mut settings_with_prebid = settings;
settings_with_prebid
Expand All @@ -1975,8 +1975,8 @@ mod tests {
let deferred = registry.js_module_ids_deferred();

assert!(
!all.contains(&"prebid"),
"should not include prebid in embedded TSJS module IDs"
all.contains(&"prebid"),
"should include the prebid shim in embedded TSJS module IDs"
);
assert!(
immediate.contains(&"creative"),
Expand All @@ -1991,8 +1991,8 @@ mod tests {
"should not include prebid in immediate IDs"
);
assert!(
!deferred.contains(&"prebid"),
"should not include prebid in deferred IDs"
deferred.contains(&"prebid"),
"should serve the prebid shim as a deferred module"
);
}

Expand Down Expand Up @@ -2077,7 +2077,7 @@ mod tests {
}

#[test]
fn js_module_ids_exclude_prebid_when_external_bundle_is_configured() {
fn js_module_ids_defer_prebid_shim_when_external_bundle_is_configured() {
let mut settings = crate::test_support::tests::create_test_settings();
settings
.integrations
Expand All @@ -2094,16 +2094,16 @@ mod tests {
let registry = IntegrationRegistry::new(&settings).expect("should create registry");

assert!(
!registry.js_module_ids().contains(&"prebid"),
"external bundle mode should not include prebid in embedded TSJS modules"
registry.js_module_ids().contains(&"prebid"),
"external bundle mode should include the prebid shim in embedded TSJS modules"
);
assert!(
!registry.js_module_ids_immediate().contains(&"prebid"),
"external bundle mode should not include prebid in immediate TSJS modules"
"the prebid shim should not load in the immediate TSJS bundle"
);
assert!(
!registry.js_module_ids_deferred().contains(&"prebid"),
"external bundle mode should not include prebid in deferred TSJS modules"
registry.js_module_ids_deferred().contains(&"prebid"),
"the prebid shim should load as a deferred TSJS module"
);
assert!(
registry.has_route(&Method::GET, "/integrations/prebid/bundle.js"),
Expand Down
6 changes: 3 additions & 3 deletions crates/trusted-server-core/src/publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3796,7 +3796,7 @@ mod tests {
}

#[test]
fn tsjs_dynamic_does_not_serve_embedded_prebid() {
fn tsjs_dynamic_serves_prebid_shim_when_enabled() {
let settings = create_test_settings();
let registry =
IntegrationRegistry::new(&settings).expect("should create integration registry");
Expand All @@ -3808,8 +3808,8 @@ mod tests {
let response = handle_tsjs_dynamic(&req, &registry).expect("should handle tsjs request");
assert_eq!(
response.status(),
StatusCode::NOT_FOUND,
"should not serve embedded prebid module"
StatusCode::OK,
"should serve the deferred prebid shim module when prebid is enabled"
);
}

Expand Down
11 changes: 6 additions & 5 deletions crates/trusted-server-core/src/tsjs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,13 @@ mod tests {
}

#[test]
fn tsjs_deferred_script_src_uses_empty_hash_for_external_or_unknown_module() {
assert_eq!(
tsjs_deferred_script_src("prebid"),
"/static/tsjs=tsjs-prebid.min.js?v=",
"prebid now ships as an external bundle and has no local hash"
fn tsjs_deferred_script_src_hashes_prebid_shim_and_empties_unknown_module() {
let prebid_src = tsjs_deferred_script_src("prebid");
assert!(
prebid_src.starts_with("/static/tsjs=tsjs-prebid.min.js?v="),
"prebid shim should be served from the deferred tsjs route"
);
assert_sha256_hex_hash(hash_query_value(&prebid_src));
assert_eq!(
tsjs_deferred_script_src("unknown-module"),
"/static/tsjs=tsjs-unknown-module.min.js?v=",
Expand Down
11 changes: 5 additions & 6 deletions crates/trusted-server-js/lib/build-all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
* tsjs-core.js — core API (always included)
* tsjs-<integration>.js — one per discovered integration
*
* Prebid is intentionally excluded from this embedded build. Use
* build-prebid-external.mjs to generate publisher-specific Prebid bundles
* outside the Cargo build.
* The prebid integration builds here as the tsjs shim only — Prebid.js itself
* is never bundled into tsjs. Use build-prebid-external.mjs to generate the
* pure Prebid.js external bundle (core + adapters + user ID modules) that the
* shim requires at runtime via integrations.prebid.external_bundle_url.
*/

import fs from 'node:fs';
Expand All @@ -34,9 +35,7 @@ const integrationModules = fs.existsSync(integrationsDir)
.filter((name) => {
const fullPath = path.join(integrationsDir, name);
return (
name !== 'prebid' &&
fs.statSync(fullPath).isDirectory() &&
fs.existsSync(path.join(fullPath, 'index.ts'))
fs.statSync(fullPath).isDirectory() && fs.existsSync(path.join(fullPath, 'index.ts'))
);
})
.sort()
Expand Down
40 changes: 39 additions & 1 deletion crates/trusted-server-js/lib/build-prebid-external.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,39 @@ function createTemporaryModulePaths() {
temporaryDir,
adaptersFile: path.join(temporaryDir, '_adapters.generated.ts'),
userIdsFile: path.join(temporaryDir, '_user_ids.generated.ts'),
entryFile: path.join(temporaryDir, '_external_entry.generated.ts'),
};
}

function generateExternalEntry(entryFile, adapters) {
const content = [
'// Auto-generated by build-prebid-external.mjs.',
'//',
'// Pure Prebid.js external bundle: core, consent modules, user ID modules,',
'// and client-side bid adapters. The Trusted Server prebid shim',
'// (tsjs-prebid, served by the server) installs the trustedServer adapter',
'// onto the `window.pbjs` global this bundle populates and drives queue',
'// processing — this bundle intentionally does NOT call processQueue().',
"import 'prebid.js';",
"import 'prebid.js/modules/consentManagementTcf.js';",
"import 'prebid.js/modules/consentManagementGpp.js';",
"import 'prebid.js/modules/consentManagementUsp.js';",
"import 'prebid.js/modules/userId.js';",
"import './_adapters.generated';",
"import { INCLUDED_PREBID_USER_ID_MODULES } from './_user_ids.generated';",
'',
'// Manifest consumed by the tsjs prebid shim to validate that every',
'// configured client_side_bidder has its adapter compiled in.',
'(window as unknown as Record<string, unknown>).__tsjs_prebid_bundle = Object.freeze({',
` adapters: ${JSON.stringify(adapters)},`,
' userIdModules: INCLUDED_PREBID_USER_ID_MODULES,',
'});',
'',
].join('\n');

fs.writeFileSync(entryFile, content);
}

export function deriveBundleMetadata(bundleBytes) {
const sha256 = crypto.createHash('sha256').update(bundleBytes).digest('hex');
const sri = `sha384-${crypto.createHash('sha384').update(bundleBytes).digest('base64')}`;
Expand Down Expand Up @@ -224,6 +254,13 @@ async function buildExternalBundle(outDir, generatedModules) {
'node_modules/prebid.js/dist/src/src/adapterManager.js'
),
},
{
find: 'prebid.js/src/adRendering.js',
replacement: path.resolve(
__dirname,
'node_modules/prebid.js/dist/src/src/adRendering.js'
),
},
],
},
build: {
Expand All @@ -233,7 +270,7 @@ async function buildExternalBundle(outDir, generatedModules) {
sourcemap: false,
minify: 'esbuild',
rollupOptions: {
input: path.join(prebidDir, 'index.ts'),
input: generatedModules.entryFile,
output: {
format: 'iife',
dir: outDir,
Expand Down Expand Up @@ -270,6 +307,7 @@ export async function main(argv = process.argv.slice(2)) {
try {
const adapters = generateAdapterImports(args.adapters, generatedModules.adaptersFile);
const userIdModules = generateUserIdImports(args.userIdModules, generatedModules.userIdsFile);
generateExternalEntry(generatedModules.entryFile, adapters);
const bundle = await buildExternalBundle(args.outDir, generatedModules);
const manifest = {
prebidVersion: prebidPackageVersion(),
Expand Down
4 changes: 2 additions & 2 deletions crates/trusted-server-js/lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"dev": "vite build --watch",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint \"src/**/*.{ts,tsx}\"",
"lint:fix": "eslint --fix \"src/**/*.{ts,tsx}\"",
"lint": "eslint \"src/**/*.{ts,tsx}\" \"test/**/*.{ts,tsx}\"",
"lint:fix": "eslint --fix \"src/**/*.{ts,tsx}\" \"test/**/*.{ts,tsx}\"",
"format": "prettier --check \"**/*.{ts,tsx,js,json,css,md}\"",
"format:write": "prettier --write \"**/*.{ts,tsx,js,json,css,md}\""
},
Expand Down
109 changes: 76 additions & 33 deletions crates/trusted-server-js/lib/src/integrations/prebid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,56 @@
// The shim on requestBids injects "trustedServer" into every ad unit so all
// bids flow through the orchestrator.

import pbjs from 'prebid.js';
import adapterManager from 'prebid.js/src/adapterManager.js';
import 'prebid.js/modules/consentManagementTcf.js';
import 'prebid.js/modules/consentManagementGpp.js';
import 'prebid.js/modules/consentManagementUsp.js';
import 'prebid.js/modules/userId.js';

// Client-side bid adapters — self-register with prebid.js on import.
// The external bundle generator aliases these placeholder modules to temporary
// modules built from its --adapters and --user-id-modules options. When a bidder
// is listed in `client_side_bidders` in trusted-server.toml, the requestBids
// shim leaves its bids untouched and the corresponding adapter handles them
// natively in the browser.
import './_adapters.generated';
import type _pbjsDefault from 'prebid.js';

import { log } from '../../core/log';
import { buildAdRequest, parseAuctionResponse } from '../../core/auction';
import type { AuctionBid, AuctionEid } from '../../core/auction';
import type { AuctionSlot } from '../../core/types';

import { INCLUDED_PREBID_USER_ID_MODULES } from './_user_ids.generated';
import { PREBID_USER_ID_MODULE_REGISTRY } from './user_id_modules';

/**
* Prebid.js public API surface (type-only; erased at build time).
*
* `getUserIdsAsEids` is added by the userId module at runtime, which the base
* package typing does not model.
*/
type PbjsGlobal = typeof _pbjsDefault & {
getUserIdsAsEids?: () => unknown[];
};

// Prebid.js itself is NOT bundled into this module. It is served as the
// external bundle configured via `integrations.prebid.external_bundle_url`
// (required whenever the prebid integration is enabled) and owns the
// `window.pbjs` global. The Rust head injector emits a stub
// (`window.pbjs = window.pbjs || {que:[],cmd:[]}`) before any script runs and
// Prebid.js installs its API onto that same object, so capturing the reference
// at module scope is safe regardless of evaluation order.
const pbjs: PbjsGlobal = (
typeof window !== 'undefined'
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
((window as any).pbjs ??= { que: [], cmd: [] })
: { que: [], cmd: [] }
) as PbjsGlobal;

/**
* Manifest stamped on `window.__tsjs_prebid_bundle` by the external Prebid.js
* bundle (see build-prebid-external.mjs): which client-side bid adapters and
* user ID modules were compiled into it.
*/
interface ExternalPrebidBundleManifest {
adapters?: string[];
userIdModules?: string[];
}

function getExternalBundleManifest(): ExternalPrebidBundleManifest | undefined {
if (typeof window === 'undefined') {
return undefined;
}
return (window as { __tsjs_prebid_bundle?: ExternalPrebidBundleManifest }).__tsjs_prebid_bundle;
}

const ADAPTER_CODE = 'trustedServer';
// OpenRTB permits vendor-specific agent types; PAIR uses 571187.
// Keep this range aligned with the signed 32-bit Rust/OpenRTB representation.
Expand Down Expand Up @@ -139,18 +166,19 @@ function readConfiguredUserIdNames(): string[] {
}

function recordUserIdModuleDiagnostics(): PrebidUserIdDiagnostics {
const includedUserIdModules = getExternalBundleManifest()?.userIdModules ?? [];
const configuredUserIdNames = [...new Set(readConfiguredUserIdNames())].sort();
const coveredConfigNames = new Set(
PREBID_USER_ID_MODULE_REGISTRY.filter((entry) =>
INCLUDED_PREBID_USER_ID_MODULES.includes(entry.moduleName)
includedUserIdModules.includes(entry.moduleName)
).flatMap((entry) => entry.configNames)
);
const missingConfiguredUserIdNames = configuredUserIdNames.filter(
(name) => !coveredConfigNames.has(name)
);

const diagnostics: PrebidUserIdDiagnostics = {
includedModules: [...INCLUDED_PREBID_USER_ID_MODULES],
includedModules: [...includedUserIdModules],
configuredUserIdNames,
missingConfiguredUserIdNames,
};
Expand Down Expand Up @@ -502,6 +530,18 @@ function collectAuctionEids(): AuctionEid[] | undefined {
* 2. `config` argument — explicit overrides from the publisher's JS
*/
export function installPrebidNpm(config?: Partial<PrebidNpmConfig>): typeof pbjs {
// The prebid integration requires the external Prebid.js bundle
// (integrations.prebid.external_bundle_url). When it failed to load (network
// error, SRI mismatch) window.pbjs is still the head-injected stub with no
// API — installing the adapter is impossible, so bail out loudly.
if (typeof (pbjs as { registerBidAdapter?: unknown }).registerBidAdapter !== 'function') {
log.error(
'[tsjs-prebid] window.pbjs has no Prebid.js API — the external Prebid bundle ' +
'failed to load. Prebid integration disabled.'
);
return pbjs;
}

const injected = getInjectedConfig();
const merged: PrebidNpmConfig = {
endpoint: config?.endpoint,
Expand Down Expand Up @@ -661,7 +701,7 @@ export function installPrebidNpm(config?: Partial<PrebidNpmConfig>): typeof pbjs
opts.bidsBackHandler = function (...args: unknown[]) {
syncPrebidEidsCookie();
if (typeof originalBidsBack === 'function') {
originalBidsBack.apply(this, args);
(originalBidsBack as (...handlerArgs: unknown[]) => void).apply(this, args);
}
};

Expand All @@ -682,24 +722,27 @@ export function installPrebidNpm(config?: Partial<PrebidNpmConfig>): typeof pbjs
pbjs.processQueue();
recordUserIdModuleDiagnostics();

// Validate that every client-side bidder has its adapter registered.
// Adapters self-register on import, so a missing adapter means the bidder
// was listed in client_side_bidders but not included in the generated
// external Prebid bundle. Without the adapter the bidder is silently dropped
// from both server-side and client-side auctions.
for (const bidder of clientSideBidders) {
try {
if (!adapterManager.getBidAdapter(bidder)) {
// Validate that every client-side bidder has its adapter compiled into the
// external Prebid.js bundle. The bundle stamps its adapter list on
// window.__tsjs_prebid_bundle; a missing adapter means the bidder was listed
// in client_side_bidders but not included in the generated bundle, so it is
// silently dropped from both server-side and client-side auctions.
const bundledAdapters = getExternalBundleManifest()?.adapters;
if (bundledAdapters === undefined) {
if (clientSideBidders.size > 0) {
log.warn(
'[tsjs-prebid] external Prebid bundle did not stamp an adapter manifest; ' +
'cannot verify client_side_bidders adapters'
);
}
} else {
for (const bidder of clientSideBidders) {
if (!bundledAdapters.includes(bidder)) {
log.error(
`[tsjs-prebid] client-side bidder "${bidder}" has no adapter loaded. ` +
`Add it to build-prebid-external.mjs --adapters.`
`[tsjs-prebid] client-side bidder "${bidder}" has no adapter in the external ` +
`Prebid bundle. Add it to build-prebid-external.mjs --adapters.`
);
}
} catch {
log.error(
`[tsjs-prebid] client-side bidder "${bidder}" has no adapter loaded. ` +
`Add it to build-prebid-external.mjs --adapters.`
);
}
}

Expand Down
Loading
Loading