('../../src/core/render');
- return {
- ...actual,
- renderCreativeIntoSlot: (slotId: string, html: string) => renderMock(slotId, html),
- };
- });
-
- (globalThis as any).fetch = vi.fn().mockRejectedValue(new Error('network-error'));
-
- const { addAdUnits } = await import('../../src/core/registry');
- const { setConfig } = await import('../../src/core/config');
- const { requestAds } = await import('../../src/core/request');
-
- addAdUnits({ code: 'slot1', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any);
- setConfig({ mode: 'thirdParty' } as any);
-
- requestAds();
- await Promise.resolve();
- await Promise.resolve();
-
- expect((globalThis as any).fetch).toHaveBeenCalled();
- expect(renderMock).not.toHaveBeenCalled();
- });
-
- it('inserts an iframe with creative HTML from unified auction', async () => {
- // mock fetch for unified auction endpoint - returns inline HTML
- const creativeHtml = '
Ad';
- (globalThis as any).fetch = vi.fn().mockResolvedValue({
- ok: true,
- status: 200,
- headers: { get: () => 'application/json' },
- json: async () => ({
- seatbid: [{ bid: [{ impid: 'slot1', adm: creativeHtml }] }],
- }),
- });
-
- const { addAdUnits } = await import('../../src/core/registry');
- const { requestAds } = await import('../../src/core/request');
-
- // Prepare slot in DOM
- const div = document.createElement('div');
- div.id = 'slot1';
- document.body.appendChild(div);
-
- // Add an ad unit and request
- addAdUnits({ code: 'slot1', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any);
- requestAds();
-
- await Promise.resolve();
- await Promise.resolve();
-
- // Verify iframe was inserted with creative HTML in srcdoc
- const iframe = document.querySelector('#slot1 iframe') as HTMLIFrameElement | null;
- expect(iframe).toBeTruthy();
- expect(iframe!.srcdoc).toContain(creativeHtml);
- });
-
- it('skips iframe insertion when slot is missing', async () => {
- // mock fetch for unified auction endpoint - returns inline HTML
- (globalThis as any).fetch = vi.fn().mockResolvedValue({
- ok: true,
- status: 200,
- headers: { get: () => 'application/json' },
- json: async () => ({
- seatbid: [
- {
- bid: [{ impid: 'missing-slot', adm: 'Creative for missing slot
' }],
- },
- ],
- }),
- });
-
- const { addAdUnits } = await import('../../src/core/registry');
- const { requestAds } = await import('../../src/core/request');
-
- addAdUnits({ code: 'missing-slot', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any);
- requestAds();
-
- await Promise.resolve();
- await Promise.resolve();
-
- // No iframe should be inserted because the slot isn't present in DOM
- const iframe = document.querySelector('iframe');
- expect(iframe).toBeNull();
- });
-});
diff --git a/crates/js/lib/test/integrations/creative/proxy_sign.test.ts b/crates/js/lib/test/integrations/creative/proxy_sign.test.ts
index 41c86a87..412f4435 100644
--- a/crates/js/lib/test/integrations/creative/proxy_sign.test.ts
+++ b/crates/js/lib/test/integrations/creative/proxy_sign.test.ts
@@ -17,6 +17,11 @@ describe('creative/proxy_sign.ts', () => {
expect(shouldProxyExternalUrl('http://cdn.example/pixel.gif')).toBe(true);
});
+ it('flags protocol-relative URLs with ports for proxying', () => {
+ // Protocol-relative URL with custom port should be proxyable
+ expect(shouldProxyExternalUrl('//local.example.com:9443/static/img/300x250.svg')).toBe(true);
+ });
+
it('rejects data, javascript, and same-origin URLs', () => {
expect(shouldProxyExternalUrl('data:image/png;base64,AAAA')).toBe(false);
expect(shouldProxyExternalUrl('javascript:alert(1)')).toBe(false);
@@ -51,4 +56,26 @@ describe('creative/proxy_sign.ts', () => {
const result = await signProxyUrl('https://cdn.example/asset.js');
expect(result).toBeNull();
});
+
+ it('preserves port in protocol-relative URL when signing', async () => {
+ // When signing a protocol-relative URL with a custom port,
+ // the port should be preserved in the absolute URL sent to the sign endpoint
+ // Note: The scheme comes from location.href, which is http: in test env
+ const signed =
+ '/first-party/proxy?tsurl=http%3A%2F%2Flocal.example.com%3A9443%2Fstatic%2Fimg%2Ftest.svg&tstoken=tok';
+ const fetchMock = vi.fn().mockResolvedValue({
+ ok: true,
+ json: async () => ({ href: signed }),
+ });
+ global.fetch = fetchMock as unknown as typeof fetch;
+
+ await signProxyUrl('//local.example.com:9443/static/img/test.svg');
+
+ // Verify the absolute URL passed to fetch includes the port
+ // In test env, location.href uses http, so the resolved URL uses http
+ // The key thing is that :9443 port is preserved
+ const bodyArg = JSON.parse((fetchMock.mock.calls[0][1] as RequestInit).body as string);
+ expect(bodyArg.url).toContain('local.example.com:9443');
+ expect(bodyArg.url).toBe('http://local.example.com:9443/static/img/test.svg');
+ });
});
diff --git a/crates/js/lib/test/integrations/ext/prebidjs.test.ts b/crates/js/lib/test/integrations/ext/prebidjs.test.ts
deleted file mode 100644
index c28fdc38..00000000
--- a/crates/js/lib/test/integrations/ext/prebidjs.test.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
-
-import installPrebidJsShim from '../../../src/integrations/ext/prebidjs';
-
-const ORIGINAL_WINDOW = global.window;
-
-function createWindow() {
- return {
- tsjs: undefined as any,
- pbjs: undefined as any,
- } as Window & { tsjs?: any; pbjs?: any };
-}
-
-describe('ext/prebidjs', () => {
- let testWindow: ReturnType;
-
- beforeEach(() => {
- testWindow = createWindow();
- Object.assign(globalThis as any, { window: testWindow });
- });
-
- afterEach(() => {
- Object.assign(globalThis as any, { window: ORIGINAL_WINDOW });
- vi.restoreAllMocks();
- });
-
- it('installs shim, aliases pbjs to tsjs, and shares queue', () => {
- const result = installPrebidJsShim();
- expect(result).toBe(true);
- expect(testWindow.pbjs).toBe(testWindow.tsjs);
- expect(Array.isArray(testWindow.tsjs!.que)).toBe(true);
- });
-
- it('flushes queued pbjs callbacks', () => {
- const callback = vi.fn();
- testWindow.pbjs = { que: [callback] } as any;
-
- installPrebidJsShim();
-
- expect(callback).toHaveBeenCalled();
- expect(testWindow.pbjs).toBe(testWindow.tsjs);
- });
-
- it('ensures shared queue and requestBids shim delegates to requestAds', () => {
- installPrebidJsShim();
-
- const api = testWindow.tsjs!;
- api.requestAds = vi.fn();
- const requestBids = testWindow.pbjs!.requestBids.bind(testWindow.pbjs);
-
- const callback = vi.fn();
- requestBids(callback);
- expect(api.requestAds).toHaveBeenCalledWith(callback, undefined);
-
- requestBids({ timeout: 100 } as any);
- expect(api.requestAds).toHaveBeenCalledWith({ timeout: 100 });
- });
-});
diff --git a/crates/js/lib/test/integrations/gam/index.test.ts b/crates/js/lib/test/integrations/gam/index.test.ts
new file mode 100644
index 00000000..8445267c
--- /dev/null
+++ b/crates/js/lib/test/integrations/gam/index.test.ts
@@ -0,0 +1,121 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+
+describe('integrations/gam', () => {
+ beforeEach(async () => {
+ await vi.resetModules();
+ // Clean up window globals
+ delete (window as any).tsGamConfig;
+ delete (window as any).__tsGamInstalled;
+ delete (window as any).googletag;
+ delete (window as any).pbjs;
+ });
+
+ it('exports setGamConfig and getGamConfig', async () => {
+ const { setGamConfig, getGamConfig } = await import('../../../src/integrations/gam/index');
+ expect(typeof setGamConfig).toBe('function');
+ expect(typeof getGamConfig).toBe('function');
+ });
+
+ it('setGamConfig updates config', async () => {
+ const { setGamConfig, getGamConfig } = await import('../../../src/integrations/gam/index');
+
+ setGamConfig({ enabled: true, bidders: ['mocktioneer'], forceRender: true });
+ const cfg = getGamConfig();
+
+ expect(cfg.enabled).toBe(true);
+ expect(cfg.bidders).toEqual(['mocktioneer']);
+ expect(cfg.forceRender).toBe(true);
+ });
+
+ it('getGamConfig returns defaults when not configured', async () => {
+ const { getGamConfig } = await import('../../../src/integrations/gam/index');
+ const cfg = getGamConfig();
+
+ expect(cfg.enabled).toBe(false);
+ expect(cfg.bidders).toEqual([]);
+ expect(cfg.forceRender).toBe(false);
+ });
+
+ it('exports tsGam API object', async () => {
+ const { tsGam } = await import('../../../src/integrations/gam/index');
+
+ expect(tsGam).toBeDefined();
+ expect(typeof tsGam.setConfig).toBe('function');
+ expect(typeof tsGam.getConfig).toBe('function');
+ expect(typeof tsGam.getStats).toBe('function');
+ });
+
+ it('getStats returns initial empty stats', async () => {
+ const { getGamStats } = await import('../../../src/integrations/gam/index');
+ const stats = getGamStats();
+
+ expect(stats.intercepted).toBe(0);
+ expect(stats.rendered).toEqual([]);
+ });
+
+ it('picks up window.tsGamConfig on init', async () => {
+ // Set config before importing
+ (window as any).tsGamConfig = {
+ enabled: true,
+ bidders: ['test-bidder'],
+ forceRender: false,
+ };
+
+ const { getGamConfig } = await import('../../../src/integrations/gam/index');
+ const cfg = getGamConfig();
+
+ expect(cfg.enabled).toBe(true);
+ expect(cfg.bidders).toEqual(['test-bidder']);
+ });
+
+ it('partial config updates preserve existing values', async () => {
+ const { setGamConfig, getGamConfig } = await import('../../../src/integrations/gam/index');
+
+ setGamConfig({ enabled: true, bidders: ['bidder1'], forceRender: false });
+ setGamConfig({ forceRender: true }); // Only update forceRender
+
+ const cfg = getGamConfig();
+ expect(cfg.enabled).toBe(true);
+ expect(cfg.bidders).toEqual(['bidder1']);
+ expect(cfg.forceRender).toBe(true);
+ });
+
+ describe('extractIframeSrc', () => {
+ it('extracts src from simple iframe tag', async () => {
+ const { extractIframeSrc } = await import('../../../src/integrations/gam/index');
+
+ const html =
+ '';
+ expect(extractIframeSrc(html)).toBe('/first-party/proxy?tsurl=https://example.com');
+ });
+
+ it('handles trailing newline (mocktioneer style)', async () => {
+ const { extractIframeSrc } = await import('../../../src/integrations/gam/index');
+
+ const html =
+ '\n';
+ expect(extractIframeSrc(html)).toBe(
+ '/first-party/proxy?tsurl=https%3A%2F%2Flocal.mocktioneer.com'
+ );
+ });
+
+ it('returns null for non-iframe content', async () => {
+ const { extractIframeSrc } = await import('../../../src/integrations/gam/index');
+
+ expect(extractIframeSrc('not an iframe
')).toBeNull();
+ expect(extractIframeSrc('')).toBeNull();
+ });
+
+ it('returns null for iframe without src', async () => {
+ const { extractIframeSrc } = await import('../../../src/integrations/gam/index');
+
+ expect(extractIframeSrc('')).toBeNull();
+ });
+
+ it('returns null for complex content with iframe', async () => {
+ const { extractIframeSrc } = await import('../../../src/integrations/gam/index');
+
+ expect(extractIframeSrc('')).toBeNull();
+ });
+ });
+});
diff --git a/crates/js/lib/vite.config.ts b/crates/js/lib/vite.config.ts
index 8b9ed866..a6cc7005 100644
--- a/crates/js/lib/vite.config.ts
+++ b/crates/js/lib/vite.config.ts
@@ -84,8 +84,6 @@ export type ModuleName = ${finalModules.map((m) => `'${m}'`).join(' | ')};
export default defineConfig(() => {
const distDir = path.resolve(__dirname, '../dist');
- const buildTimestamp = new Date().toISOString();
- const banner = `// build: ${buildTimestamp}\n`;
return {
build: {
@@ -93,11 +91,11 @@ export default defineConfig(() => {
outDir: distDir,
assetsDir: '.',
sourcemap: false,
- minify: 'esbuild',
+ minify: 'esbuild' as const,
rollupOptions: {
input: path.resolve(__dirname, 'src/index.ts'),
output: {
- format: 'iife',
+ format: 'iife' as const,
dir: distDir,
entryFileNames: 'tsjs-unified.js',
inlineDynamicImports: true,
diff --git a/crates/js/src/bundle.rs b/crates/js/src/bundle.rs
index 3ad93d9a..3dda7da9 100644
--- a/crates/js/src/bundle.rs
+++ b/crates/js/src/bundle.rs
@@ -45,7 +45,8 @@ impl TsjsBundle {
}
}
- pub(crate) const fn bundle(self) -> &'static str {
+ /// Returns the JavaScript bundle content.
+ pub const fn bundle(self) -> &'static str {
METAS[self as usize].bundle
}
diff --git a/docs/guide/error-reference.md b/docs/guide/error-reference.md
index e1c91ecf..193c882b 100644
--- a/docs/guide/error-reference.md
+++ b/docs/guide/error-reference.md
@@ -659,7 +659,7 @@ fastly log-tail
fastly compute serve
# Test endpoint
-curl http://localhost:7676/first-party/ad?slot=test&w=300&h=250
+curl http://localhost:7676/ad/render?slot=test&w=300&h=250
```
---
diff --git a/docs/guide/what-is-trusted-server.md b/docs/guide/what-is-trusted-server.md
index 1a42b747..d0b7ec37 100644
--- a/docs/guide/what-is-trusted-server.md
+++ b/docs/guide/what-is-trusted-server.md
@@ -1,6 +1,6 @@
# What is Trusted Server?
-Trusted Server is an open-source, cloud based orchestration framework and runtime for publishers. It moves code execution and operations that traditionally occurs in browsers (via 3rd party JS) to secure, zero-cold-start WASM binaries running in WASI supported environments.
+Trusted Server is an open-source, cloud based orchestration framework and runtime for publishers. It moves code execution and operations that traditionally occurs in browsers (via external JS) to secure, zero-cold-start WASM binaries running in WASI supported environments.
Trusted Server is the new execution layer for the open-web, returning control of 1st party data, security, and overall user-experience back to publishers.