Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions crates/trusted-server-core/src/integrations/lockr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use validator::Validate;

use crate::constants::INTERNAL_HEADERS;
use crate::error::TrustedServerError;
use crate::integrations::sdk_url::parse_sdk_network_url;
use crate::integrations::{
AttributeRewriteAction, INTEGRATION_MAX_BODY_BYTES, IntegrationAttributeContext,
IntegrationAttributeRewriter, IntegrationEndpoint, IntegrationProxy, IntegrationRegistration,
Expand Down Expand Up @@ -98,10 +99,15 @@ impl LockrIntegration {

/// Check if a URL is a Lockr SDK URL.
fn is_lockr_sdk_url(&self, url: &str) -> bool {
let lower = url.to_ascii_lowercase();
(lower.contains("aim.loc.kr") || lower.contains("identity.loc.kr"))
&& lower.contains("identity-lockr")
&& lower.ends_with(".js")
// Keep browser/Rust cases in test/fixtures/sdk-url-ownership.json aligned.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ nitpick — Same as sdk_url.rs:8: use the repo-relative fixture path so it resolves from this crate.

Suggested change
// Keep browser/Rust cases in test/fixtures/sdk-url-ownership.json aligned.
// Keep browser/Rust cases in crates/trusted-server-js/lib/test/fixtures/sdk-url-ownership.json aligned.

let Some(parsed) = parse_sdk_network_url(url) else {
return false;
};
let host = parsed.host_str().unwrap_or_default();
let path = parsed.path().to_ascii_lowercase();
matches!(host, "aim.loc.kr" | "identity.loc.kr")
&& path.contains("identity-lockr")
&& path.ends_with(".js")
}

/// Handle SDK serving — fetch from Lockr CDN and serve through first-party domain.
Expand Down Expand Up @@ -437,6 +443,7 @@ mod tests {
use edgezero_core::http::Method as HttpMethod;
use serde_json::json;

use crate::integrations::sdk_url_test_support::{assert_sdk_url_rewrite, sdk_url_cases};
use crate::platform::test_support::{StubHttpClient, build_services_with_http_client};
use crate::test_support::tests::create_test_settings;

Expand All @@ -463,6 +470,24 @@ mod tests {
}
}

#[test]
fn sdk_url_ownership_matches_shared_corpus() {
let integration = LockrIntegration::new(test_config());

for case in sdk_url_cases() {
let matches = case.owner.as_deref() == Some(LOCKR_INTEGRATION_ID);
assert_eq!(
integration.is_lockr_sdk_url(&case.url),
matches,
"should match expected lockr ownership for {}: {}",
case.name,
case.url
);

assert_sdk_url_rewrite(integration.as_ref(), &case, "/integrations/lockr/sdk");
}
}

#[test]
fn test_lockr_sdk_url_detection() {
let integration = LockrIntegration::new(test_config());
Expand Down
3 changes: 3 additions & 0 deletions crates/trusted-server-core/src/integrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ pub mod osano;
pub mod permutive;
pub mod prebid;
mod registry;
mod sdk_url;
#[cfg(test)]
mod sdk_url_test_support;
pub mod sourcepoint;
pub mod testlight;

Expand Down
43 changes: 40 additions & 3 deletions crates/trusted-server-core/src/integrations/permutive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use validator::Validate;

use crate::constants::INTERNAL_HEADERS;
use crate::error::TrustedServerError;
use crate::integrations::sdk_url::parse_sdk_network_url;
use crate::integrations::{
AttributeRewriteAction, INTEGRATION_MAX_BODY_BYTES, IntegrationAttributeContext,
IntegrationAttributeRewriter, IntegrationEndpoint, IntegrationProxy, IntegrationRegistration,
Expand Down Expand Up @@ -99,9 +100,17 @@ impl PermutiveIntegration {

/// Check if a URL is a Permutive SDK URL.
fn is_permutive_sdk_url(&self, url: &str) -> bool {
let lower = url.to_ascii_lowercase();
(lower.contains(".edge.permutive.app") || lower.contains("cdn.permutive.com"))
&& lower.ends_with("-web.js")
// Keep browser/Rust cases in test/fixtures/sdk-url-ownership.json aligned.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ nitpick — Same as sdk_url.rs:8: use the repo-relative fixture path so it resolves from this crate.

Suggested change
// Keep browser/Rust cases in test/fixtures/sdk-url-ownership.json aligned.
// Keep browser/Rust cases in crates/trusted-server-js/lib/test/fixtures/sdk-url-ownership.json aligned.

let Some(parsed) = parse_sdk_network_url(url) else {
return false;
};
let host = parsed.host_str().unwrap_or_default();
let path = parsed.path().to_ascii_lowercase();
(host == "cdn.permutive.com"
|| host
.strip_suffix(".edge.permutive.app")
.is_some_and(|subdomain| !subdomain.is_empty()))
&& path.ends_with("-web.js")
}

/// Handle SDK serving - fetch from Permutive CDN and serve through first-party domain.
Expand Down Expand Up @@ -474,9 +483,37 @@ mod tests {
use std::sync::Arc;

use super::*;
use crate::integrations::sdk_url_test_support::{assert_sdk_url_rewrite, sdk_url_cases};
use crate::platform::test_support::{StubHttpClient, build_services_with_http_client};
use crate::test_support::tests::create_test_settings;

#[test]
fn sdk_url_ownership_matches_shared_corpus() {
let integration = PermutiveIntegration::new(PermutiveConfig {
enabled: true,
organization_id: "example".to_string(),
workspace_id: "workspace".to_string(),
project_id: String::new(),
api_endpoint: default_api_endpoint(),
secure_signals_endpoint: default_secure_signals_endpoint(),
cache_ttl_seconds: 3600,
rewrite_sdk: true,
});

for case in sdk_url_cases() {
let matches = case.owner.as_deref() == Some(PERMUTIVE_INTEGRATION_ID);
assert_eq!(
integration.is_permutive_sdk_url(&case.url),
matches,
"should match expected permutive ownership for {}: {}",
case.name,
case.url
);

assert_sdk_url_rewrite(integration.as_ref(), &case, "/integrations/permutive/sdk");
}
}

#[test]
fn test_permutive_sdk_url_generation() {
let config = PermutiveConfig {
Expand Down
22 changes: 22 additions & 0 deletions crates/trusted-server-core/src/integrations/sdk_url.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! Shared input policy for SDK URL ownership checks.

use url::Url;

/// Parse an HTTP(S) SDK URL with an explicit authority, or a protocol-relative URL.
///
/// Trims ASCII whitespace and rejects relative paths and shorthand schemes.
/// Browser/Rust ownership cases live in test/fixtures/sdk-url-ownership.json.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏ nitpick — test/fixtures/sdk-url-ownership.json is relative to crates/trusted-server-js/lib/, not to this crate, so it's hard to find from trusted-server-core. Same pointer in lockr.rs:102 and permutive.rs:103 (separate suggestions there).

Suggested change
/// Browser/Rust ownership cases live in test/fixtures/sdk-url-ownership.json.
/// Browser/Rust ownership cases live in crates/trusted-server-js/lib/test/fixtures/sdk-url-ownership.json.

pub(super) fn parse_sdk_network_url(url: &str) -> Option<Url> {
let value = url.trim_matches([' ', '\t', '\n', '\r', '\x0c', '\x0b']);
let parsed = if value.starts_with("//") {
Url::parse(&format!("https:{value}"))
} else {
if !value.split_once("://").is_some_and(|(scheme, _)| {
scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https")
}) {
return None;
}
Url::parse(value)
};
parsed.ok()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! Shared SDK URL corpus and attribute-rewrite assertions.

use serde::Deserialize;

use super::{AttributeRewriteAction, IntegrationAttributeContext, IntegrationAttributeRewriter};

/// One raw URL and its expected integration owner from the shared corpus.
#[derive(Deserialize)]
pub(super) struct SdkUrlCase {
/// Descriptive case name for assertion failures.
pub(super) name: String,
/// Input before browser DOM URL resolution.
pub(super) url: String,
/// Expected integration ID, or none for an unowned URL.
pub(super) owner: Option<String>,
}

/// Load the URL ownership cases used by both Rust and browser tests.
///
/// # Panics
///
/// Panics if the committed fixture is invalid.
pub(super) fn sdk_url_cases() -> Vec<SdkUrlCase> {
serde_json::from_str(include_str!(
"../../../trusted-server-js/lib/test/fixtures/sdk-url-ownership.json"
))
.expect("should parse shared SDK URL ownership corpus")
}

/// Assert SDK rewriting for both script and link attribute contexts.
///
/// # Panics
///
/// Panics if the rewrite result disagrees with the corpus owner and SDK route.
pub(super) fn assert_sdk_url_rewrite(
integration: &dyn IntegrationAttributeRewriter,
case: &SdkUrlCase,
sdk_path: &str,
) {
let expected = if case.owner.as_deref() == Some(integration.integration_id()) {
AttributeRewriteAction::Replace(sdk_path.to_string())
} else {
AttributeRewriteAction::Keep
};
for (attribute, element) in [("src", "script"), ("href", "link")] {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 wrench: The href iteration repeats the src check. rewrite() ignores the attribute name and context, while the registry routes href through handles_attribute() (registry.rs:1078), which nothing in this helper calls. Changing either integration's handles_attribute to matches!(attribute, "src") still passes all 2,982 core lib tests.

With this assertion, both mutants fail (should route href to the lockr rewriter, should route href to the permutive rewriter). Applied alone, it passes cargo fmt, all eight clippy aliases, the five adapter test aliases, and the parity suite.

Suggested change
for (attribute, element) in [("src", "script"), ("href", "link")] {
for (attribute, element) in [("src", "script"), ("href", "link")] {
assert!(
integration.handles_attribute(attribute),
"should route {attribute} to the {} rewriter",
integration.integration_id()
);

let context = IntegrationAttributeContext {
attribute_name: attribute,
element_name: element,
request_host: "edge.example.com",
request_scheme: "https",
origin_host: "origin.example.com",
};
assert_eq!(
integration.rewrite(attribute, &case.url, &context),
expected,
"should apply expected {} {attribute} rewrite for {}: {}",
integration.integration_id(),
case.name,
case.url
);
}
}
31 changes: 11 additions & 20 deletions crates/trusted-server-js/lib/src/integrations/lockr/script_guard.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createScriptGuard } from '../../shared/script_guard';
import { parseSdkNetworkUrl } from '../../shared/sdk_url';

/**
* Lockr SDK Script Interception Guard
Expand All @@ -17,28 +18,18 @@ import { createScriptGuard } from '../../shared/script_guard';

/**
* Check if a URL is a Lockr SDK URL.
* Matches the logic from lockr.rs:79-86
* Browser/Rust ownership cases live in test/fixtures/sdk-url-ownership.json.
*/
function isLockrSdkUrl(url: string): boolean {
if (!url) return false;

const lower = url.toLowerCase();

// Check for aim.loc.kr domain
if (lower.includes('aim.loc.kr')) {
return true;
}

// Check for identity.loc.kr with identity-lockr and .js extension
if (
lower.includes('identity.loc.kr') &&
lower.includes('identity-lockr') &&
lower.endsWith('.js')
) {
return true;
}

return false;
const parsed = parseSdkNetworkUrl(url);
if (!parsed) return false;

const path = parsed.pathname.toLowerCase();
return (
(parsed.hostname === 'aim.loc.kr' || parsed.hostname === 'identity.loc.kr') &&
path.includes('identity-lockr') &&
path.endsWith('.js')
);
}

const guard = createScriptGuard({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { createScriptGuard } from '../../shared/script_guard';
import { parseSdkNetworkUrl } from '../../shared/sdk_url';

/**
* Check if a URL is a Permutive SDK URL.
* Matches the logic from permutive.rs:97-101
* Browser/Rust ownership cases live in test/fixtures/sdk-url-ownership.json.
*/
function isPermutiveSdkUrl(url: string): boolean {
if (!url) return false;
const parsed = parseSdkNetworkUrl(url);
if (!parsed) return false;

const lower = url.toLowerCase();
const host = parsed.hostname;
const edgeSuffix = '.edge.permutive.app';
return (
(lower.includes('.edge.permutive.app') || lower.includes('cdn.permutive.com')) &&
lower.endsWith('-web.js')
(host === 'cdn.permutive.com' ||
(host.endsWith(edgeSuffix) && host.length > edgeSuffix.length)) &&
parsed.pathname.toLowerCase().endsWith('-web.js')
);
}

Expand Down
16 changes: 16 additions & 0 deletions crates/trusted-server-js/lib/src/shared/sdk_url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Parse an HTTP(S) SDK URL with an explicit authority, or a protocol-relative URL.
*
* Uses the same ASCII whitespace and prefix policy as Rust's SDK URL parser.
* Browser/Rust ownership cases live in test/fixtures/sdk-url-ownership.json.
*/
export function parseSdkNetworkUrl(url: string): URL | null {
const value = url.replace(/^[ \t\n\r\f\v]+|[ \t\n\r\f\v]+$/g, '');
if (!/^(?:https?:)?\/\//i.test(value)) return null;

try {
return new URL(value.startsWith('//') ? `https:${value}` : value);
} catch {
return null;
}
}
Loading
Loading