Skip to content

Commit 31fc460

Browse files
Migrate [edge_cookie] config to [ec] per spec §14
Rename EdgeCookie struct to Ec, secret_key field to passphrase, and the TOML section from [edge_cookie] to [ec] to align with the spec's configuration schema. Add optional ec_store and partner_store fields to the Ec struct in preparation for Story 3 (KV identity graph) and Story 4 (partner registry). Remove the edge_cookie.rs legacy re-export shim — no consumers remain after the ec/ module migration.
1 parent 95fc8e8 commit 31fc460

16 files changed

Lines changed: 110 additions & 102 deletions

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 |

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.

crates/trusted-server-core/src/ec/generation.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ pub fn generate_ec_id(
7070
) -> Result<String, Report<TrustedServerError>> {
7171
log::trace!("Input for fresh EC ID: client_ip={client_ip}");
7272

73-
let mut mac = HmacSha256::new_from_slice(settings.edge_cookie.secret_key.expose().as_bytes())
73+
let mut mac = HmacSha256::new_from_slice(settings.ec.passphrase.expose().as_bytes())
7474
.change_context(TrustedServerError::Ec {
75-
message: "Failed to create HMAC instance".to_string(),
76-
})?;
75+
message: "Failed to create HMAC instance".to_string(),
76+
})?;
7777
mac.update(client_ip.as_bytes());
7878
let hmac_hash = hex::encode(mac.finalize().into_bytes());
7979

crates/trusted-server-core/src/edge_cookie.rs

Lines changed: 0 additions & 11 deletions
This file was deleted.

crates/trusted-server-core/src/integrations/google_tag_manager.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,8 +1308,8 @@ cookie_domain = ".test-publisher.com"
13081308
origin_url = "https://origin.test-publisher.com"
13091309
proxy_secret = "test-secret"
13101310
1311-
[edge_cookie]
1312-
secret_key = "test-secret-key"
1311+
[ec]
1312+
passphrase = "test-secret-key"
13131313
13141314
[integrations.google_tag_manager]
13151315
enabled = true
@@ -1341,8 +1341,8 @@ cookie_domain = ".test-publisher.com"
13411341
origin_url = "https://origin.test-publisher.com"
13421342
proxy_secret = "test-secret"
13431343
1344-
[edge_cookie]
1345-
secret_key = "test-secret-key"
1344+
[ec]
1345+
passphrase = "test-secret-key"
13461346
13471347
[integrations.google_tag_manager]
13481348
container_id = "GTM-DEFAULT"

crates/trusted-server-core/src/integrations/prebid.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,8 +1411,8 @@ cookie_domain = ".test-publisher.com"
14111411
origin_url = "https://origin.test-publisher.com"
14121412
proxy_secret = "test-secret"
14131413
1414-
[edge_cookie]
1415-
secret_key = "test-secret-key"
1414+
[ec]
1415+
passphrase = "test-secret-key"
14161416
"#;
14171417

14181418
/// Parse a TOML string containing only the `[integrations.prebid]` section

crates/trusted-server-core/src/lib.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
//! - [`settings`]: Configuration management and validation
1818
//! - [`streaming_replacer`]: Streaming URL replacement for large responses
1919
//! - [`ec`]: Edge Cookie (EC) identity subsystem — ID generation, consent gating, lifecycle
20-
//! - [`edge_cookie`]: Legacy re-exports from [`ec`]
2120
//! - [`test_support`]: Testing utilities and mocks
2221
//! - [`why`]: Debugging and introspection utilities
2322
@@ -42,7 +41,6 @@ pub mod constants;
4241
pub mod cookies;
4342
pub mod creative;
4443
pub mod ec;
45-
pub mod edge_cookie;
4644
pub mod error;
4745
pub mod fastly_storage;
4846
pub mod geo;

crates/trusted-server-core/src/settings.rs

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -204,28 +204,42 @@ impl DerefMut for IntegrationSettings {
204204
}
205205
}
206206

207-
/// Edge Cookie configuration.
207+
/// Edge Cookie (EC) configuration.
208+
///
209+
/// Mapped from the `[ec]` TOML section. Controls EC identity generation,
210+
/// KV store names, and partner registry.
208211
#[allow(unused)]
209212
#[derive(Debug, Default, Clone, Deserialize, Serialize, Validate)]
210-
pub struct EdgeCookie {
211-
#[validate(custom(function = EdgeCookie::validate_secret_key))]
212-
pub secret_key: Redacted<String>,
213+
pub struct Ec {
214+
/// Publisher passphrase used as HMAC key for EC generation.
215+
#[validate(custom(function = Ec::validate_passphrase))]
216+
pub passphrase: Redacted<String>,
217+
218+
/// Fastly KV store name for the EC identity graph.
219+
/// Required for Stories 3+ (KV identity graph).
220+
#[serde(default)]
221+
pub ec_store: Option<String>,
222+
223+
/// Fastly KV store name for the partner registry.
224+
/// Required for Story 4+ (partner registry).
225+
#[serde(default)]
226+
pub partner_store: Option<String>,
213227
}
214228

215-
impl EdgeCookie {
229+
impl Ec {
216230
/// Known placeholder values that must not be used in production.
217-
pub const SECRET_KEY_PLACEHOLDERS: &[&str] = &["secret-key", "secret_key", "trusted-server"];
231+
pub const PASSPHRASE_PLACEHOLDERS: &[&str] = &["secret-key", "secret_key", "trusted-server"];
218232

219-
/// Returns `true` if `secret_key` matches a known placeholder value
233+
/// Returns `true` if `passphrase` matches a known placeholder value
220234
/// (case-insensitive).
221235
#[must_use]
222-
pub fn is_placeholder_secret_key(secret_key: &str) -> bool {
223-
Self::SECRET_KEY_PLACEHOLDERS
236+
pub fn is_placeholder_passphrase(passphrase: &str) -> bool {
237+
Self::PASSPHRASE_PLACEHOLDERS
224238
.iter()
225-
.any(|p| p.eq_ignore_ascii_case(secret_key))
239+
.any(|p| p.eq_ignore_ascii_case(passphrase))
226240
}
227241

228-
/// Validates that the secret key is not empty.
242+
/// Validates that the passphrase is not empty.
229243
///
230244
/// Placeholder detection is intentionally **not** performed here because
231245
/// this validator runs at build time (via `from_toml_and_env`) when the
@@ -234,10 +248,10 @@ impl EdgeCookie {
234248
///
235249
/// # Errors
236250
///
237-
/// Returns a validation error if the secret key is empty.
238-
pub fn validate_secret_key(secret_key: &Redacted<String>) -> Result<(), ValidationError> {
239-
if secret_key.expose().is_empty() {
240-
return Err(ValidationError::new("empty_secret_key"));
251+
/// Returns a validation error if the passphrase is empty.
252+
pub fn validate_passphrase(passphrase: &Redacted<String>) -> Result<(), ValidationError> {
253+
if passphrase.expose().is_empty() {
254+
return Err(ValidationError::new("empty_passphrase"));
241255
}
242256
Ok(())
243257
}
@@ -343,7 +357,7 @@ pub struct Settings {
343357
pub publisher: Publisher,
344358
#[serde(default)]
345359
#[validate(nested)]
346-
pub edge_cookie: EdgeCookie,
360+
pub ec: Ec,
347361
#[serde(default)]
348362
pub integrations: IntegrationSettings,
349363
#[serde(default, deserialize_with = "vec_from_seq_or_map")]
@@ -439,8 +453,8 @@ impl Settings {
439453
pub fn reject_placeholder_secrets(&self) -> Result<(), Report<TrustedServerError>> {
440454
let mut insecure_fields: Vec<&str> = Vec::new();
441455

442-
if EdgeCookie::is_placeholder_secret_key(self.edge_cookie.secret_key.expose()) {
443-
insecure_fields.push("edge_cookie.secret_key");
456+
if Ec::is_placeholder_passphrase(self.ec.passphrase.expose()) {
457+
insecure_fields.push("ec.passphrase");
444458
}
445459
if Publisher::is_placeholder_proxy_secret(self.publisher.proxy_secret.expose()) {
446460
insecure_fields.push("publisher.proxy_secret");
@@ -722,7 +736,7 @@ mod tests {
722736
settings.publisher.origin_url,
723737
"https://origin.test-publisher.com"
724738
);
725-
assert_eq!(settings.edge_cookie.secret_key.expose(), "test-secret-key");
739+
assert_eq!(settings.ec.passphrase.expose(), "test-secret-key");
726740

727741
settings.validate().expect("Failed to validate settings");
728742
}
@@ -757,32 +771,32 @@ mod tests {
757771
}
758772

759773
#[test]
760-
fn is_placeholder_secret_key_rejects_all_known_placeholders() {
761-
for placeholder in EdgeCookie::SECRET_KEY_PLACEHOLDERS {
774+
fn is_placeholder_passphrase_rejects_all_known_placeholders() {
775+
for placeholder in Ec::PASSPHRASE_PLACEHOLDERS {
762776
assert!(
763-
EdgeCookie::is_placeholder_secret_key(placeholder),
764-
"should detect placeholder secret_key '{placeholder}'"
777+
Ec::is_placeholder_passphrase(placeholder),
778+
"should detect placeholder passphrase '{placeholder}'"
765779
);
766780
}
767781
}
768782

769783
#[test]
770-
fn is_placeholder_secret_key_is_case_insensitive() {
784+
fn is_placeholder_passphrase_is_case_insensitive() {
771785
assert!(
772-
EdgeCookie::is_placeholder_secret_key("SECRET-KEY"),
773-
"should detect case-insensitive placeholder secret_key"
786+
Ec::is_placeholder_passphrase("SECRET-KEY"),
787+
"should detect case-insensitive placeholder passphrase"
774788
);
775789
assert!(
776-
EdgeCookie::is_placeholder_secret_key("Trusted-Server"),
777-
"should detect mixed-case placeholder secret_key"
790+
Ec::is_placeholder_passphrase("Trusted-Server"),
791+
"should detect mixed-case placeholder passphrase"
778792
);
779793
}
780794

781795
#[test]
782-
fn is_placeholder_secret_key_accepts_non_placeholder() {
796+
fn is_placeholder_passphrase_accepts_non_placeholder() {
783797
assert!(
784-
!EdgeCookie::is_placeholder_secret_key("test-secret-key"),
785-
"should accept non-placeholder secret_key"
798+
!Ec::is_placeholder_passphrase("test-secret-key"),
799+
"should accept non-placeholder passphrase"
786800
);
787801
}
788802

@@ -1398,8 +1412,8 @@ mod tests {
13981412
origin_url = "https://origin.test-publisher.com"
13991413
proxy_secret = "unit-test-proxy-secret"
14001414
1401-
[edge_cookie]
1402-
secret_key = "test-secret-key"
1415+
[ec]
1416+
passphrase = "test-secret-key"
14031417
14041418
[request_signing]
14051419
config_store_id = "test-config-store-id"

crates/trusted-server-core/src/settings_data.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,22 +58,22 @@ mod tests {
5858
///
5959
/// Panics if the replacement patterns no longer match the test TOML,
6060
/// which would cause the substitution to silently no-op.
61-
fn toml_with_secrets(secret_key: &str, proxy_secret: &str) -> String {
61+
fn toml_with_secrets(passphrase: &str, proxy_secret: &str) -> String {
6262
let original = crate_test_settings_str();
63-
let after_secret_key = original.replace(
64-
r#"secret_key = "test-secret-key""#,
65-
&format!(r#"secret_key = "{secret_key}""#),
63+
let after_passphrase = original.replace(
64+
r#"passphrase = "test-secret-key""#,
65+
&format!(r#"passphrase = "{passphrase}""#),
6666
);
6767
assert_ne!(
68-
after_secret_key, original,
69-
"should have replaced secret_key value"
68+
after_passphrase, original,
69+
"should have replaced passphrase value"
7070
);
71-
let result = after_secret_key.replace(
71+
let result = after_passphrase.replace(
7272
r#"proxy_secret = "unit-test-proxy-secret""#,
7373
&format!(r#"proxy_secret = "{proxy_secret}""#),
7474
);
7575
assert_ne!(
76-
result, after_secret_key,
76+
result, after_passphrase,
7777
"should have replaced proxy_secret value"
7878
);
7979
result
@@ -88,8 +88,8 @@ mod tests {
8888
.expect_err("should reject placeholder secret_key");
8989
let root = err.current_context();
9090
assert!(
91-
matches!(root, TrustedServerError::InsecureDefault { field } if field.contains("edge_cookie.secret_key")),
92-
"error should mention edge_cookie.secret_key, got: {root}"
91+
matches!(root, TrustedServerError::InsecureDefault { field } if field.contains("ec.passphrase")),
92+
"error should mention ec.passphrase, got: {root}"
9393
);
9494
}
9595

@@ -118,8 +118,8 @@ mod tests {
118118
match root {
119119
TrustedServerError::InsecureDefault { field } => {
120120
assert!(
121-
field.contains("edge_cookie.secret_key"),
122-
"error should mention edge_cookie.secret_key, got: {field}"
121+
field.contains("ec.passphrase"),
122+
"error should mention ec.passphrase, got: {field}"
123123
);
124124
assert!(
125125
field.contains("publisher.proxy_secret"),

crates/trusted-server-core/src/test_support.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ pub mod tests {
3030
enabled = false
3131
rewrite_attributes = ["href", "link", "url"]
3232
33-
[edge_cookie]
34-
secret_key = "test-secret-key"
33+
[ec]
34+
passphrase = "test-secret-key"
3535
[request_signing]
3636
config_store_id = "test-config-store-id"
3737
secret_store_id = "test-secret-store-id"

0 commit comments

Comments
 (0)