From 0eda2fbcaa1c8843d3a6aeb39d8773036f85f4d2 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 4 Nov 2026 09:56:58 -0600 Subject: [PATCH 1/3] Extract lockr app IDs from audit page metadata --- .../src/commands/audit/generate/analyzer.rs | 20 +++ .../src/commands/audit/generate/mod.rs | 167 ++++++++++++++++++ 2 files changed, 187 insertions(+) diff --git a/crates/trusted-server-cli/src/commands/audit/generate/analyzer.rs b/crates/trusted-server-cli/src/commands/audit/generate/analyzer.rs index f1255a21f9..9f5e0327d9 100644 --- a/crates/trusted-server-cli/src/commands/audit/generate/analyzer.rs +++ b/crates/trusted-server-cli/src/commands/audit/generate/analyzer.rs @@ -11,6 +11,8 @@ use crate::commands::audit::generate::{ }; use crate::error::{CliResult, report_error}; +const LOCKR_APP_ID_SELECTOR: &str = "meta[name=\"lockr-signin-app_id\"]"; + static GTM_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"\bGTM-[A-Z0-9]{4,20}\b").expect("should compile GTM regex")); static GPT_INLINE_REGEX: LazyLock = LazyLock::new(|| { @@ -65,6 +67,18 @@ pub(crate) fn analyze_collected_page(collected: &CollectedPage) -> CliResult CliResult, pub(crate) js_asset_count: usize, pub(crate) third_party_asset_count: usize, + /// Public application ID discovered in lockr page metadata. + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) lockr_app_id: Option, pub(crate) detected_integrations: Vec, pub(crate) assets: Vec, pub(crate) warnings: Vec, @@ -494,6 +497,13 @@ fn build_draft_config_with_generator( } } + if let Some(app_id) = &artifact.lockr_app_id { + draft + .push_str("\n# Verify lockr SDK/API behavior on the serving domain before enabling.\n"); + draft.push_str("[integrations.lockr]\nenabled = false\n"); + draft.push_str(&format!("app_id = {}\n", toml_string(app_id))); + } + for integration in detected { if !matches!( integration, @@ -2256,6 +2266,154 @@ mod tests { assert!(summary.contains("Draft config: review before validation and push")); } + #[test] + fn run_generate_extracts_lockr_app_id_from_page_metadata() { + for (metadata, expected_id) in [ + ( + r#""#, + "example-app-id", + ), + ( + r#""#, + "example-app-id", + ), + ( + r#""#, + "example-\"app\\id\nline", + ), + ] { + for with_script in [false, true] { + let temp = TempDir::new().expect("should create temp dir"); + let mut args = audit_args("https://publisher.example/page"); + args.js_assets = Some(temp.path().join("js-assets.toml")); + args.config = Some(temp.path().join("trusted-server.toml")); + let mut page = collected_page(); + page.html = format!("{metadata}"); + page.script_tags = if with_script { + vec![CollectedScriptTag { + src: None, + inline_text: Some("window.lockr = {};".to_string()), + }] + } else { + Vec::new() + }; + page.network_requests.clear(); + + run_generate(&args, &FakeCollector::new(page), &mut Vec::new()) + .expect("should generate outputs from offline lockr page"); + + let artifact_text = + fs::read_to_string(args.js_assets.as_ref().expect("should set audit path")) + .expect("should read audit artifact"); + let artifact: toml::Value = + toml::from_str(&artifact_text).expect("should parse audit artifact"); + assert_eq!( + artifact["lockr_app_id"].as_str(), + Some(expected_id), + "should persist the normalized public app ID" + ); + let integrations = artifact["detected_integrations"] + .as_array() + .expect("should record detected integrations"); + assert_eq!(integrations.len(), 1, "should detect lockr exactly once"); + assert_eq!(integrations[0]["id"].as_str(), Some("lockr")); + assert_eq!( + integrations[0]["evidence"].as_str(), + Some("meta[name=\"lockr-signin-app_id\"]"), + "should retain metadata evidence even when a script also detects lockr" + ); + assert_eq!(artifact["js_asset_count"].as_integer(), Some(0)); + + let draft = + fs::read_to_string(args.config.as_ref().expect("should set config path")) + .expect("should read generated config"); + let config: toml::Value = + toml::from_str(&draft).expect("should parse generated config"); + let lockr = &config["integrations"]["lockr"]; + assert_eq!(lockr["app_id"].as_str(), Some(expected_id)); + assert_eq!( + lockr["enabled"].as_bool(), + Some(false), + "discovering an ID should not enable lockr" + ); + assert!( + draft.contains("# - Detected lockr;"), + "should retain manual review" + ); + let warnings = validate::check_candidate(&draft, &draft) + .expect("should validate generated config"); + assert!( + warnings.is_empty(), + "should pass deploy validation: {warnings:?}" + ); + } + } + } + + #[test] + fn run_generate_keeps_lockr_without_app_id_on_manual_review_path() { + for (metadata, with_script) in [ + ("", true), + (r#""#, true), + (r#""#, true), + ( + r#""#, + true, + ), + ( + r#""#, + true, + ), + (r#""#, false), + (r#""#, false), + ( + r#""#, + false, + ), + ] { + let temp = TempDir::new().expect("should create temp dir"); + let mut args = audit_args("https://publisher.example/page"); + args.js_assets = Some(temp.path().join("js-assets.toml")); + args.config = Some(temp.path().join("trusted-server.toml")); + let mut page = collected_page(); + page.html = format!("{metadata}"); + page.script_tags = if with_script { + vec![CollectedScriptTag { + src: None, + inline_text: Some("window.lockr = {};".to_string()), + }] + } else { + Vec::new() + }; + page.network_requests.clear(); + + run_generate(&args, &FakeCollector::new(page), &mut Vec::new()) + .expect("should generate outputs without a lockr app ID"); + + let artifact_text = + fs::read_to_string(args.js_assets.as_ref().expect("should set audit path")) + .expect("should read audit artifact"); + let artifact: toml::Value = + toml::from_str(&artifact_text).expect("should parse audit artifact"); + assert!( + artifact.get("lockr_app_id").is_none(), + "should not invent an ID" + ); + let draft = fs::read_to_string(args.config.as_ref().expect("should set config path")) + .expect("should read generated config"); + let config: toml::Value = + toml::from_str(&draft).expect("should parse generated config"); + assert!( + config["integrations"].get("lockr").is_none(), + "should not activate a lockr section without an ID" + ); + assert!( + draft.contains("# - Detected lockr;"), + "should retain manual review" + ); + } + } + #[test] fn run_generate_respects_no_config() { let temp = TempDir::new().expect("should create temp dir"); @@ -2375,6 +2533,7 @@ mod tests { fn build_draft_config_writes_disabled_js_asset_proxy_candidates() { let url = Url::parse("https://publisher.example.com/page").expect("should parse URL"); let artifact = AuditArtifact { + lockr_app_id: None, audited_url: url.to_string(), page_title: Some("Example".to_string()), js_asset_count: 2, @@ -2457,6 +2616,7 @@ mod tests { fn generated_asset_proxy_paths_are_opaque() { let url = Url::parse("https://publisher.example.com/page").expect("should parse URL"); let artifact = AuditArtifact { + lockr_app_id: None, audited_url: url.to_string(), page_title: None, js_asset_count: 1, @@ -2497,6 +2657,7 @@ mod tests { fn asset_proxy_generation_deduplicates_and_summarizes_skips() { let url = Url::parse("https://publisher.example.com/page").expect("should parse URL"); let artifact = AuditArtifact { + lockr_app_id: None, audited_url: url.to_string(), page_title: None, js_asset_count: 4, @@ -2554,6 +2715,7 @@ mod tests { fn asset_proxy_generation_warns_about_query_string_candidates() { let url = Url::parse("https://publisher.example.com/page").expect("should parse URL"); let artifact = AuditArtifact { + lockr_app_id: None, audited_url: url.to_string(), page_title: None, js_asset_count: 2, @@ -2601,6 +2763,7 @@ mod tests { fn asset_proxy_generation_with_no_candidates_removes_placeholder_asset() { let url = Url::parse("https://publisher.example.com/page").expect("should parse URL"); let artifact = AuditArtifact { + lockr_app_id: None, audited_url: url.to_string(), page_title: None, js_asset_count: 1, @@ -2663,6 +2826,7 @@ mod tests { fn build_draft_config_uses_final_url_and_detected_integrations() { let url = Url::parse("https://www.publisher.example:8443/path").expect("should parse URL"); let artifact = AuditArtifact { + lockr_app_id: None, audited_url: url.to_string(), page_title: Some("Example".to_string()), js_asset_count: 2, @@ -2702,6 +2866,7 @@ mod tests { fn build_draft_config_extracts_gtm_container_id_from_url_evidence() { let url = Url::parse("https://publisher.example.com").expect("should parse URL"); let artifact = AuditArtifact { + lockr_app_id: None, audited_url: url.to_string(), page_title: None, js_asset_count: 0, @@ -2745,6 +2910,7 @@ mod tests { "https://tags.example.com/gtm.js?id=GTM-ABCDEFGHIJKLMNOPQRSTU", ] { let artifact = AuditArtifact { + lockr_app_id: None, audited_url: url.to_string(), page_title: None, js_asset_count: 1, @@ -2789,6 +2955,7 @@ mod tests { fn build_draft_config_does_not_enable_gtm_without_container_id() { let url = Url::parse("https://publisher.example/path").expect("should parse URL"); let artifact = AuditArtifact { + lockr_app_id: None, audited_url: url.to_string(), page_title: None, js_asset_count: 1, From a0a8d1367ef20a0ce0c5f33e355ca9e89f7487bf Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 4 Nov 2026 10:12:07 -0600 Subject: [PATCH 2/3] Document lockr app ID extraction during audits --- docs/guide/cli.md | 14 ++++++++++++++ docs/guide/integrations/lockr.md | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/docs/guide/cli.md b/docs/guide/cli.md index 70227ca493..46ab38348a 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -342,6 +342,20 @@ register routes or rewrite HTML until you set runtime-injected scripts; JS Asset Proxy only rewrites matching script `src` URLs present in HTML processed by Trusted Server. +The audit also extracts the public lockr app ID from rendered page metadata: + +```html + +``` + +It trims the `content` value and records a nonempty ID as `lockr_app_id` in +`js-assets.toml`, alongside the metadata source evidence. The draft config fills +`[integrations.lockr].app_id` but explicitly sets `enabled = false`. Missing or +empty IDs are not populated; detected lockr remains on the manual-review path. +Finding an ID does not verify SDK/API behavior on another serving domain. +Complete the [Lockr verification checks](/guide/integrations/lockr#verification) +before enabling it. + If a config already exists, avoid overwriting it: ```bash diff --git a/docs/guide/integrations/lockr.md b/docs/guide/integrations/lockr.md index f6831545f3..ee4e1998da 100644 --- a/docs/guide/integrations/lockr.md +++ b/docs/guide/integrations/lockr.md @@ -14,6 +14,11 @@ app_id = "example-lockr-app-id" The integration is off when the section is absent. When the section exists, `enabled` defaults to `true` and `app_id` must be nonempty. Use the application ID from your existing `lockr-signin-app_id` metadata. Keep the publisher's SDK loading, metadata, and initialization in place. The integration does not inject them. +To discover the ID automatically, run +[`ts audit generate `](/guide/cli#audit-a-public-page). It fills `app_id` from +nonempty page metadata but explicitly leaves the generated integration disabled. +Verify SDK/API behavior on the intended serving domain before enabling it. + | Field | Default | Behavior | | ------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------- | | `enabled` | `true` | Enable the integration when its section exists | From 41dcf1ffda9b3be9eca1444692987ad159f16950 Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 10 Nov 2026 10:20:15 -0600 Subject: [PATCH 3/3] Address lockr audit review feedback --- .../src/commands/audit/generate/analyzer.rs | 54 +++++++++++++++- .../src/commands/audit/generate/mod.rs | 64 +++++++++++++++++-- docs/guide/cli.md | 20 ++++-- docs/guide/integrations/lockr.md | 6 +- 4 files changed, 126 insertions(+), 18 deletions(-) diff --git a/crates/trusted-server-cli/src/commands/audit/generate/analyzer.rs b/crates/trusted-server-cli/src/commands/audit/generate/analyzer.rs index 9f5e0327d9..8d9ca7137d 100644 --- a/crates/trusted-server-cli/src/commands/audit/generate/analyzer.rs +++ b/crates/trusted-server-cli/src/commands/audit/generate/analyzer.rs @@ -73,11 +73,19 @@ pub(crate) fn analyze_collected_page(collected: &CollectedPage) -> CliResult"# + ), + script_tags: Vec::new(), + network_requests: Vec::new(), + gpt_slots: Vec::new(), + links: Vec::new(), + sitemap_locs: Vec::new(), + warnings: vec!["existing warning".to_string()], + }; + + let artifact = + analyze_collected_page(&collected).expect("should analyze collected page"); + + assert_eq!( + artifact.lockr_app_id.as_deref(), + Some("first-app-id"), + "should retain the first nonempty normalized ID" + ); + let mut expected_warnings = vec!["existing warning".to_string()]; + if conflicting { + expected_warnings.push( + "page carries more than one distinct lockr app ID; the draft uses the first" + .to_string(), + ); + } + assert_eq!( + artifact.warnings, expected_warnings, + "should preserve existing warnings and warn once only for distinct IDs without echoing them" + ); + } + } + #[test] fn analyze_collected_page_uses_html_title_when_browser_title_absent() { let collected = CollectedPage { diff --git a/crates/trusted-server-cli/src/commands/audit/generate/mod.rs b/crates/trusted-server-cli/src/commands/audit/generate/mod.rs index ee302938b8..eaabe0b3f7 100644 --- a/crates/trusted-server-cli/src/commands/audit/generate/mod.rs +++ b/crates/trusted-server-cli/src/commands/audit/generate/mod.rs @@ -457,10 +457,14 @@ fn build_draft_config_with_generator( } if let Some(app_id) = &artifact.lockr_app_id { - draft - .push_str("\n# Verify lockr SDK/API behavior on the serving domain before enabling.\n"); - draft.push_str("[integrations.lockr]\nenabled = false\n"); - draft.push_str(&format!("app_id = {}\n", toml_string(app_id))); + draft = draft.replacen( + "# [integrations.lockr]\n# enabled = true\n# app_id = \"your-lockr-app-id\"", + &format!( + "# Enable lockr on staging and verify SDK/API behavior before production use.\n[integrations.lockr]\nenabled = false\napp_id = {}", + toml_string(app_id), + ), + 1, + ); } for integration in detected { @@ -2417,13 +2421,21 @@ mod tests { .as_array() .expect("should record detected integrations"); assert_eq!(integrations.len(), 1, "should detect lockr exactly once"); - assert_eq!(integrations[0]["id"].as_str(), Some("lockr")); + assert_eq!( + integrations[0]["id"].as_str(), + Some("lockr"), + "should record lockr as the detected integration" + ); assert_eq!( integrations[0]["evidence"].as_str(), Some("meta[name=\"lockr-signin-app_id\"]"), "should retain metadata evidence even when a script also detects lockr" ); - assert_eq!(artifact["js_asset_count"].as_integer(), Some(0)); + assert_eq!( + artifact["js_asset_count"].as_integer(), + Some(0), + "should not count inline scripts as JS assets" + ); let draft = fs::read_to_string(args.config.as_ref().expect("should set config path")) @@ -2431,7 +2443,11 @@ mod tests { let config: toml::Value = toml::from_str(&draft).expect("should parse generated config"); let lockr = &config["integrations"]["lockr"]; - assert_eq!(lockr["app_id"].as_str(), Some(expected_id)); + assert_eq!( + lockr["app_id"].as_str(), + Some(expected_id), + "should write the normalized app ID to the draft config" + ); assert_eq!( lockr["enabled"].as_bool(), Some(false), @@ -2447,6 +2463,40 @@ mod tests { warnings.is_empty(), "should pass deploy validation: {warnings:?}" ); + let enabled = draft.replacen( + "[integrations.lockr]\nenabled = false\n", + "[integrations.lockr]\nenabled = true\n", + 1, + ); + assert_ne!(enabled, draft, "should find the generated lockr section"); + validate::check_candidate(&enabled, &draft) + .expect("should pass deploy validation once lockr is enabled"); + assert!( + !draft.contains("# [integrations.lockr]"), + "should fill the template's lockr block instead of retaining a second example" + ); + assert_eq!( + draft + .lines() + .filter(|line| *line == "[integrations.lockr]") + .count(), + 1, + "should generate exactly one lockr table" + ); + let optional_setting = draft.replacen( + "# api_endpoint = \"https://identity.example.com\"", + "api_endpoint = \"https://identity.example.com\"", + 1, + ); + let with_optional: toml::Value = toml::from_str(&optional_setting) + .expect("should parse after uncommenting a lockr setting"); + assert_eq!( + with_optional["integrations"]["lockr"]["api_endpoint"].as_str(), + Some("https://identity.example.com"), + "should keep optional settings in the generated lockr section" + ); + validate::check_candidate(&optional_setting, &draft) + .expect("should validate after uncommenting a lockr setting"); } } } diff --git a/docs/guide/cli.md b/docs/guide/cli.md index 46ab38348a..e8c37d1b77 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -348,13 +348,19 @@ The audit also extracts the public lockr app ID from rendered page metadata: ``` -It trims the `content` value and records a nonempty ID as `lockr_app_id` in -`js-assets.toml`, alongside the metadata source evidence. The draft config fills -`[integrations.lockr].app_id` but explicitly sets `enabled = false`. Missing or -empty IDs are not populated; detected lockr remains on the manual-review path. -Finding an ID does not verify SDK/API behavior on another serving domain. -Complete the [Lockr verification checks](/guide/integrations/lockr#verification) -before enabling it. +It trims the `content` value and records the first nonempty ID as `lockr_app_id` +in `js-assets.toml`, alongside the metadata source evidence. If the page contains +distinct nonempty IDs, the artifact includes a warning but retains the first ID. +The draft config fills the template's `[integrations.lockr].app_id` but explicitly +sets `enabled = false`. Missing or empty IDs are not populated; detected lockr +remains on the manual-review path. + +Finding an ID does not verify SDK/API behavior on another serving domain. If +Trusted Server serves a hostname that is not registered with lockr, set +`origin_override` to the publisher's authorized, registered origin. Enable lockr +on a staging service and complete the +[Lockr verification checks](/guide/integrations/lockr#verification) before enabling +it in production. The checks require the integration's routes to be enabled. If a config already exists, avoid overwriting it: diff --git a/docs/guide/integrations/lockr.md b/docs/guide/integrations/lockr.md index 826b91d53b..d16bff6ad9 100644 --- a/docs/guide/integrations/lockr.md +++ b/docs/guide/integrations/lockr.md @@ -17,7 +17,11 @@ The integration is off when the section is absent. When the section exists, `ena To discover the ID automatically, run [`ts audit generate `](/guide/cli#audit-a-public-page). It fills `app_id` from nonempty page metadata but explicitly leaves the generated integration disabled. -Verify SDK/API behavior on the intended serving domain before enabling it. +If Trusted Server serves a hostname that is not registered with Lockr, set +`origin_override` to the publisher's authorized, registered origin. Enable the +integration on a staging service and run the [verification](#verification) checks +before enabling it in production. The checks require the integration's routes to +be enabled. | Field | Default | Behavior | | ------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------- |