Add Edge Cookie (EC) sync support for trusted-server integration#98
Add Edge Cookie (EC) sync support for trusted-server integration#98ChristianPavilonis wants to merge 7 commits intomainfrom
Conversation
Implement mocktioneer as a full EC sync partner with three new endpoints: - GET /sync/start: pixel sync redirect chain (sets mtkid, redirects to TS /sync) - GET /sync/done: callback endpoint completing the redirect chain - GET /resolve: S2S pull sync resolution (deterministic UID from ec_hash+IP) Also adds OpenRTB 2.6 user.eids support and EC identity metadata in creatives, enabling end-to-end demo of the trusted-server EC identity pipeline. Security hardening from review: - Constant-time token comparison via subtle::ConstantTimeEq (SHA-256 digest) - Hostname validation on ts_domain (rejects path/auth/port injection) - Domain allowlist via MOCKTIONEER_TS_DOMAINS env var - Hex-only ec_hash validation - Log sanitization for user-supplied values - Deterministic mtkid generation (SHA-256 of host, no randomness)
… for configurable bind address
Prebid Server places eids under user.ext.eids (OpenRTB 2.5) rather than the top-level user.eids (OpenRTB 2.6). extract_ec_info() now checks both locations, with top-level taking priority when both are present.
…ntegration Document the three new EC endpoints (/sync/start, /sync/done, /resolve), the trusted-server integration guide with partner registration walkthrough, and update existing pages (API overview, tracking, creatives, configuration) to reflect deterministic mtkid generation and new route/env var additions.
…d creative metadata
The /resolve endpoint now accepts ec_id in {64-hex}.{6-alnum} format
instead of the bare 64-hex ec_hash prefix. The hash is extracted
internally via extract_ec_hash(). EdgeCookieInfo collapses ec_value
and ec_hash into a single ec_id field. Docs updated to match.
aram356
left a comment
There was a problem hiding this comment.
PR Review
Summary
Solid EC sync implementation with good security practices (constant-time auth, open redirect protection, log sanitization, deterministic UIDs). Two items need resolution before merge.
Findings
🔧 Needs Change
cargo fmtfailure: import ordering inroutes.rs:21—use crate::render::extract_ec_hashis out of alphabetical order. Runcargo fmt.- edgezero deps on feature branch: all five
edgezero-*deps inCargo.toml:24-28point atbranch = "feature/configureable-axum-host"instead ofmain. Merging this puts mocktioneer on an unmerged upstream branch.
❓ Questions
ts_syncedunvalidated:SyncDoneParamsderivesValidatebutts_syncedhas no constraint — any non-"1"value silently means failure. Intentional? (routes.rs:599)
🤔 Thoughts
- Auth fails open on WASM:
std::env::varreturnsErron Cloudflare Workers, silently disabling pull-token auth and domain allowlists. Well-documented in code comments, but for production use, config injection (rather than env vars at request time) would be safer. Not blocking.
🌱 Seeds
ts_reasonunbounded: no max length validation on the query param (routes.rs:602). Low risk since HTTP servers cap query strings, but#[validate(length(max = ...))]would be defensive.
👍 Praise
- Constant-time token comparison (
routes.rs:854-858): SHA-256 both sides beforect_eq— correct approach, no length leak. - Open redirect protection (
routes.rs:641-663): two-layer defense (hostname char validation + optional allowlist) with good injection test coverage. - EC hash extraction (
render.rs:60-70): strict{64-hex}.{6-alnum}format validation with single parse+validate function. - Deterministic UID generation (
routes.rs:794-801):SHA-256(ec_hash|ip)consistent with no-randomness principle. - Log sanitization (
routes.rs:876-881): control char stripping + truncation, used consistently for user-supplied values.
CI Status
- fmt: FAIL (import ordering)
- clippy: PASS
- tests: PASS (100 passed, 1 ignored)
| edgezero-adapter-fastly = { git = "https://github.com/stackpop/edgezero.git", branch = "main", package = "edgezero-adapter-fastly", default-features = false } | ||
| edgezero-cli = { git = "https://github.com/stackpop/edgezero.git", branch = "main", package = "edgezero-cli" } | ||
| edgezero-core = { git = "https://github.com/stackpop/edgezero.git", branch = "main", package = "edgezero-core" } | ||
| edgezero-adapter-axum = { git = "https://github.com/stackpop/edgezero.git", branch = "feature/configureable-axum-host", package = "edgezero-adapter-axum", default-features = false } |
There was a problem hiding this comment.
🔧 edgezero deps pinned to unmerged feature branch
All five edgezero-* dependencies point at branch = "feature/configureable-axum-host" instead of main. Merging this PR puts mocktioneer on an unmerged edgezero feature branch — if that branch is rebased, force-pushed, or deleted, CI breaks.
Suggested fix: either (a) merge the edgezero feature branch to main first and repoint these deps, or (b) split the dep bump into its own PR. If the feature branch is genuinely needed for this PR's functionality, document why in the PR description.
There was a problem hiding this comment.
Acknowledged. The edgezero feature/configureable-axum-host branch (PR stackpop/edgezero#231) needs to be merged to main first. Once that lands, we'll repoint all five deps to main. Leaving this unresolved until that happens.
- Add #[validate(length(min=1, max=1))] to ts_synced for early rejection - Add #[validate(length(max=256))] to ts_reason as defensive bound - Fix import ordering for cargo fmt compliance - Add mkt field extraction from user.eids[].uids[].ext.mkt - Log EC/EID info during OpenRTB auction for observability - Configure Fastly service_id and debug logging
aram356
left a comment
There was a problem hiding this comment.
PR Review
Summary
This PR adds EC sync support with 3 new endpoints, OpenRTB 2.6 EID types, and creative EC metadata. The security posture is strong (constant-time auth, log sanitization, cookie flags), but there are CI blockers and a few design questions that need addressing before merge.
Findings
🔧 Needs Change
cargo fmtfailure: Import ordering issue inroutes.rs— two separateuse crate::render::imports, out of alphabetical order. CI will reject. (routes.rs:21)- edgezero deps pinned to feature branch: All 5 edgezero workspace deps point to
branch = "feature/configureable-axum-host"instead ofmain. This PR can't merge cleanly unless that edgezero branch is merged first. The branch name also has a typo ("configureable" → "configurable"). If the branch is deleted or force-pushed, CI breaks for everyone. (Cargo.toml:24-28) std::env::remove_varunsound in tests: Multiple non-ignored tests callremove_varwhich is deprecated-unsafe since Rust 1.83 (project uses 1.91.1).cargo testruns in parallel — this is a data race. (routes.rs:1483-1695)
❓ Questions
- Deterministic
mtkid= same ID for ALL users on same host:mtkidis derived fromSHA-256("mtkid:" || host). Every visitor to the same host gets the exact same cookie value. This satisfies determinism but defeats the purpose of a buyer UID — trusted-server will see all users as the same buyer. Is this intentional for testing (predictable assertions)? If so, it should be documented prominently. (routes.rs:345-376)
🤔 Thoughts
ts_domainvalidation gaps: Character-class-based, not RFC-compliant. Accepts double-dot TLDs, dash-leading labels, and overlong labels. Acceptable for a mock bidder, but worth noting since the PR emphasizes "open-redirect protection." (routes.rs:575)- Auth silently disabled on Cloudflare Workers:
std::env::varreturnsErron CF Workers, so/resolveauth and domain allowlist are both silently disabled.register_partner.shsetsMOCKTIONEER_PULL_TOKENwhich creates a false sense of security for CF deployments. Consider a startup log warning on wasm32 targets. (routes.rs:764-767)
🌱 Seeds
- GET with query params for S2S
/resolve:ec_idandipappear in access/proxy logs. For S2S this is acceptable, but POST would be cleaner for a production sync API.
📝 Notes
ts_syncedaccepts any string: Only"1"means success; all other values silently treated as failure. (routes.rs:598)ResolveResponse.uidalwaysSome:Option<String>butNoneis never returned. Could simplify toString. (routes.rs:616)ipparam has no format validation: Any 1-45 char string accepted. No security risk (SHA-256 input only), but could confuse callers. (routes.rs:611)
⛏️ Nitpicks
sed -Ein example script: Not POSIX-portable. Minor. (register_partner.sh:25)
CI Status
- fmt: FAIL
- clippy: PASS
- tests: PASS (100 passed, 1 ignored)
| use validator::{Validate, ValidationError}; | ||
|
|
||
| use crate::aps::ApsBidRequest; | ||
| use crate::render::extract_ec_hash; |
There was a problem hiding this comment.
🔧 cargo fmt failure — import ordering
use crate::render::extract_ec_hash is out of alphabetical order relative to the other crate:: imports, and there are two separate use crate::render:: imports (lines 21 and 26). CI will reject this.
Fix: Consolidate into one import block and let cargo fmt sort:
use crate::render::{creative_html, extract_ec_hash, info_html, render_svg, render_template_str, SignatureStatus};| #[test] | ||
| fn handle_resolve_returns_deterministic_uid() { | ||
| // Ensure no auth token is set (tests may run concurrently) | ||
| std::env::remove_var(PULL_TOKEN_ENV); |
There was a problem hiding this comment.
🔧 std::env::remove_var is unsound in concurrent tests on Rust 1.91.1
Multiple non-ignored tests call std::env::remove_var(PULL_TOKEN_ENV) (lines 1483, 1511, 1539, 1588, 1695). Since Rust 1.83, set_var/remove_var are deprecated as unsafe — they are not thread-safe and cargo test runs tests in parallel. This is a data race.
The #[ignore] test at line 1559 correctly notes this, but the non-ignored tests have the same problem.
Fix options:
- Remove
remove_varcalls from non-ignored tests — ensure the env var is never set during normal runs and test the "no auth" path implicitly - Use
#[serial]from theserial_testcrate for all tests that touch env vars - Use a test-local config struct instead of env vars
| const TS_ALLOWED_DOMAINS_ENV: &str = "MOCKTIONEER_TS_DOMAINS"; | ||
|
|
||
| /// Returns true if `s` looks like a valid hostname (no path, auth, port, or fragment). | ||
| fn is_valid_hostname(s: &str) -> bool { |
There was a problem hiding this comment.
🤔 Hostname validation is character-class-based, not RFC-compliant
This rejects path/auth/port/fragment characters but accepts:
evil.com..(double dots)-starts-with-dash- Labels > 63 chars
- Total length > 253 chars
For a mock bidder this is acceptable, but since the PR description emphasizes "open-redirect protection," worth tightening — or at minimum adding a comment noting the intentional trade-off.
|
|
||
| /// Constant-time token comparison using `subtle::ConstantTimeEq`. | ||
| /// Compares SHA-256 digests to avoid leaking length information. | ||
| fn constant_time_token_eq(provided: &str, expected: &str) -> bool { |
There was a problem hiding this comment.
🤔 Correctly implemented constant-time comparison
SHA-256 double-hash before ConstantTimeEq is the right pattern — normalizes length to avoid leaking it via timing. The sha2 crate is well-justified with 3 uses (mtkid, UID derivation, and here).
|
|
||
| #[derive(Deserialize, Validate)] | ||
| struct SyncDoneParams { | ||
| /// Whether the sync succeeded ("1") or failed ("0"). |
There was a problem hiding this comment.
📝 ts_synced accepts any string value
The handler only checks == "1" for success. Values like "yes", "true", "" are silently treated as failure. Consider validating to "0" | "1" with a custom validator, or at minimum documenting the expected values.
| } | ||
|
|
||
| #[derive(Serialize)] | ||
| struct ResolveResponse { |
There was a problem hiding this comment.
📝 uid is Option<String> but always Some
handle_resolve always returns Some(uid) — the None variant is never used. Simplify to String, or add a code path that returns None (e.g., when EC lookup fails).
| #[validate(custom(function = "validate_ec_id"))] | ||
| ec_id: String, | ||
| /// Client IP address. | ||
| #[validate(length(min = 1, max = 45))] |
There was a problem hiding this comment.
📝 ip has no format validation
Only length-validated (1-45 chars). Any string is accepted, including "not-an-ip". No security risk since it's only fed to SHA-256, but could confuse callers expecting validation.
| MOCKTIONEER_PULL_TOKEN="${MOCKTIONEER_PULL_TOKEN:-mtk-pull-token-change-me}" | ||
|
|
||
| # Extract hostname from mocktioneer URL for allowed_return_domains | ||
| MOCKTIONEER_HOST=$(echo "${MOCKTIONEER_BASE_URL}" | sed -E 's|https?://||' | sed -E 's|/.*||') |
There was a problem hiding this comment.
⛏️ sed -E is not POSIX
sed -E is a GNU/BSD extension. Minor since this is an example script, but sed 's|https\{0,1\}://||' would be more portable.
aram356
left a comment
There was a problem hiding this comment.
PR Review
Summary
Solid, well-tested addition of the Edge Cookie (EC) sync surface area — three new endpoints, OpenRTB 2.6 user.eids support, and a partner registration script. Security hardening (constant-time auth, hostname validation, log sanitization) is thoughtfully done and the test coverage is adversarial and thorough. However, a handful of issues need to be resolved before merge, most notably an unmerged upstream dep pin, an empty-env-var auth bypass, a silent behavioral change on an already-shipped endpoint, and a well-known default token baked into register_partner.sh.
Findings
🔧 Needs Change
- Workspace deps pin to unmerged upstream branch (
Cargo.toml:24-28) — see inline. - Empty
MOCKTIONEER_PULL_TOKENsilently bypasses auth (routes.rs:774-787) — see inline. /pixelmtkid semantically changed from per-visitor UUID → per-host deterministic hash (routes.rs:378-410) — see inline.- Default
MOCKTIONEER_PULL_TOKENbaked intoregister_partner.sh(examples/register_partner.sh:22) — see inline. /sync/doneCache-Control doc/code mismatch (docs/api/sync.md:170) — see inline.
❓ Questions
- WASM silent auth/allowlist bypass on Cloudflare — on CF Workers,
std::env::varreturnsErr, so bothMOCKTIONEER_PULL_TOKENandMOCKTIONEER_TS_DOMAINSare silently ignored. Code comments acknowledge this but there's no runtime signal — operators who configure these viawrangler.tomlwill think they're enforced. Should we log a warning at startup when running onwasm32-unknown-unknown/wasm32-wasip1? Or route secret reads through an adapter-level config hook? At minimum,docs/integrations/trusted-server.mdshould call this out more prominently than the code comment does. - Open-redirect-by-default when allowlist is unset (
routes.rs:651-663) — see inline.
🤔 Thoughts
- Log injection via attacker-controlled cookie (
routes.rs:684-688) — inline. - Log injection via
ipparam (routes.rs:803-808) — inline. .expect()on post-validation unwrap (routes.rs:792) — inline.ts_syncedaccepts arbitrary strings (routes.rs:723) — inline.ipis length-only validated (routes.rs:611) — inline.
⛏ Nitpicks
- Stale empty
impl SizeDimensions {}(crates/mocktioneer-core/src/routes.rs:116) — pre-existing, but easy campground cleanup while the file is open. extract_ec_hashinvoked twice per resolve (validation then extraction) — micro, acceptable.- sed-based host extraction is fragile (
examples/register_partner.sh:25-29) — inline. - JSON heredoc interpolates env vars unquoted (
examples/register_partner.sh:40-58) — inline. - Docs repeat the well-known default token (
docs/api/resolve.md:26) — inline. hex_encodeuses lowercase whileurlencodinguses uppercaseHEX_CHARS; consistent internally but the two tables duplicate.
🌱 Seeds
PARTNER_IDhardcoded (routes.rs:558) — inline.- Consider a startup log summarizing active EC-sync security posture (which env vars are set/unset). Would help diagnose the empty-token and WASM bypass issues above.
is_local_hostdoesn't cover0.0.0.0or private ranges — fine for the current test use, note for later.
📌 Out of Scope
- The upstream branch name
feature/configureable-axum-hosthas a typo ("configureable"). That lives instackpop/edgezero, not here — worth fixing there.
👍 Praise
- Constant-time auth via SHA-256 digest +
subtle::ConstantTimeEq(routes.rs:853-858) — textbook-correct. Hashing both sides beforect_eqavoids any length-leak concern. - HTML-comment
--escape (render.rs:164-165) — small but important detail for embedding attacker-adjacent JSON in a comment. - Top-level
user.eidsvsuser.ext.eidsfallback with top-level priority (render.rs:84-97) — pragmatic handling of the OpenRTB 2.5/2.6 split. is_local_hosthandles bracketed IPv6 with port (routes.rs:861-872) — a detail most implementations miss.- Adversarial tests included — path injection, auth injection, non-hex
ec_id, missing params, deterministic UID verification, cookie reuse. Good coverage.
CI Status
- fmt: PASS
- clippy: PASS (
-D warnings,--all-targets --all-features) - tests: PASS (109 passed, 1 intentionally
#[ignore]'d for env-var race)
| edgezero-adapter-cloudflare = { git = "https://github.com/stackpop/edgezero.git", branch = "feature/configureable-axum-host", package = "edgezero-adapter-cloudflare", default-features = false } | ||
| edgezero-adapter-fastly = { git = "https://github.com/stackpop/edgezero.git", branch = "feature/configureable-axum-host", package = "edgezero-adapter-fastly", default-features = false } | ||
| edgezero-cli = { git = "https://github.com/stackpop/edgezero.git", branch = "feature/configureable-axum-host", package = "edgezero-cli" } | ||
| edgezero-core = { git = "https://github.com/stackpop/edgezero.git", branch = "feature/configureable-axum-host", package = "edgezero-core" } |
There was a problem hiding this comment.
🔧 Workspace deps pin to unmerged upstream branch
All five edgezero-* deps point at feature/configureable-axum-host (unmerged, also a typo — "configureable"). Two commits in this PR (7b8ca36, 95122e7) are unrelated to EC sync and only exist to bump these pins.
This PR cannot cleanly land until:
- the upstream branch is merged to
maininstackpop/edgezeroand these deps return tobranch = "main", or - the dep bump is split into its own PR that lands after upstream.
Leaving deps on a feature branch also means Cargo.lock churns whenever upstream force-pushes that branch.
| .unwrap_or(""); | ||
|
|
||
| let provided_token = auth_header.strip_prefix("Bearer ").unwrap_or(""); | ||
| if !constant_time_token_eq(provided_token, &expected_token) { |
There was a problem hiding this comment.
🔧 Empty MOCKTIONEER_PULL_TOKEN silently bypasses auth
std::env::var returns Ok("") when the variable is set to an empty string (common in Docker/k8s deployments that do -e MOCKTIONEER_PULL_TOKEN=). This path then compares Bearer (empty provided_token after strip_prefix) against expected_token = "". Both hash to SHA-256("") → ct_eq returns true → auth passes.
Fix:
if let Ok(expected_token) = std::env::var(PULL_TOKEN_ENV) {
if expected_token.is_empty() {
log::warn!("{} is set but empty; auth disabled", PULL_TOKEN_ENV);
} else {
let auth_header = headers
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let provided_token = auth_header.strip_prefix("Bearer ").unwrap_or("");
if !constant_time_token_eq(provided_token, &expected_token) {
// ...existing reject...
}
}
}| ); | ||
| set_cookie = Some(cookie_val); | ||
| } | ||
| let (_, set_cookie) = get_or_create_mtkid(&headers, &host); |
There was a problem hiding this comment.
🔧 Semantic behavior change on existing endpoint
Before this PR, /pixel minted a fresh Uuid::now_v7() for each first-time visitor, so every browser got a unique mtkid cookie. After this PR, get_or_create_mtkid returns SHA-256("mtkid:" || host)[..32] — all visitors to the same host share a single mtkid.
This satisfies the CLAUDE.md determinism rule (so it may be intentional), but it's a breaking semantic change on an already-shipped endpoint and the PR description doesn't call it out. Any downstream that was using mtkid as a per-visitor discriminator is now broken.
Please either:
- Explicitly document this break in the PR body / CHANGELOG (and the
/pixelAPI docs), or - Keep
Uuid::now_v7()for/pixeland only use the deterministic ID for the new/sync/startpath.
| TS_ADMIN_PASS="${TS_ADMIN_PASS:?Set TS_ADMIN_PASS to the Basic Auth password}" | ||
| MOCKTIONEER_BASE_URL="${MOCKTIONEER_BASE_URL:-https://origin-mocktioneer.cdintel.com}" | ||
| MOCKTIONEER_API_KEY="${MOCKTIONEER_API_KEY:-mtk-demo-key-change-me}" | ||
| MOCKTIONEER_PULL_TOKEN="${MOCKTIONEER_PULL_TOKEN:-mtk-pull-token-change-me}" |
There was a problem hiding this comment.
🔧 Default pull token is a well-known string
mtk-pull-token-change-me is the default baked into the script. If the mocktioneer deployment side also runs with that default (the docs' curl examples use the same string), anyone reading this repo can authenticate against every default deployment.
Fix — mirror the treatment of TS_ADMIN_USER / TS_ADMIN_PASS:
MOCKTIONEER_API_KEY="${MOCKTIONEER_API_KEY:?Set MOCKTIONEER_API_KEY to the partner API key}"
MOCKTIONEER_PULL_TOKEN="${MOCKTIONEER_PULL_TOKEN:?Set MOCKTIONEER_PULL_TOKEN to the pull sync bearer token}"Same concern in docs/api/resolve.md:26 and docs/guide/configuration.md — the docs should stop instructing users to curl with mtk-pull-token-change-me.
| HTTP/1.1 200 OK | ||
| Content-Type: image/gif | ||
| Content-Length: 43 | ||
| Cache-Control: no-store |
There was a problem hiding this comment.
🔧 Cache-Control doc/code mismatch
The actual response from routes.rs:739-742 sets Cache-Control: no-store, no-cache, must-revalidate, max-age=0, not just no-store.
Suggested fix:
Cache-Control: no-store, no-cache, must-revalidate, max-age=0
| #[validate(custom(function = "validate_ec_id"))] | ||
| ec_id: String, | ||
| /// Client IP address. | ||
| #[validate(length(min = 1, max = 45))] |
There was a problem hiding this comment.
🤔 ip is length-only validated
Non-IP strings are accepted. The UID stays deterministic so it functions, but invalid IPs pollute audit logs and make the endpoint's contract fuzzy. Consider parsing with std::net::IpAddr::from_str.
|
|
||
| # Extract hostname from pull sync URL for pull_sync_allowed_domains | ||
| RESOLVE_URL="${MOCKTIONEER_BASE_URL}/resolve" | ||
| RESOLVE_HOST=$(echo "${RESOLVE_URL}" | sed -E 's|https?://||' | sed -E 's|/.*||' | sed -E 's|:.*||') |
There was a problem hiding this comment.
⛏ sed-based host extraction is fragile
- Neither pipeline strips userinfo (
user:pass@host→user:pass@host). MOCKTIONEER_HOST(line 25) does not strip a port whileRESOLVE_HOST(line 29) does.
A single consistent helper would remove the drift:
host_of() { echo "$1" | sed -E 's|https?://||; s|/.*||; s|.*@||; s|:.*||'; }
MOCKTIONEER_HOST=$(host_of "${MOCKTIONEER_BASE_URL}")
RESOLVE_HOST=$(host_of "${RESOLVE_URL}")| "pull_sync_ttl_sec": 86400, | ||
| "pull_sync_rate_limit": 10, | ||
| "ts_pull_token": "${MOCKTIONEER_PULL_TOKEN}" | ||
| } |
There was a problem hiding this comment.
⛏ JSON heredoc interpolates env vars unquoted
If MOCKTIONEER_API_KEY, MOCKTIONEER_PULL_TOKEN, or the admin creds contain a " or \, the heredoc emits malformed JSON. Safer with jq -n --arg ... to construct the payload, then pipe into curl --data @-.
|
|
||
| ```bash | ||
| curl "http://127.0.0.1:8787/resolve?ec_id=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2.AbC123&ip=1.2.3.4" \ | ||
| -H "Authorization: Bearer mtk-pull-token-change-me" |
There was a problem hiding this comment.
⛏ Docs repeat the well-known default token
mtk-pull-token-change-me appears here and in docs/integrations/trusted-server.md:110,125 and in docs/guide/configuration.md. Combined with the script default in examples/register_partner.sh (see the blocker above), this becomes a universal credential. Consider using an obviously-placeholder like <YOUR_PULL_TOKEN> or $MOCKTIONEER_PULL_TOKEN.
| // --------------------------------------------------------------------------- | ||
|
|
||
| /// The partner ID that mocktioneer uses when registering with trusted-server. | ||
| const PARTNER_ID: &str = "mocktioneer"; |
There was a problem hiding this comment.
🌱 PARTNER_ID hardcoded
Fine for now, but future multi-tenant / multi-deployment demos will need a way to register as distinct partner IDs. Likely an env var when that need appears.
Summary
/sync/start,/sync/done,/resolve) that enable mocktioneer to act as a full EC sync partner with trusted-server — supporting pixel sync redirect chains, callback handling, and S2S pull sync resolutionuser.eidssupport and embed EC identity metadata (EdgeCookieInfo) in creative HTML comments, enabling end-to-end demo of the trusted-server EC identity pipelineexamples/register_partner.shfor one-step partner registration with trusted-serverNew Endpoints
/sync/start?ts_domain=...mtkidcookie, redirects browser to TS/sync/sync/done?ts_synced=.../resolve?ec_hash=...&ip=...mtk-{sha256(ec_hash|ip)[0:12]}Security
subtle::ConstantTimeEqon SHA-256 digests (no length leak)ts_domainvalidated as clean hostname (no/,@,:,?,#) + optional allowlist viaMOCKTIONEER_TS_DOMAINSenv varec_hashrequires exactly 64 hex characters; log output sanitized against control charsmtkidderived fromSHA-256("mtkid:" || host)— no randomness per CLAUDE.mdTesting
--ignored)cargo fmt,cargo clippycleanWASM Note
MOCKTIONEER_PULL_TOKENandMOCKTIONEER_TS_DOMAINSusestd::env::varwhich returnsErron Cloudflare Workers. Auth and domain allowlist are silently disabled on that platform. Documented in code comments.