Skip to content

Commit e52ff39

Browse files
Add EC module with lifecycle management, consent gating, and config migration
- Add ec/ module with EcContext lifecycle, generation, cookies, and consent - Compute cookie domain from publisher.domain, move EC cookie helpers - Fix auction consent gating, restore cookie_domain for non-EC cookies - Add integration proxy revocation, refactor EC parsing, clean up ec_hash - Remove fresh_id and ec_fresh per EC spec §12.1 - Migrate [edge_cookie] config to [ec] per spec §14
1 parent c4981d4 commit e52ff39

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1216
-714
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ both runtime behavior and build/tooling changes.
366366
| `crates/trusted-server-core/src/tsjs.rs` | Script tag generation with module IDs |
367367
| `crates/trusted-server-core/src/html_processor.rs` | Injects `<script>` at `<head>` start |
368368
| `crates/trusted-server-core/src/publisher.rs` | `/static/tsjs=` handler, concatenates modules |
369-
| `crates/trusted-server-core/src/edge_cookie.rs` | Edge Cookie (EC) ID generation |
369+
| `crates/trusted-server-core/src/ec/` | EC identity subsystem (generation, consent, cookies) |
370370
| `crates/trusted-server-core/src/cookies.rs` | Cookie handling |
371371
| `crates/trusted-server-core/src/consent/mod.rs` | GDPR and broader consent management |
372372
| `crates/trusted-server-core/src/http_util.rs` | HTTP abstractions and request utilities |

PUBLISHER_IDS_AUDIT.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ This document lists all publisher-specific IDs and configurations found in the c
2020
- `cookie_domain = ".test-publisher.com"` (line 3)
2121
- `origin_url = "https://origin.test-publisher.com"` (line 4)
2222

23-
**KV Store Names:**
24-
*(Removed — `counter_store` and `opid_store` were removed in the EC rename; they were vestigial from the template-based SyntheticID generation.)*
23+
**KV Store Names (user-specific):**
24+
- `counter_store = "jevans_synth_id_counter"` (line 24)
25+
- `opid_store = "jevans_synth_id_opid"` (line 25)
2526

2627
## Hardcoded in Source Code
2728

crates/integration-tests/fixtures/configs/viceroy-template.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
[local_server.backends]
88

99
[local_server.kv_stores]
10-
[[local_server.kv_stores.creative_store]]
10+
# These inline placeholders satisfy Viceroy's local KV configuration
11+
# requirements without exercising KV-backed application behavior.
12+
[[local_server.kv_stores.creative_store]]
1113
key = "placeholder"
1214
data = "placeholder"
1315

crates/js/lib/package-lock.json

Lines changed: 137 additions & 134 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/js/lib/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"eslint": "^9.10.0",
2727
"eslint-config-prettier": "^10.1.8",
2828
"eslint-plugin-import": "^2.29.1",
29-
"eslint-plugin-jsdoc": "^62.8.0",
29+
"eslint-plugin-jsdoc": "^62.5.4",
3030
"eslint-plugin-unicorn": "^62.0.0",
3131
"jsdom": "^28.0.0",
3232
"prettier": "^3.2.5",

crates/js/lib/src/core/render.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,15 @@ import NORMALIZE_CSS from './styles/normalize.css?inline';
77
import IFRAME_TEMPLATE from './templates/iframe.html?raw';
88

99
// Sandbox permissions granted to creative iframes.
10-
// Ad creatives routinely contain scripts for tracking, click handling, and
11-
// viewability measurement, so allow-scripts and allow-same-origin are required
12-
// for creatives to render correctly. Server-side sanitization is the primary
13-
// defense against malicious markup; the sandbox provides defense-in-depth.
10+
// Notably absent:
11+
// allow-scripts, allow-same-origin — prevent JS execution and same-origin
12+
// access, which are the primary attack vectors for malicious creatives.
13+
// allow-forms — server-side sanitization strips <form> elements, so form
14+
// submission from creatives is not a supported use case. Omitting this token
15+
// is consistent with that server-side policy and reduces the attack surface.
1416
const CREATIVE_SANDBOX_TOKENS = [
15-
'allow-forms',
1617
'allow-popups',
1718
'allow-popups-to-escape-sandbox',
18-
'allow-same-origin',
19-
'allow-scripts',
2019
'allow-top-navigation-by-user-activation',
2120
] as const;
2221

crates/js/lib/src/integrations/prebid/index.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -269,13 +269,6 @@ export function installPrebidNpm(config?: Partial<PrebidNpmConfig>): typeof pbjs
269269
} else {
270270
unit.bids.push({ bidder: ADAPTER_CODE, params: tsParams });
271271
}
272-
273-
// Remove server-side bidder entries — they are now handled via the
274-
// trustedServer adapter. Only keep client-side bidders (which run via
275-
// their native Prebid.js adapters) and the trustedServer bid itself.
276-
unit.bids = unit.bids.filter(
277-
(b) => b.bidder === ADAPTER_CODE || clientSideBidders.has(b.bidder ?? '')
278-
);
279272
}
280273

281274
// Ensure the trustedServer adapter is allowed to return bids under any

crates/js/lib/test/core/render.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ describe('render', () => {
2727
expect(iframe.srcdoc).toContain('<span>ad</span>');
2828
expect(div.querySelector('iframe')).toBe(iframe);
2929
const sandbox = iframe.getAttribute('sandbox') ?? '';
30-
expect(sandbox).toContain('allow-forms');
30+
expect(sandbox).not.toContain('allow-forms');
3131
expect(sandbox).toContain('allow-popups');
3232
expect(sandbox).toContain('allow-popups-to-escape-sandbox');
3333
expect(sandbox).toContain('allow-top-navigation-by-user-activation');
34-
expect(sandbox).toContain('allow-same-origin');
35-
expect(sandbox).toContain('allow-scripts');
34+
expect(sandbox).not.toContain('allow-same-origin');
35+
expect(sandbox).not.toContain('allow-scripts');
3636
});
3737

3838
it('preserves dollar sequences when building the creative document', async () => {

crates/js/lib/test/integrations/prebid/index.test.ts

Lines changed: 24 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -411,13 +411,14 @@ describe('prebid/installPrebidNpm', () => {
411411
];
412412
pbjs.requestBids({ adUnits } as any);
413413

414-
// Each ad unit should only have trustedServer — original bidders are absorbed
414+
// Each ad unit should have trustedServer added
415415
for (const unit of adUnits) {
416-
expect(unit.bids).toHaveLength(1);
417-
expect(unit.bids[0].bidder).toBe('trustedServer');
416+
const hasTsBidder = unit.bids.some((b: any) => b.bidder === 'trustedServer');
417+
expect(hasTsBidder).toBe(true);
418418
}
419419

420-
expect(adUnits[0].bids[0].params.bidderParams).toEqual({ appnexus: {} });
420+
const trustedServerBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer');
421+
expect(trustedServerBid.params.bidderParams).toEqual({ appnexus: {} });
421422

422423
// Should call through to original requestBids
423424
expect(mockRequestBids).toHaveBeenCalled();
@@ -433,7 +434,7 @@ describe('prebid/installPrebidNpm', () => {
433434
expect(tsCount).toBe(1);
434435
});
435436

436-
it('captures per-bidder params on trustedServer bid and removes originals', () => {
437+
it('captures per-bidder params on trustedServer bid', () => {
437438
const pbjs = installPrebidNpm();
438439

439440
const adUnits = [
@@ -446,10 +447,8 @@ describe('prebid/installPrebidNpm', () => {
446447
];
447448
pbjs.requestBids({ adUnits } as any);
448449

449-
// Only trustedServer should remain — original bidders are absorbed
450-
expect(adUnits[0].bids).toHaveLength(1);
451-
const trustedServerBid = adUnits[0].bids[0] as any;
452-
expect(trustedServerBid.bidder).toBe('trustedServer');
450+
const trustedServerBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer');
451+
expect(trustedServerBid).toBeDefined();
453452
expect(trustedServerBid.params.bidderParams).toEqual({
454453
appnexus: { placementId: 123 },
455454
rubicon: { accountId: 'abc' },
@@ -483,12 +482,11 @@ describe('prebid/installPrebidNpm', () => {
483482
];
484483
pbjs.requestBids({ adUnits } as any);
485484

486-
// Original kargo bids should be removed, only trustedServer remains
487-
expect(adUnits[0].bids).toHaveLength(1);
488-
expect(adUnits[0].bids[0].params.zone).toBe('header');
485+
const tsBid0 = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any;
486+
expect(tsBid0.params.zone).toBe('header');
489487

490-
expect(adUnits[1].bids).toHaveLength(1);
491-
expect(adUnits[1].bids[0].params.zone).toBe('fixed_bottom');
488+
const tsBid1 = adUnits[1].bids.find((b: any) => b.bidder === 'trustedServer') as any;
489+
expect(tsBid1.params.zone).toBe('fixed_bottom');
492490
});
493491

494492
it('omits zone when mediaTypes.banner.name is not set', () => {
@@ -503,8 +501,8 @@ describe('prebid/installPrebidNpm', () => {
503501
];
504502
pbjs.requestBids({ adUnits } as any);
505503

506-
expect(adUnits[0].bids).toHaveLength(1);
507-
expect(adUnits[0].bids[0].params.zone).toBeUndefined();
504+
const tsBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any;
505+
expect(tsBid.params.zone).toBeUndefined();
508506
});
509507

510508
it('omits zone when ad unit has no mediaTypes', () => {
@@ -513,8 +511,8 @@ describe('prebid/installPrebidNpm', () => {
513511
const adUnits = [{ bids: [{ bidder: 'rubicon', params: {} }] }];
514512
pbjs.requestBids({ adUnits } as any);
515513

516-
expect(adUnits[0].bids).toHaveLength(1);
517-
expect(adUnits[0].bids[0].params.zone).toBeUndefined();
514+
const tsBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any;
515+
expect(tsBid.params.zone).toBeUndefined();
518516
});
519517

520518
it('clears stale zone when existing trustedServer bid is reused', () => {
@@ -551,10 +549,10 @@ describe('prebid/installPrebidNpm', () => {
551549
mockPbjs.adUnits = [{ bids: [{ bidder: 'openx', params: {} }] }] as any[];
552550
pbjs.requestBids({} as any);
553551

554-
// Original openx bid should be removed, only trustedServer remains
555-
const unit = mockPbjs.adUnits[0] as any;
556-
expect(unit.bids).toHaveLength(1);
557-
expect(unit.bids[0].bidder).toBe('trustedServer');
552+
const hasTsBidder = (mockPbjs.adUnits[0] as any).bids.some(
553+
(b: any) => b.bidder === 'trustedServer'
554+
);
555+
expect(hasTsBidder).toBe(true);
558556
});
559557
});
560558
});
@@ -613,7 +611,7 @@ describe('prebid/client-side bidders', () => {
613611
delete (window as any).__tsjs_prebid;
614612
});
615613

616-
it('excludes client-side bidders from trustedServer bidderParams and removes server-side bids', () => {
614+
it('excludes client-side bidders from trustedServer bidderParams', () => {
617615
(window as any).__tsjs_prebid = { clientSideBidders: ['rubicon'] };
618616

619617
const pbjs = installPrebidNpm();
@@ -629,18 +627,13 @@ describe('prebid/client-side bidders', () => {
629627
];
630628
pbjs.requestBids({ adUnits } as any);
631629

632-
// Only rubicon (client-side) and trustedServer should remain
633-
expect(adUnits[0].bids).toHaveLength(2);
634630
const tsBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any;
635631
expect(tsBid).toBeDefined();
636632
// rubicon should NOT be in bidderParams — it runs client-side
637633
expect(tsBid.params.bidderParams).toEqual({
638634
appnexus: { placementId: 123 },
639635
kargo: { placementId: 'k1' },
640636
});
641-
// appnexus and kargo should be removed (absorbed into trustedServer)
642-
expect(adUnits[0].bids.find((b: any) => b.bidder === 'appnexus')).toBeUndefined();
643-
expect(adUnits[0].bids.find((b: any) => b.bidder === 'kargo')).toBeUndefined();
644637
});
645638

646639
it('preserves client-side bidder bids as standalone entries', () => {
@@ -680,8 +673,6 @@ describe('prebid/client-side bidders', () => {
680673
];
681674
pbjs.requestBids({ adUnits } as any);
682675

683-
// 3 bids: rubicon (client-side), openx (client-side), trustedServer
684-
expect(adUnits[0].bids).toHaveLength(3);
685676
const tsBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any;
686677
// Only appnexus should be in bidderParams
687678
expect(tsBid.params.bidderParams).toEqual({
@@ -691,9 +682,6 @@ describe('prebid/client-side bidders', () => {
691682
// Both client-side bidders should remain
692683
expect(adUnits[0].bids.find((b: any) => b.bidder === 'rubicon')).toBeDefined();
693684
expect(adUnits[0].bids.find((b: any) => b.bidder === 'openx')).toBeDefined();
694-
695-
// Server-side bidder should be removed
696-
expect(adUnits[0].bids.find((b: any) => b.bidder === 'appnexus')).toBeUndefined();
697685
});
698686

699687
it('behaves normally when no client-side bidders are configured', () => {
@@ -710,10 +698,7 @@ describe('prebid/client-side bidders', () => {
710698
];
711699
pbjs.requestBids({ adUnits } as any);
712700

713-
// All original bidders should be removed, only trustedServer remains
714-
expect(adUnits[0].bids).toHaveLength(1);
715-
const tsBid = adUnits[0].bids[0] as any;
716-
expect(tsBid.bidder).toBe('trustedServer');
701+
const tsBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any;
717702
expect(tsBid.params.bidderParams).toEqual({
718703
appnexus: { placementId: 123 },
719704
rubicon: { accountId: 'abc' },
@@ -735,10 +720,7 @@ describe('prebid/client-side bidders', () => {
735720
];
736721
pbjs.requestBids({ adUnits } as any);
737722

738-
// All original bidders should be removed, only trustedServer remains
739-
expect(adUnits[0].bids).toHaveLength(1);
740-
const tsBid = adUnits[0].bids[0] as any;
741-
expect(tsBid.bidder).toBe('trustedServer');
723+
const tsBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any;
742724
expect(tsBid.params.bidderParams).toEqual({
743725
appnexus: { placementId: 123 },
744726
rubicon: { accountId: 'abc' },
@@ -760,8 +742,7 @@ describe('prebid/client-side bidders', () => {
760742
];
761743
pbjs.requestBids({ adUnits } as any);
762744

763-
// All 3 should be present: rubicon, appnexus (both client-side), and trustedServer
764-
expect(adUnits[0].bids).toHaveLength(3);
745+
// trustedServer should still be present (even with empty bidderParams)
765746
const tsBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any;
766747
expect(tsBid).toBeDefined();
767748
expect(tsBid.params.bidderParams).toEqual({});

crates/trusted-server-core/README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,11 @@ Behavior is covered by an extensive test suite in `crates/trusted-server-core/sr
4949

5050
## Edge Cookie (EC) Identifier Propagation
5151

52-
- `edge_cookie.rs` generates an edge cookie identifier per user request and exposes helpers:
53-
- `generate_ec_id` — creates a fresh HMAC-based ID using the client IP address and appends a short random suffix (format: `64hex.6alnum`).
54-
- `get_ec_id` — extracts an existing ID from the `x-ts-ec` header or `ts-ec` cookie.
55-
- `get_or_generate_ec_id` — reuses the existing ID when present, otherwise creates one.
52+
- The `ec/` module owns the EC identity subsystem:
53+
- `ec/generation.rs` — creates HMAC-based IDs using the client IP and publisher passphrase (format: `64hex.6alnum`).
54+
- `ec/mod.rs``EcContext` struct with two-phase lifecycle (`read_from_request` + `generate_if_needed`), `get_ec_id` helper.
55+
- `ec/consent.rs` — EC-specific consent gating wrapper.
56+
- `ec/cookies.rs``Set-Cookie` header creation and expiration helpers.
5657
- `publisher.rs::handle_publisher_request` stamps proxied origin responses with `x-ts-ec`, and (when absent) issues the `ts-ec` cookie so the browser keeps the identifier on subsequent requests.
5758
- `proxy.rs::handle_first_party_proxy` replays the identifier to third-party creative origins by appending `ts-ec=<value>` to the reconstructed target URL, follows redirects (301/302/303/307/308) up to four hops, and keeps downstream fetches linked to the same user scope.
5859
- `proxy.rs::handle_first_party_click` adds `ts-ec=<value>` to outbound click redirect URLs so analytics endpoints can associate clicks with impressions without third-party cookies.

0 commit comments

Comments
 (0)