Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
run: cargo test --workspace

- name: Run host-target CLI tests
run: cargo test --package trusted-server-cli --target x86_64-unknown-linux-gnu
run: ./scripts/test-cli.sh

- name: Verify Fastly WASM release build
env:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/pkg
/target
/crates/integration-tests/target
/dist/prebid/

# env
.env*
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ cargo test --workspace
# Run host-target CLI tests (workspace default target is wasm32-wasip1)
# Use your host triple, for example x86_64-unknown-linux-gnu on CI/Linux
# or aarch64-apple-darwin on Apple Silicon macOS.
# Use the local helper (recommended):
# ./scripts/test-cli.sh
cargo test --package trusted-server-cli --target <host-triple>

# Format
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ temp-env = "0.3.6"
tempfile = "3.24"
tokio = { version = "1.49", features = ["sync", "macros", "io-util", "rt", "time"] }
toml = "1.0"
toml_edit = "0.23.10"
trusted-server-core = { path = "crates/trusted-server-core" }
url = "2.5.8"
urlencoding = "2.1"
Expand Down
245 changes: 7 additions & 238 deletions crates/js/lib/build-all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,242 +8,24 @@
* tsjs-core.js — core API (always included)
* tsjs-<integration>.js — one per discovered integration
*
* Environment variables:
* TSJS_PREBID_ADAPTERS — Comma-separated list of Prebid.js bid adapter
* names to include in the bundle (e.g. "rubicon,appnexus,openx").
* Each name must have a corresponding {name}BidAdapter.js module in
* the prebid.js package. Default: no adapters.
*
* TSJS_PREBID_USER_ID_MODULES — Ignored for production builds. User ID
* modules are selected from src/integrations/prebid/user_id_modules.json
* so attested bundles are deterministic. For local experiments only, use
* TSJS_PREBID_USER_ID_MODULES_DEV_OVERRIDE.
* Prebid is intentionally excluded from this embedded build. Use
* build-prebid-external.mjs to generate publisher-specific Prebid bundles
* outside the Cargo build.
*/

import crypto from 'node:crypto';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { build } from 'vite';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
const srcDir = path.resolve(__dirname, 'src');
const distDir = path.resolve(__dirname, '..', 'dist');
const integrationsDir = path.join(srcDir, 'integrations');

// ---------------------------------------------------------------------------
// Prebid adapter generation
// ---------------------------------------------------------------------------

const DEFAULT_PREBID_ADAPTERS = '';
const DEFAULT_PREBID_ADAPTERS_DESCRIPTION = DEFAULT_PREBID_ADAPTERS || 'no adapters';
const ADAPTERS_FILE = path.join(integrationsDir, 'prebid', '_adapters.generated.ts');
const USER_IDS_FILE = path.join(integrationsDir, 'prebid', '_user_ids.generated.ts');

const USER_ID_REGISTRY_FILE = path.join(integrationsDir, 'prebid', 'user_id_modules.json');
const USER_IDS_MANIFEST_FILE = path.join(distDir, 'prebid-user-id-modules.json');
const LIVE_INTENT_SHIM_ALIAS = 'prebid.js/modules/liveIntentIdSystem.js';
const PREBID_PACKAGE_DIR = path.join(__dirname, 'node_modules', 'prebid.js');
const PREBID_LIVE_INTENT_STANDARD = path.join(
PREBID_PACKAGE_DIR,
'dist',
'src',
'libraries',
'liveIntentId',
'idSystem.js'
);
const PREBID_GLOBAL_MODULE = path.join(PREBID_PACKAGE_DIR, 'dist', 'src', 'src', 'prebidGlobal.js');
const LIVE_INTENT_SHIM = path.join(
integrationsDir,
'prebid',
'prebid_modules',
'liveIntentIdSystem.ts'
);

/**
* Generate `_adapters.generated.ts` with import statements for each adapter
* listed in the TSJS_PREBID_ADAPTERS environment variable.
*
* Invalid adapter names (those without a matching module in prebid.js) are
* logged and skipped.
*/
function generatePrebidAdapters() {
const raw = process.env.TSJS_PREBID_ADAPTERS ?? DEFAULT_PREBID_ADAPTERS;
const names = raw
.split(',')
.map((s) => s.trim())
.filter(Boolean);

const modulesDir = path.join(__dirname, 'node_modules', 'prebid.js', 'modules');

// Validate each adapter and build import lines
const imports = [];
for (const name of names) {
const moduleFile = `${name}BidAdapter.js`;
const modulePath = path.join(modulesDir, moduleFile);
if (!fs.existsSync(modulePath)) {
console.error(
`[build-all] WARNING: Prebid adapter "${name}" not found (expected ${moduleFile}), skipping`
);
continue;
}
imports.push(`import 'prebid.js/modules/${moduleFile}';`);
}

if (imports.length === 0) {
if (names.length === 0) {
console.log(
'[build-all] No Prebid adapters configured; bundle will have no client-side adapters'
);
} else {
console.error(
'[build-all] WARNING: No valid Prebid adapters found, bundle will have no client-side adapters'
);
}
}

const header = [
'// Auto-generated by build-all.mjs — manual edits will be overwritten at build time.',
'//',
'// Controls which Prebid.js bid adapters are included in the bundle.',
'// Set the TSJS_PREBID_ADAPTERS environment variable to a comma-separated list',
'// of adapter names (e.g. "rubicon,appnexus,openx") before building.',
`// Default: ${DEFAULT_PREBID_ADAPTERS_DESCRIPTION}`,
].join('\n');
const content = imports.length === 0 ? `${header}\n` : `${header}\n\n${imports.join('\n')}\n`;

fs.writeFileSync(ADAPTERS_FILE, content);

const adapterNames = names.filter((name) =>
fs.existsSync(path.join(modulesDir, `${name}BidAdapter.js`))
);
console.log('[build-all] Prebid adapters:', adapterNames);
}

function readUserIdRegistry() {
return JSON.parse(fs.readFileSync(USER_ID_REGISTRY_FILE, 'utf8'));
}

function requireExistingFile(filePath, description) {
if (!fs.existsSync(filePath)) {
throw new Error(`[build-all] Missing ${description}: ${filePath}`);
}
}

function prebidPackageVersion() {
const packageJsonPath = path.join(PREBID_PACKAGE_DIR, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
return packageJson.version;
}

function sourceToModuleMap(entries) {
const map = {};
for (const entry of entries) {
for (const source of entry.eidSources ?? []) {
map[source] = entry.moduleName;
}
}
return map;
}

function validateUserIdImport(entry) {
requireExistingFile(LIVE_INTENT_SHIM, 'LiveIntent ESM shim');
requireExistingFile(PREBID_LIVE_INTENT_STANDARD, 'Prebid LiveIntent standard ESM module');
requireExistingFile(PREBID_GLOBAL_MODULE, 'Prebid global module');

if (entry.moduleName === 'liveIntentIdSystem') {
return;
}

try {
require.resolve(entry.importPath, { paths: [__dirname] });
} catch (error) {
throw new Error(
`[build-all] Required Prebid user ID module "${entry.moduleName}" could not be resolved from ${entry.importPath}: ${error.message}`
);
}
}

/**
* Generate `_user_ids.generated.ts` with deterministic User ID imports.
*
* Production builds intentionally ignore TSJS_PREBID_USER_ID_MODULES so the
* attested JS artifact does not vary per publisher. A dev-only override exists
* for local experiments and should not be used for trusted deployments.
*/
function generatePrebidUserIdModules() {
const registry = readUserIdRegistry();
const entriesByModule = new Map(registry.modules.map((entry) => [entry.moduleName, entry]));
const override = process.env.TSJS_PREBID_USER_ID_MODULES_DEV_OVERRIDE;
const moduleNames = override
? override
.split(',')
.map((s) => s.trim())
.filter(Boolean)
: registry.defaultPreset;

if (process.env.TSJS_PREBID_USER_ID_MODULES && !override) {
console.warn(
'[build-all] TSJS_PREBID_USER_ID_MODULES is ignored for deterministic attested builds. ' +
'Use TSJS_PREBID_USER_ID_MODULES_DEV_OVERRIDE only for local experiments.'
);
}

if (override) {
console.warn(
'[build-all] WARNING: using TSJS_PREBID_USER_ID_MODULES_DEV_OVERRIDE. ' +
'This changes the Prebid bundle and breaks production attestation assumptions.'
);
}

const selectedEntries = moduleNames.map((moduleName) => {
const entry = entriesByModule.get(moduleName);
if (!entry) {
throw new Error(`[build-all] Unknown Prebid user ID module in preset: ${moduleName}`);
}
validateUserIdImport(entry);
return entry;
});

const imports = selectedEntries.map((entry) => `import '${entry.importPath}';`);

const content = [
'// Auto-generated by build-all.mjs — manual edits will be overwritten at build time.',
'//',
'// Deterministic Prebid.js user ID module preset for attested builds.',
'// TSJS_PREBID_USER_ID_MODULES is intentionally ignored in production builds.',
'// Use TSJS_PREBID_USER_ID_MODULES_DEV_OVERRIDE only for local experiments.',
`// Modules: ${moduleNames.join(', ')}`,
'',
...imports,
'',
].join('\n');

fs.writeFileSync(USER_IDS_FILE, content);

const manifest = {
prebidVersion: prebidPackageVersion(),
deterministic: !override,
modules: moduleNames,
sourceToModule: sourceToModuleMap(registry.modules),
generatedFileHash: crypto.createHash('sha256').update(content).digest('hex'),
};

console.log('[build-all] Prebid user ID modules:', moduleNames);
return manifest;
}

generatePrebidAdapters();
const prebidUserIdManifest = generatePrebidUserIdModules();

// ---------------------------------------------------------------------------

// Clean dist directory
fs.rmSync(distDir, { recursive: true, force: true });
fs.mkdirSync(distDir, { recursive: true });
fs.writeFileSync(USER_IDS_MANIFEST_FILE, `${JSON.stringify(prebidUserIdManifest, null, 2)}\n`);

// Discover integration modules: directories in src/integrations/ with index.ts
const integrationModules = fs.existsSync(integrationsDir)
Expand All @@ -252,11 +34,12 @@ const integrationModules = fs.existsSync(integrationsDir)
.filter((name) => {
const fullPath = path.join(integrationsDir, name);
return (
fs.statSync(fullPath).isDirectory() && fs.existsSync(path.join(fullPath, 'index.ts'))
name !== 'prebid' &&
fs.statSync(fullPath).isDirectory() &&
fs.existsSync(path.join(fullPath, 'index.ts'))
);
})
.sort()
: [];
: [];

console.log('[build-all] Discovered integrations:', integrationModules);

Expand All @@ -268,20 +51,6 @@ async function buildModule(name, entryPath) {
await build({
configFile: false,
root: __dirname,
resolve: {
alias: {
[LIVE_INTENT_SHIM_ALIAS]: LIVE_INTENT_SHIM,
'prebid.js/modules/liveIntentIdSystem': LIVE_INTENT_SHIM,
'tsjs-prebid/liveIntentIdSystemStandard': PREBID_LIVE_INTENT_STANDARD,
'tsjs-prebid/prebidGlobal': PREBID_GLOBAL_MODULE,
// prebid.js doesn't expose src/adapterManager.js via its package
// "exports" map, but we need it for client-side bidder validation.
'prebid.js/src/adapterManager.js': path.resolve(
__dirname,
'node_modules/prebid.js/dist/src/src/adapterManager.js'
),
},
},
build: {
emptyOutDir: false,
outDir: distDir,
Expand Down
Loading
Loading