Skip to content

Commit fbf83bc

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 54ffdeb commit fbf83bc

File tree

16 files changed

+110
-102
lines changed

16 files changed

+110
-102
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 |

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
@@ -1307,8 +1307,8 @@ cookie_domain = ".test-publisher.com"
13071307
origin_url = "https://origin.test-publisher.com"
13081308
proxy_secret = "test-secret"
13091309
1310-
[edge_cookie]
1311-
secret_key = "test-secret-key"
1310+
[ec]
1311+
passphrase = "test-secret-key"
13121312
"#;
13131313

13141314
/// 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
@@ -192,28 +192,42 @@ impl DerefMut for IntegrationSettings {
192192
}
193193
}
194194

195-
/// Edge Cookie configuration.
195+
/// Edge Cookie (EC) configuration.
196+
///
197+
/// Mapped from the `[ec]` TOML section. Controls EC identity generation,
198+
/// KV store names, and partner registry.
196199
#[allow(unused)]
197200
#[derive(Debug, Default, Clone, Deserialize, Serialize, Validate)]
198-
pub struct EdgeCookie {
199-
#[validate(custom(function = EdgeCookie::validate_secret_key))]
200-
pub secret_key: Redacted<String>,
201+
pub struct Ec {
202+
/// Publisher passphrase used as HMAC key for EC generation.
203+
#[validate(custom(function = Ec::validate_passphrase))]
204+
pub passphrase: Redacted<String>,
205+
206+
/// Fastly KV store name for the EC identity graph.
207+
/// Required for Stories 3+ (KV identity graph).
208+
#[serde(default)]
209+
pub ec_store: Option<String>,
210+
211+
/// Fastly KV store name for the partner registry.
212+
/// Required for Story 4+ (partner registry).
213+
#[serde(default)]
214+
pub partner_store: Option<String>,
201215
}
202216

203-
impl EdgeCookie {
217+
impl Ec {
204218
/// Known placeholder values that must not be used in production.
205-
pub const SECRET_KEY_PLACEHOLDERS: &[&str] = &["secret-key", "secret_key", "trusted-server"];
219+
pub const PASSPHRASE_PLACEHOLDERS: &[&str] = &["secret-key", "secret_key", "trusted-server"];
206220

207-
/// Returns `true` if `secret_key` matches a known placeholder value
221+
/// Returns `true` if `passphrase` matches a known placeholder value
208222
/// (case-insensitive).
209223
#[must_use]
210-
pub fn is_placeholder_secret_key(secret_key: &str) -> bool {
211-
Self::SECRET_KEY_PLACEHOLDERS
224+
pub fn is_placeholder_passphrase(passphrase: &str) -> bool {
225+
Self::PASSPHRASE_PLACEHOLDERS
212226
.iter()
213-
.any(|p| p.eq_ignore_ascii_case(secret_key))
227+
.any(|p| p.eq_ignore_ascii_case(passphrase))
214228
}
215229

216-
/// Validates that the secret key is not empty.
230+
/// Validates that the passphrase is not empty.
217231
///
218232
/// Placeholder detection is intentionally **not** performed here because
219233
/// this validator runs at build time (via `from_toml_and_env`) when the
@@ -222,10 +236,10 @@ impl EdgeCookie {
222236
///
223237
/// # Errors
224238
///
225-
/// Returns a validation error if the secret key is empty.
226-
pub fn validate_secret_key(secret_key: &Redacted<String>) -> Result<(), ValidationError> {
227-
if secret_key.expose().is_empty() {
228-
return Err(ValidationError::new("empty_secret_key"));
239+
/// Returns a validation error if the passphrase is empty.
240+
pub fn validate_passphrase(passphrase: &Redacted<String>) -> Result<(), ValidationError> {
241+
if passphrase.expose().is_empty() {
242+
return Err(ValidationError::new("empty_passphrase"));
229243
}
230244
Ok(())
231245
}
@@ -331,7 +345,7 @@ pub struct Settings {
331345
pub publisher: Publisher,
332346
#[serde(default)]
333347
#[validate(nested)]
334-
pub edge_cookie: EdgeCookie,
348+
pub ec: Ec,
335349
#[serde(default)]
336350
pub integrations: IntegrationSettings,
337351
#[serde(default, deserialize_with = "vec_from_seq_or_map")]
@@ -427,8 +441,8 @@ impl Settings {
427441
pub fn reject_placeholder_secrets(&self) -> Result<(), Report<TrustedServerError>> {
428442
let mut insecure_fields: Vec<&str> = Vec::new();
429443

430-
if EdgeCookie::is_placeholder_secret_key(self.edge_cookie.secret_key.expose()) {
431-
insecure_fields.push("edge_cookie.secret_key");
444+
if Ec::is_placeholder_passphrase(self.ec.passphrase.expose()) {
445+
insecure_fields.push("ec.passphrase");
432446
}
433447
if Publisher::is_placeholder_proxy_secret(self.publisher.proxy_secret.expose()) {
434448
insecure_fields.push("publisher.proxy_secret");
@@ -717,7 +731,7 @@ mod tests {
717731
settings.publisher.origin_url,
718732
"https://origin.test-publisher.com"
719733
);
720-
assert_eq!(settings.edge_cookie.secret_key.expose(), "test-secret-key");
734+
assert_eq!(settings.ec.passphrase.expose(), "test-secret-key");
721735

722736
settings.validate().expect("Failed to validate settings");
723737
}
@@ -752,32 +766,32 @@ mod tests {
752766
}
753767

754768
#[test]
755-
fn is_placeholder_secret_key_rejects_all_known_placeholders() {
756-
for placeholder in EdgeCookie::SECRET_KEY_PLACEHOLDERS {
769+
fn is_placeholder_passphrase_rejects_all_known_placeholders() {
770+
for placeholder in Ec::PASSPHRASE_PLACEHOLDERS {
757771
assert!(
758-
EdgeCookie::is_placeholder_secret_key(placeholder),
759-
"should detect placeholder secret_key '{placeholder}'"
772+
Ec::is_placeholder_passphrase(placeholder),
773+
"should detect placeholder passphrase '{placeholder}'"
760774
);
761775
}
762776
}
763777

764778
#[test]
765-
fn is_placeholder_secret_key_is_case_insensitive() {
779+
fn is_placeholder_passphrase_is_case_insensitive() {
766780
assert!(
767-
EdgeCookie::is_placeholder_secret_key("SECRET-KEY"),
768-
"should detect case-insensitive placeholder secret_key"
781+
Ec::is_placeholder_passphrase("SECRET-KEY"),
782+
"should detect case-insensitive placeholder passphrase"
769783
);
770784
assert!(
771-
EdgeCookie::is_placeholder_secret_key("Trusted-Server"),
772-
"should detect mixed-case placeholder secret_key"
785+
Ec::is_placeholder_passphrase("Trusted-Server"),
786+
"should detect mixed-case placeholder passphrase"
773787
);
774788
}
775789

776790
#[test]
777-
fn is_placeholder_secret_key_accepts_non_placeholder() {
791+
fn is_placeholder_passphrase_accepts_non_placeholder() {
778792
assert!(
779-
!EdgeCookie::is_placeholder_secret_key("test-secret-key"),
780-
"should accept non-placeholder secret_key"
793+
!Ec::is_placeholder_passphrase("test-secret-key"),
794+
"should accept non-placeholder passphrase"
781795
);
782796
}
783797

@@ -1419,8 +1433,8 @@ mod tests {
14191433
origin_url = "https://origin.test-publisher.com"
14201434
proxy_secret = "unit-test-proxy-secret"
14211435
1422-
[edge_cookie]
1423-
secret_key = "test-secret-key"
1436+
[ec]
1437+
passphrase = "test-secret-key"
14241438
14251439
[request_signing]
14261440
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)