Skip to main content

perimeterx_fastly_enforcer/
pxconfig.rs

1#[cfg(feature = "kv_store")]
2use crate::modules::kvstore_ext::KVStoreGetExt;
3use crate::modules::pxconstants::{
4    CUSTOM_COOKIE_HEADER, GRAPHQL_BODY_MAX_LENGTH, MCP_ENDPOINT_PATH, WHITELIST_EXT,
5};
6use crate::px_error;
7use crate::pxcontext::{PXContext, PXModuleMode, TokenVersion};
8#[cfg(target_arch = "wasm32")]
9use fastly::secret_store::SecretStore;
10#[cfg(not(feature = "kv_store"))]
11use fastly::ConfigStore;
12#[cfg(feature = "kv_store")]
13use fastly::KVStore;
14use fastly::{Request, Response};
15use regex::Regex;
16use serde::{
17    de::{self, Deserializer, MapAccess, Visitor},
18    ser::{SerializeMap, Serializer},
19    Deserialize, Serialize,
20};
21use std::collections::HashSet;
22use std::fmt;
23use strum::{EnumProperty, IntoEnumIterator};
24use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty as EnumPropertyMacro};
25
26/// Custom parameters structure
27#[derive(Default, Clone)]
28pub struct PXCustomParams {
29    pub custom_param1: String,
30    pub custom_param2: String,
31    pub custom_param3: String,
32    pub custom_param4: String,
33    pub custom_param5: String,
34    pub custom_param6: String,
35    pub custom_param7: String,
36    pub custom_param8: String,
37    pub custom_param9: String,
38    pub custom_param10: String,
39}
40
41/// callback function to fill PXCustomParams structure
42pub type PXEnrichCustomParamsFn = fn(req: &Request, conf: &PXConfig, params: &mut PXCustomParams);
43/// callback function to determine if a request is sensitive
44pub type PXIsSensitiveRequestFn = fn(req: &Request, conf: &PXConfig) -> bool;
45/// callback function to determine if a request should be filtered
46pub type PXIsFilteredRequestFn = fn(req: &Request, conf: &PXConfig) -> bool;
47/// callback function to determine if a request should be enforced
48pub type PXIsEnforcedRequestFn = fn(req: &Request, conf: &PXConfig) -> bool;
49/// callback function to determine if a request should be monitored
50pub type PXIsMonitoredRequestFn = fn(req: &Request, conf: &PXConfig) -> bool;
51/// callback function executed after sending page_requested or block activity to the collector
52pub type PXAdditionalActivityHandlerFn = fn(req: &Request, conf: &PXConfig, ctx: &PXContext);
53/// callback function to create a custom preflight response
54pub type PXCorsCustomPreflightHandlerFn = fn(req: &Request, conf: &PXConfig) -> Option<Response>;
55/// callback function to create custom CORS headers for block responses
56pub type PXCorsCustomBlockResponseHeadersFn =
57    fn(req: &Request, conf: &PXConfig) -> Vec<(String, String)>;
58
59/// Raw credentials extracted from a login request (before hashing).
60#[derive(Debug, Clone, Default, PartialEq, Eq)]
61pub struct PXRawCredentials {
62    pub user: Option<String>,
63    pub pass: Option<String>,
64}
65
66impl PXRawCredentials {
67    /// Treat empty strings as missing credential fields.
68    pub fn without_empty_fields(mut self) -> Self {
69        if self.user.as_deref().is_some_and(str::is_empty) {
70            self.user = None;
71        }
72        if self.pass.as_deref().is_some_and(str::is_empty) {
73            self.pass = None;
74        }
75        self
76    }
77}
78
79/// Custom credential extraction for `sent_through: custom` endpoints.
80pub type PXExtractCredentialsFn =
81    fn(req: &Request, endpoint_index: usize) -> Option<PXRawCredentials>;
82
83/// Custom login-success evaluation for `login_successful_reporting_method: custom`.
84pub type PXLoginSuccessfulFn = fn(resp: &Response, endpoint_index: usize) -> Option<bool>;
85
86/// Credential endpoint configuration object (`px_login_credentials_extraction` item).
87#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
88pub struct PXCredentialEndpointConfig {
89    pub path: String,
90    #[serde(default)]
91    pub path_type: String,
92    pub method: String,
93    #[serde(default)]
94    pub sent_through: String,
95    #[serde(default)]
96    pub user_field: String,
97    #[serde(default)]
98    pub pass_field: String,
99    #[serde(default)]
100    pub protocol: String,
101    #[serde(default)]
102    pub login_successful_reporting_method: String,
103    #[serde(default)]
104    pub login_successful_statuses: Vec<u16>,
105    #[serde(default)]
106    pub login_successful_body_regex: String,
107    #[serde(default)]
108    pub login_successful_header_name: String,
109    #[serde(default)]
110    pub login_successful_header_value: String,
111}
112
113/// Runtime-compiled credential endpoint (regex path, etc.).
114#[derive(Clone)]
115pub struct PXPreparedCredentialEndpoint {
116    pub config: PXCredentialEndpointConfig,
117    pub path_regex: Option<Regex>,
118}
119
120/// Stringly-typed key for every configurable field on [`PXConfig`].
121///
122/// Each variant's [`AsRef<str>`] / [`Display`] representation is the canonical
123/// snake-case name with a leading `px_` prefix — matching both the
124/// ConfigStore/KVStore key used to populate the field and the JSON key used by
125/// the custom `Serialize`/`Deserialize` impls below.
126///
127/// Special flags:
128/// - `#[strum(props(static = "true"))]`: static fields that are included in the static configuration
129/// - `#[strum(props(sensitive = "true"))]`: sensitive fields that are redacted in the serialized output
130/// - `#[strum(props(required = "true"))]`: required fields that must be present in the configuration
131/// - `#[strum(props(local = "true"))]`: local fields that are only available in the local Enforcer
132#[derive(
133    Display, AsRefStr, EnumIter, EnumPropertyMacro, Clone, Copy, PartialEq, Eq, Hash, Debug,
134)]
135#[strum(serialize_all = "snake_case")]
136#[strum(prefix = "px_")]
137#[doc(hidden)]
138pub enum PXConfigKey {
139    #[strum(props(static = "true", required = "true"))]
140    AppId,
141    #[strum(props(sensitive = "true", static = "true", required = "true"))]
142    CookieSecret,
143    #[strum(props(sensitive = "true", static = "true", required = "true"))]
144    AuthToken,
145    Debug,
146    BlockingScore,
147    ModuleEnabled,
148    ModuleMode,
149    SensitiveHeaders,
150    SensitiveRoutes,
151    SensitiveRoutesRegex,
152    FilterByRoute,
153    FilterByExtension,
154    FilterByUserAgent,
155    FilterByIp,
156    FilterByHttpMethod,
157    CustomCookieHeader,
158    EnforcedRoutes,
159    MonitoredRoutes,
160    BypassMonitorHeader,
161    FirstPartyEnabled,
162    CustomLogo,
163    JsRef,
164    CssRef,
165    #[strum(props(local = "true"))]
166    HumanSapiHost,
167    #[strum(props(local = "true"))]
168    HumanSapiBackend,
169    #[strum(props(local = "true"))]
170    HumanCollectorHost,
171    #[strum(props(local = "true"))]
172    HumanCollectorBackend,
173    #[strum(props(local = "true"))]
174    HumanClientHost,
175    #[strum(props(local = "true"))]
176    HumanClientBackend,
177    #[strum(props(local = "true"))]
178    HumanCaptchaHost,
179    #[strum(props(local = "true"))]
180    HumanCaptchaBackend,
181    IpHeaders,
182    LogEndpoint,
183    DataEnrichmentHeaderName,
184    ExtractedCookies,
185    CorsSupportEnabled,
186    CorsPreflightRequestFilterEnabled,
187    GraphqlEnabled,
188    GraphqlRoutes,
189    SensitiveGraphqlOperationNames,
190    SensitiveGraphqlOperationTypes,
191    GraphqlBodyMaxLength,
192    GraphqlKeywords,
193    S2sTimeout,
194    TokenVersion,
195    CustomFirstPartyCaptchaEndpoint,
196    CustomFirstPartySensorEndpoint,
197    CustomFirstPartyXhrEndpoint,
198    UserAgentMaxLength,
199    RiskCookieMaxLength,
200    RiskCookieMinIterations,
201    RiskCookieMaxIterations,
202    AgenticTrustEnabled,
203    AgenticTrustMcpEndpointPath,
204    SecuredPxhdEnabled,
205    PxhdDomain,
206    JwtCookieName,
207    JwtCookieUserIdFieldName,
208    JwtCookieAdditionalFieldNames,
209    JwtHeaderName,
210    JwtHeaderUserIdFieldName,
211    JwtHeaderAdditionalFieldNames,
212    #[strum(props(sensitive = "true", static = "true"))]
213    LoggerAuthToken,
214    LoginCredentialsExtractionEnabled,
215    LoginCredentialsExtraction,
216    CredentialsIntelligenceVersion,
217    CompromisedCredentialsHeader,
218    SendRawUsernameOnAdditionalS2sActivity,
219    AdditionalS2sActivityEnabled,
220    AdditionalS2sActivityHeaderEnabled,
221    LoginSuccessfulReportingMethod,
222    LoginSuccessfulBodyRegex,
223    LoginSuccessfulHeaderName,
224    LoginSuccessfulHeaderValue,
225    LoginSuccessfulStatus,
226}
227
228impl PXConfigKey {
229    /// Returns `true` if this key represents a sensitive field whose value
230    /// must be redacted before being serialized to telemetry / logs.
231    pub fn is_sensitive(&self) -> bool {
232        self.get_str("sensitive") == Some("true")
233    }
234
235    /// Returns `true` if this key is marked as static (e.g. for minimal
236    /// config payloads such as startup telemetry).
237    pub fn is_static(&self) -> bool {
238        self.get_str("static") == Some("true")
239    }
240
241    /// Returns `true` if this key must be configured before the enforcer can
242    /// operate correctly.
243    pub fn is_required(&self) -> bool {
244        self.get_str("required") == Some("true")
245    }
246
247    /// Returns `true` if this key is local-only and should be excluded from
248    /// serialized output (e.g. telemetry payloads sent to the collector).
249    pub fn is_local(&self) -> bool {
250        self.get_str("local") == Some("true")
251    }
252}
253
254/// Typed view of a value pulled from [`PXConfig`] via [`PXConfig::get`] or
255/// [`PXConfig::fields`]. Borrows from the backing config so it is cheap to
256/// produce.
257pub enum PXConfigValue<'a> {
258    Bool(bool),
259    U8(u8),
260    U16(u16),
261    U16Vec(&'a [u16]),
262    U32(u32),
263    USize(usize),
264    Str(&'a str),
265    StrVec(&'a [String]),
266    RegexVec(&'a [Regex]),
267    ModuleMode(PXModuleMode),
268    TokenVersion(TokenVersion),
269    CiEndpoints(&'a [PXCredentialEndpointConfig]),
270}
271
272impl fmt::Display for PXConfigValue<'_> {
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        match self {
275            Self::Bool(v) => write!(f, "Bool({v})"),
276            Self::U8(v) => write!(f, "U8({v})"),
277            Self::U16(v) => write!(f, "U16({v})"),
278            Self::U16Vec(v) => write!(f, "U16Vec({v:?})"),
279            Self::U32(v) => write!(f, "U32({v})"),
280            Self::USize(v) => write!(f, "USize({v})"),
281            Self::Str(v) => write!(f, "Str({v})"),
282            Self::StrVec(v) => write!(f, "StrVec({v:?})"),
283            Self::RegexVec(v) => {
284                let pats: Vec<&str> = v.iter().map(Regex::as_str).collect();
285                write!(f, "RegexVec({pats:?})")
286            }
287            Self::ModuleMode(v) => write!(f, "ModuleMode({})", v.as_ref()),
288            Self::TokenVersion(v) => write!(f, "TokenVersion({})", v.as_ref()),
289            Self::CiEndpoints(v) => write!(f, "CiEndpoints({v:?})"),
290        }
291    }
292}
293
294impl Serialize for PXConfigValue<'_> {
295    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
296        match self {
297            Self::Bool(v) => v.serialize(s),
298            Self::U8(v) => v.serialize(s),
299            Self::U16(v) => v.serialize(s),
300            Self::U16Vec(v) => v.serialize(s),
301            Self::U32(v) => v.serialize(s),
302            Self::USize(v) => v.serialize(s),
303            Self::Str(v) => v.serialize(s),
304            Self::StrVec(v) => v.serialize(s),
305            Self::RegexVec(v) => {
306                let pats: Vec<String> = v
307                    .iter()
308                    .map(|r| format!("{REGEX_PREFIX}{}", r.as_str()))
309                    .collect();
310                pats.serialize(s)
311            }
312            Self::ModuleMode(v) => v.serialize(s),
313            Self::TokenVersion(v) => v.serialize(s),
314            Self::CiEndpoints(v) => v.serialize(s),
315        }
316    }
317}
318
319const REDACTED: &str = "***REDACTED***";
320const REDACT_THRESHOLD: usize = 50;
321const REDACT_TAIL_LEN: usize = 5;
322pub(crate) const REGEX_PREFIX: &str = "_REGEXP ";
323
324fn redact(s: &str) -> String {
325    let char_count = s.chars().count();
326    if char_count >= REDACT_THRESHOLD {
327        let tail: String = s.chars().skip(char_count - REDACT_TAIL_LEN).collect();
328        format!("{REDACTED}{tail}")
329    } else if char_count > 0 {
330        REDACTED.to_owned()
331    } else {
332        s.to_owned()
333    }
334}
335
336struct Redacted<'a>(&'a PXConfigValue<'a>);
337
338impl Serialize for Redacted<'_> {
339    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
340        match self.0 {
341            PXConfigValue::Bool(v) => redact(&v.to_string()).serialize(s),
342            PXConfigValue::U8(v) => redact(&v.to_string()).serialize(s),
343            PXConfigValue::U16(v) => redact(&v.to_string()).serialize(s),
344            PXConfigValue::U16Vec(v) => v
345                .iter()
346                .map(|n| redact(&n.to_string()))
347                .collect::<Vec<_>>()
348                .serialize(s),
349            PXConfigValue::U32(v) => redact(&v.to_string()).serialize(s),
350            PXConfigValue::USize(v) => redact(&v.to_string()).serialize(s),
351            PXConfigValue::Str(v) => redact(v).serialize(s),
352            PXConfigValue::StrVec(v) => v
353                .iter()
354                .map(|item| redact(item))
355                .collect::<Vec<_>>()
356                .serialize(s),
357            PXConfigValue::RegexVec(v) => v
358                .iter()
359                .map(|item| redact(item.as_str()))
360                .collect::<Vec<_>>()
361                .serialize(s),
362            PXConfigValue::ModuleMode(v) => redact(v.as_ref()).serialize(s),
363            PXConfigValue::TokenVersion(v) => redact(v.as_ref()).serialize(s),
364            PXConfigValue::CiEndpoints(v) => v.serialize(s),
365        }
366    }
367}
368
369impl Serialize for PXModuleMode {
370    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
371        s.serialize_str(self.as_ref())
372    }
373}
374
375impl<'de> Deserialize<'de> for PXModuleMode {
376    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
377        let s = String::deserialize(d)?;
378        s.parse().map_err(de::Error::custom)
379    }
380}
381
382impl Serialize for TokenVersion {
383    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
384        s.serialize_str(self.as_ref())
385    }
386}
387
388impl<'de> Deserialize<'de> for TokenVersion {
389    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
390        let s = String::deserialize(d)?;
391        s.parse().map_err(de::Error::custom)
392    }
393}
394
395/// Enforcer configuration.
396///
397/// Configuration must be set before calling `PXEnforcer::enforce()`.
398///
399/// Most fields are populated from the underlying ConfigStore / KVStore via
400/// [`PXConfig::new`]. Callback function fields are runtime-only and excluded
401/// from iteration / serialization.
402pub struct PXConfig {
403    #[cfg(not(feature = "kv_store"))]
404    /// Backing Fastly ConfigStore used to load `px_*` keys at startup.
405    pub store: Option<ConfigStore>,
406    #[cfg(feature = "kv_store")]
407    /// Backing Fastly KVStore used to load and persist `px_*` keys.
408    pub store: Option<KVStore>,
409
410    /// `px_app_id`: HUMAN application ID used in activities, Risk API calls, and derived hosts.
411    pub app_id: String,
412    /// `px_cookie_secret`: secret used to validate risk cookies, PXDE cookies, and telemetry commands.
413    pub cookie_secret: String,
414    /// `px_auth_token`: bearer token used when sending Risk API, async activity, and telemetry requests.
415    pub auth_token: String,
416    /// `px_debug`: enables verbose PerimeterX debug logs in this Fastly implementation.
417    pub debug: bool,
418    /// `px_blocking_score`: minimum score, inclusive, that should block for Cookie V3/Risk API flows.
419    pub blocking_score: u8,
420    /// `px_module_enabled`: master on/off switch; disabled modules pass requests without verification.
421    pub module_enabled: bool,
422    /// `px_module_mode`: active blocking blocks high-risk requests; monitor simulates blocks and passes.
423    pub module_mode: PXModuleMode,
424    /// `px_sensitive_headers`: request header names removed from Risk API and async activity payloads.
425    pub sensitive_headers: Vec<String>,
426    /// `px_sensitive_routes`: path prefixes that always trigger Risk API after valid low-risk cookies.
427    pub sensitive_routes: Vec<String>,
428    /// `px_sensitive_routes_regex`: compiled regex routes with the same sensitive-route semantics.
429    pub sensitive_routes_regex: Vec<Regex>,
430    /// `px_filter_by_route`: path prefixes filtered from the enforcement flow before context creation.
431    pub filter_by_route: Vec<String>,
432    /// `px_filter_by_extension`: file extensions filtered from verification before context creation.
433    pub filter_by_extension: Vec<String>,
434    /// `px_filter_by_user_agent`: user-agent values filtered from verification before context creation.
435    pub filter_by_user_agent: Vec<String>,
436    /// `px_filter_by_ip`: client IP values filtered from verification before context creation.
437    pub filter_by_ip: Vec<String>,
438    /// `px_filter_by_http_method`: HTTP methods filtered from verification before context creation.
439    pub filter_by_http_method: Vec<String>,
440    /// `px_custom_cookie_header`: alternate header used to read PX cookies instead of the `Cookie` header.
441    pub custom_cookie_header: String,
442    /// `px_enforced_routes`: routes treated as active blocking even while the module is in monitor mode.
443    pub enforced_routes: Vec<String>,
444    /// `px_monitored_routes`: routes treated as monitor even while the module is in active blocking mode.
445    pub monitored_routes: Vec<String>,
446    /// `px_bypass_monitor_header`: header name whose value `1` forces blocking flow in monitor mode.
447    pub bypass_monitor_header: String,
448    /// `px_first_party_enabled`: enables first-party proxying for sensor, captcha, and XHR endpoints.
449    pub first_party_enabled: bool,
450    /// `px_custom_logo`: block-page logo URL; empty keeps the logo hidden.
451    pub custom_logo: String,
452    /// `px_js_ref`: custom block-page JavaScript URL loaded after default scripts.
453    pub js_ref: String,
454    /// `px_css_ref`: custom block-page CSS URL loaded by the rendered block template.
455    pub css_ref: String,
456    /// `px_ip_headers`: trusted header names, checked in order, for extracting the real client IP.
457    pub ip_headers: Vec<String>,
458    /// `px_log_endpoint`: Fastly logging endpoint used by platform-specific activity delivery.
459    pub log_endpoint: String,
460    /// `px_data_enrichment_header_name`: request header to receive verified PXDE JSON; empty disables it.
461    pub data_enrichment_header_name: String,
462    /// `px_extracted_cookies`: cookie names copied into Risk API additional fields.
463    pub extracted_cookies: Vec<String>,
464    /// `px_cors_support_enabled`: enables CORS handling for preflight and block responses.
465    pub cors_support_enabled: bool,
466    /// `px_cors_preflight_request_filter_enabled`: passes CORS preflight requests before verification.
467    pub cors_preflight_request_filter_enabled: bool,
468    /// `px_graphql_enabled`: enables extraction of GraphQL operation data from matching POST requests.
469    pub graphql_enabled: bool,
470    /// `px_graphql_routes`: regex routes that identify requests eligible for GraphQL extraction.
471    pub graphql_routes: Vec<Regex>,
472    /// `px_sensitive_graphql_operation_names`: operation names that mark GraphQL requests as sensitive.
473    pub sensitive_graphql_operation_names: Vec<String>,
474    /// `px_sensitive_graphql_operation_types`: operation types that mark GraphQL requests as sensitive.
475    pub sensitive_graphql_operation_types: Vec<String>,
476    /// `px_graphql_body_max_length`: maximum body prefix read while parsing GraphQL JSON.
477    pub graphql_body_max_length: usize,
478    /// `px_graphql_keywords`: regex patterns matched against GraphQL query text for activity keywords.
479    pub graphql_keywords: Vec<Regex>,
480    /// `px_s2s_timeout`: Risk API timeout in milliseconds; timeouts fail open.
481    pub s2s_timeout: u32,
482    /// `px_token_version`: risk-cookie/mobile token format version expected by validators.
483    pub token_version: TokenVersion,
484    /// `px_custom_first_party_captcha_endpoint`: custom path treated as first-party captcha proxy.
485    pub custom_first_party_captcha_endpoint: String,
486    /// `px_custom_first_party_sensor_endpoint`: custom path treated as first-party sensor proxy.
487    pub custom_first_party_sensor_endpoint: String,
488    /// `px_custom_first_party_xhr_endpoint`: custom path treated as first-party XHR proxy.
489    pub custom_first_party_xhr_endpoint: String,
490    /// `px_user_agent_max_length`: maximum user-agent length used for risk-cookie validation.
491    pub user_agent_max_length: usize,
492    /// `px_risk_cookie_max_length`: maximum risk-cookie value length accepted before validation fails.
493    pub risk_cookie_max_length: usize,
494    /// `px_risk_cookie_min_iterations`: minimum accepted PBKDF2 iteration count for Cookie V3.
495    pub risk_cookie_min_iterations: usize,
496    /// `px_risk_cookie_max_iterations`: maximum accepted PBKDF2 iteration count for Cookie V3.
497    pub risk_cookie_max_iterations: usize,
498    /// `px_agentic_trust_enabled`: enables agentic trust verification for MCP requests.
499    pub agentic_trust_enabled: bool,
500    /// `px_agentic_trust_mcp_endpoint_path`: MCP endpoint path used for agentic trust verification.
501    pub agentic_trust_mcp_endpoint_path: String,
502    /// `px_logger_auth_token`: token required by header-based enforcer log collection.
503    pub logger_auth_token: String,
504    /// `px_secured_pxhd_enabled`: when true, `_pxhd` response cookies include the `Secure` attribute.
505    pub secured_pxhd_enabled: bool,
506    /// `px_pxhd_domain`: when non-empty, overrides Risk API `pxhdDomain` on `_pxhd` Set-Cookie.
507    pub pxhd_domain: String,
508    /// `px_jwt_cookie_name`: cookie name that carries the customer JWT.
509    pub jwt_cookie_name: String,
510    /// `px_jwt_cookie_user_id_field_name`: dot path in the JWT payload for the app user ID.
511    pub jwt_cookie_user_id_field_name: String,
512    /// `px_jwt_cookie_additional_field_names`: dot paths of extra JWT payload fields to extract.
513    pub jwt_cookie_additional_field_names: Vec<String>,
514    /// `px_jwt_header_name`: request header name that carries the customer JWT.
515    pub jwt_header_name: String,
516    /// `px_jwt_header_user_id_field_name`: dot path in the JWT payload for the app user ID.
517    pub jwt_header_user_id_field_name: String,
518    /// `px_jwt_header_additional_field_names`: dot paths of extra JWT payload fields to extract.
519    pub jwt_header_additional_field_names: Vec<String>,
520
521    /// `px_human_sapi_host`: HUMAN SAPI host for Risk API and telemetry requests.
522    pub human_sapi_host: String,
523    /// `px_human_sapi_backend`: Fastly backend for HUMAN SAPI requests.
524    pub human_sapi_backend: String,
525    /// `px_human_collector_host`: HUMAN collector host for activity and XHR requests.
526    pub human_collector_host: String,
527    /// `px_human_collector_backend`: Fastly backend for HUMAN collector requests.
528    pub human_collector_backend: String,
529    /// `px_human_client_host`: HUMAN client host for first-party sensor requests.
530    pub human_client_host: String,
531    /// `px_human_client_backend`: Fastly backend for HUMAN client requests.
532    pub human_client_backend: String,
533    /// `px_human_captcha_host`: HUMAN captcha host for block-page and first-party captcha requests.
534    pub human_captcha_host: String,
535    /// `px_human_captcha_backend`: Fastly backend for HUMAN captcha requests.
536    pub human_captcha_backend: String,
537    /// `px_login_credentials_extraction_enabled`: master switch for Credentials Intelligence.
538    pub login_credentials_extraction_enabled: bool,
539    /// `px_login_credentials_extraction`: credential endpoint definitions.
540    pub login_credentials_extraction: Vec<PXCredentialEndpointConfig>,
541    /// Compiled credential endpoints (regex paths, etc.).
542    pub prepared_ci_endpoints: Vec<PXPreparedCredentialEndpoint>,
543    /// `px_credentials_intelligence_version`: default hashing protocol (`v2`, `multistep_sso`, `both`).
544    pub credentials_intelligence_version: String,
545    /// `px_compromised_credentials_header`: origin request header when credentials are breached.
546    pub compromised_credentials_header: String,
547    /// `px_send_raw_username_on_additional_s2s_activity`: include raw username on additional_s2s when allowed.
548    pub send_raw_username_on_additional_s2s_activity: bool,
549    /// `px_additional_s2s_activity_enabled`: send additional_s2s automatically after response.
550    pub additional_s2s_activity_enabled: bool,
551    /// `px_additional_s2s_activity_header_enabled`: attach additional_s2s payload to origin request headers.
552    pub additional_s2s_activity_header_enabled: bool,
553    /// `px_login_successful_reporting_method`: default login-success detection method.
554    pub login_successful_reporting_method: String,
555    /// `px_login_successful_body_regex`: default body regex for login-success detection.
556    pub login_successful_body_regex: String,
557    /// `px_login_successful_header_name`: default response header name for login-success detection.
558    pub login_successful_header_name: String,
559    /// `px_login_successful_header_value`: default response header value for login-success detection.
560    pub login_successful_header_value: String,
561    /// `px_login_successful_status`: default HTTP status codes meaning login success.
562    pub login_successful_status: Vec<u16>,
563
564    // Runtime-only callbacks
565    /// `px_enrich_custom_parameters`: fills up to ten custom params on Risk and async activities.
566    pub enrich_params_fn: Option<PXEnrichCustomParamsFn>,
567    /// `px_custom_is_sensitive_request`: runtime callback for marking a request sensitive.
568    pub is_sensitive_request_fn: Option<PXIsSensitiveRequestFn>,
569    /// `px_custom_is_enforced_request`: runtime callback for marking a request enforced.
570    pub is_enforced_request_fn: Option<PXIsEnforcedRequestFn>,
571    /// `px_custom_is_monitored_request`: runtime callback for marking a request monitored.
572    pub is_monitored_request_fn: Option<PXIsMonitoredRequestFn>,
573    /// `px_filter_by_custom_function`: runtime callback for filtering a request before verification.
574    pub is_filtered_request_fn: Option<PXIsFilteredRequestFn>,
575    /// `px_additional_activity_handler`: runs after page_requested or block activity is sent.
576    pub additional_activity_handler_fn: Option<PXAdditionalActivityHandlerFn>,
577    /// `px_cors_custom_preflight_handler`: custom response hook for CORS preflight requests.
578    pub cors_custom_preflight_handler_fn: Option<PXCorsCustomPreflightHandlerFn>,
579    /// `px_cors_create_custom_block_response_headers`: custom CORS headers for block responses.
580    pub cors_create_custom_block_response_headers_fn: Option<PXCorsCustomBlockResponseHeadersFn>,
581    /// Runtime callback for custom credential extraction (`sent_through: custom`).
582    pub ci_extract_credentials_fn: Option<PXExtractCredentialsFn>,
583    /// Runtime callback for custom login-success reporting.
584    pub ci_login_successful_fn: Option<PXLoginSuccessfulFn>,
585}
586
587impl Default for PXConfig {
588    fn default() -> Self {
589        Self {
590            store: None,
591            app_id: String::new(),
592            cookie_secret: String::new(),
593            auth_token: String::new(),
594            debug: false,
595            blocking_score: 100,
596            module_enabled: false,
597            module_mode: PXModuleMode::Monitor,
598            sensitive_headers: vec!["Cookie".to_owned(), "Cookies".to_owned()],
599            sensitive_routes: Vec::new(),
600            sensitive_routes_regex: Vec::new(),
601            filter_by_route: Vec::new(),
602            filter_by_extension: WHITELIST_EXT.iter().map(|ext| (*ext).to_owned()).collect(),
603            filter_by_user_agent: Vec::new(),
604            filter_by_ip: Vec::new(),
605            filter_by_http_method: Vec::new(),
606            custom_cookie_header: CUSTOM_COOKIE_HEADER.to_owned(),
607            enforced_routes: Vec::new(),
608            monitored_routes: Vec::new(),
609            bypass_monitor_header: "x-px-block".to_owned(),
610            first_party_enabled: true,
611            custom_logo: String::new(),
612            js_ref: String::new(),
613            css_ref: String::new(),
614            human_sapi_host: default_human_sapi_host(""),
615            human_sapi_backend: "human_sapi".to_owned(),
616            human_collector_host: default_human_collector_host(""),
617            human_collector_backend: "human_collector".to_owned(),
618            human_client_host: "client.perimeterx.net".to_owned(),
619            human_client_backend: "human_client".to_owned(),
620            human_captcha_host: "captcha.px-cdn.net".to_owned(),
621            human_captcha_backend: "human_captcha".to_owned(),
622            ip_headers: Vec::new(),
623            log_endpoint: String::new(),
624            data_enrichment_header_name: String::new(),
625            extracted_cookies: Vec::new(),
626            cors_support_enabled: false,
627            cors_preflight_request_filter_enabled: false,
628            graphql_enabled: false,
629            graphql_routes: default_graphql_routes(),
630            sensitive_graphql_operation_names: Vec::new(),
631            sensitive_graphql_operation_types: Vec::new(),
632            graphql_body_max_length: GRAPHQL_BODY_MAX_LENGTH,
633            graphql_keywords: Vec::new(),
634            s2s_timeout: 2000,
635            token_version: TokenVersion::V3,
636            custom_first_party_captcha_endpoint: String::new(),
637            custom_first_party_sensor_endpoint: String::new(),
638            custom_first_party_xhr_endpoint: String::new(),
639            user_agent_max_length: 8528,
640            risk_cookie_max_length: 2048,
641            risk_cookie_min_iterations: 500,
642            risk_cookie_max_iterations: 5000,
643            agentic_trust_enabled: false,
644            agentic_trust_mcp_endpoint_path: MCP_ENDPOINT_PATH.to_owned(),
645            logger_auth_token: String::new(),
646            secured_pxhd_enabled: false,
647            pxhd_domain: String::new(),
648            jwt_cookie_name: String::new(),
649            jwt_cookie_user_id_field_name: String::new(),
650            jwt_cookie_additional_field_names: Vec::new(),
651            jwt_header_name: String::new(),
652            jwt_header_user_id_field_name: String::new(),
653            jwt_header_additional_field_names: Vec::new(),
654            login_credentials_extraction_enabled: false,
655            login_credentials_extraction: Vec::new(),
656            prepared_ci_endpoints: Vec::new(),
657            credentials_intelligence_version: "both".to_owned(),
658            compromised_credentials_header:
659                crate::modules::pxconstants::DEFAULT_COMPROMISED_CREDENTIALS_HEADER.to_owned(),
660            send_raw_username_on_additional_s2s_activity: false,
661            additional_s2s_activity_enabled: true,
662            additional_s2s_activity_header_enabled: false,
663            login_successful_reporting_method: "status".to_owned(),
664            login_successful_body_regex: String::new(),
665            login_successful_header_name: String::new(),
666            login_successful_header_value: String::new(),
667            login_successful_status: vec![200],
668            enrich_params_fn: None,
669            is_sensitive_request_fn: None,
670            is_enforced_request_fn: None,
671            is_monitored_request_fn: None,
672            is_filtered_request_fn: None,
673            additional_activity_handler_fn: None,
674            cors_custom_preflight_handler_fn: None,
675            cors_create_custom_block_response_headers_fn: None,
676            ci_extract_credentials_fn: None,
677            ci_login_successful_fn: None,
678        }
679    }
680}
681
682fn compile_ci_path_regex(path: &str) -> Option<Regex> {
683    let raw = path.strip_prefix(REGEX_PREFIX).unwrap_or(path);
684    match Regex::new(raw) {
685        Ok(re) => Some(re),
686        Err(e) => {
687            px_error!("Invalid CI endpoint regex {:?}: {}", raw, e);
688            None
689        }
690    }
691}
692
693pub(crate) fn rebuild_prepared_ci_endpoints(conf: &mut PXConfig) {
694    conf.prepared_ci_endpoints = conf
695        .login_credentials_extraction
696        .iter()
697        .map(|config| {
698            let path_regex = if config.path_type.eq_ignore_ascii_case("regex") {
699                compile_ci_path_regex(&config.path)
700            } else {
701                None
702            };
703            PXPreparedCredentialEndpoint {
704                config: config.clone(),
705                path_regex,
706            }
707        })
708        .collect();
709}
710
711fn is_empty_ci_endpoints_value(v: &serde_json::Value) -> bool {
712    match v {
713        serde_json::Value::Object(map) => map.is_empty(),
714        serde_json::Value::Array(arr) => {
715            arr.is_empty()
716                || arr
717                    .iter()
718                    .all(|item| item.as_object().is_some_and(serde_json::Map::is_empty))
719        }
720        _ => false,
721    }
722}
723
724fn parse_ci_endpoints_from_value(
725    v: &serde_json::Value,
726) -> Result<Vec<PXCredentialEndpointConfig>, serde_json::Error> {
727    if is_empty_ci_endpoints_value(v) {
728        return Ok(Vec::new());
729    }
730    if let Some(raw) = v.as_str() {
731        if raw.trim().is_empty() {
732            return Ok(Vec::new());
733        }
734        if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(raw) {
735            return parse_ci_endpoints_from_value(&parsed);
736        }
737        serde_json::from_str(raw)
738    } else {
739        serde_json::from_value(v.clone())
740    }
741}
742
743fn parse_ci_endpoints(raw: &str) -> Vec<PXCredentialEndpointConfig> {
744    match parse_ci_endpoints_from_value(&serde_json::Value::String(raw.to_owned())) {
745        Ok(endpoints) => endpoints,
746        Err(e) => {
747            px_error!(
748                "failed to parse px_login_credentials_extraction from store: {}",
749                e
750            );
751            Vec::new()
752        }
753    }
754}
755
756fn parse_status_vec(raw: &str) -> Vec<u16> {
757    if let Ok(vec) = serde_json::from_str::<Vec<u16>>(raw) {
758        return vec;
759    }
760    raw.split(',')
761        .filter_map(|s| s.trim().parse::<u16>().ok())
762        .collect()
763}
764
765fn default_graphql_routes() -> Vec<Regex> {
766    Regex::new("^/graphql$")
767        .map(|r| vec![r])
768        .unwrap_or_default()
769}
770
771/// Parse a raw store value into a `Vec<String>`, handling both
772/// JSON-encoded arrays (written by `serde_json::Value::to_string()`)
773/// and plain comma-separated values.
774pub(crate) fn parse_vec(input: &str) -> Vec<String> {
775    if let Ok(vec) = serde_json::from_str::<Vec<String>>(input) {
776        return vec.into_iter().filter(|s| !s.is_empty()).collect();
777    }
778    input
779        .split(',')
780        .map(|s| s.trim())
781        .filter(|s| !s.is_empty())
782        .map(|s| s.to_owned())
783        .collect()
784}
785
786fn parse_regex_vec(key_name: &str, input: &str) -> Vec<Regex> {
787    let items: Vec<String> = if let Ok(vec) = serde_json::from_str::<Vec<String>>(input) {
788        vec.into_iter().filter(|s| !s.is_empty()).collect()
789    } else {
790        input
791            .split(',')
792            .map(|s| s.trim())
793            .filter(|s| !s.is_empty())
794            .map(|s| s.to_owned())
795            .collect()
796    };
797    items
798        .iter()
799        .filter_map(|s| match Regex::new(s) {
800            Ok(r) => Some(r),
801            Err(_) => {
802                px_error!("[{}] Invalid regex {:?}", key_name, s);
803                None
804            }
805        })
806        .collect()
807}
808
809/// Convert a `serde_json::Value` to a plain-text string suitable for the
810/// KVStore.  Strings are stored without JSON quotes; arrays are stored as
811/// comma-separated values so `parse_vec` / `parse_regex_vec` can read them
812/// back without JSON decoding.
813#[cfg(feature = "kv_store")]
814pub(crate) fn json_value_to_raw_store(v: &serde_json::Value) -> String {
815    match v {
816        serde_json::Value::String(s) => s.clone(),
817        serde_json::Value::Array(arr) => {
818            let all_strings = arr.iter().all(serde_json::Value::is_string);
819            if all_strings {
820                arr.iter()
821                    .filter_map(|item| item.as_str())
822                    .collect::<Vec<_>>()
823                    .join(",")
824            } else {
825                v.to_string()
826            }
827        }
828        other => other.to_string(),
829    }
830}
831
832/// Strip surrounding double-quote characters that appear when a
833/// `serde_json::Value::to_string()` output is stored verbatim in the KVStore.
834pub(crate) fn strip_json_quotes(s: &str) -> &str {
835    s.strip_prefix('"')
836        .and_then(|inner| inner.strip_suffix('"'))
837        .unwrap_or(s)
838}
839
840fn parse_string(input: &str) -> String {
841    if input.eq_ignore_ascii_case("none") {
842        String::new()
843    } else {
844        input.to_owned()
845    }
846}
847
848fn default_human_sapi_host(app_id: &str) -> String {
849    format!("sapi-{app_id}.perimeterx.net")
850}
851
852fn default_human_collector_host(app_id: &str) -> String {
853    format!("collector-{app_id}.perimeterx.net")
854}
855
856/// Compile a sequence of pattern strings into `Regex` objects, dropping (and
857/// logging) any that fail to compile. Used by the JSON deserialization paths
858/// where `Regex` itself does not implement `Deserialize`. Strips the leading
859/// `_REGEXP` prefix added by `PXConfigValue::RegexVec`'s `Serialize` impl.
860fn compile_regex_vec<I, S>(patterns: I, key_name: &str) -> Vec<Regex>
861where
862    I: IntoIterator<Item = S>,
863    S: AsRef<str>,
864{
865    patterns
866        .into_iter()
867        .filter_map(|p| {
868            let raw = p.as_ref().strip_prefix(REGEX_PREFIX).unwrap_or(p.as_ref());
869            match Regex::new(raw) {
870                Ok(r) => Some(r),
871                Err(_) => {
872                    px_error!("Invalid regex {:?} for key {}", raw, key_name);
873                    None
874                }
875            }
876        })
877        .collect()
878}
879
880/// Keys loaded from Fastly Secret Store when configured; values override
881/// ConfigStore/KVStore for the same `px_*` names.
882#[cfg(any(target_arch = "wasm32", test))]
883const SECRET_STORE_KEYS: &[PXConfigKey] = &[
884    PXConfigKey::AppId,
885    PXConfigKey::CookieSecret,
886    PXConfigKey::AuthToken,
887    PXConfigKey::LoggerAuthToken,
888];
889
890#[cfg(any(target_arch = "wasm32", test))]
891pub(crate) fn secret_store_keys() -> &'static [PXConfigKey] {
892    SECRET_STORE_KEYS
893}
894
895/// Apply secret values onto an already-loaded config. Used by [`PXConfig::new`]
896/// and unit tests.
897#[cfg(any(target_arch = "wasm32", test))]
898pub(crate) fn apply_secret_values(conf: &mut PXConfig, secrets: &[(PXConfigKey, &str)]) {
899    for (key, raw) in secrets {
900        conf.set_from_raw(*key, raw);
901    }
902}
903
904fn config_value_is_set(value: &PXConfigValue<'_>) -> bool {
905    match value {
906        PXConfigValue::Str(v) => !v.is_empty(),
907        PXConfigValue::StrVec(v) => !v.is_empty(),
908        PXConfigValue::RegexVec(v) => !v.is_empty(),
909        PXConfigValue::U16Vec(v) => !v.is_empty(),
910        PXConfigValue::CiEndpoints(v) => !v.is_empty(),
911        PXConfigValue::Bool(_)
912        | PXConfigValue::U8(_)
913        | PXConfigValue::U16(_)
914        | PXConfigValue::U32(_)
915        | PXConfigValue::USize(_)
916        | PXConfigValue::ModuleMode(_)
917        | PXConfigValue::TokenVersion(_) => true,
918    }
919}
920
921pub(crate) fn load_secrets_from_store(conf: &mut PXConfig, secret_store_name: &str) {
922    if secret_store_name.is_empty() {
923        return;
924    }
925
926    #[cfg(not(target_arch = "wasm32"))]
927    {
928        let _ = conf;
929    }
930
931    #[cfg(target_arch = "wasm32")]
932    let secret_store = match SecretStore::open(secret_store_name) {
933        Ok(store) => store,
934        Err(e) => {
935            px_error!("Failed to open Secret Store {:?}: {}", secret_store_name, e);
936            return;
937        }
938    };
939
940    #[cfg(target_arch = "wasm32")]
941    {
942        let mut overlay = Vec::with_capacity(secret_store_keys().len());
943        for key in secret_store_keys() {
944            let secret_name = key.as_ref();
945            match secret_store.try_get(secret_name) {
946                Ok(Some(secret)) => match secret.try_plaintext() {
947                    Ok(plaintext) => {
948                        if let Ok(value) = std::str::from_utf8(plaintext.as_ref()) {
949                            overlay.push((*key, value.to_owned()));
950                        } else {
951                            px_error!(
952                                "Secret {:?} in Secret Store {:?} is not valid UTF-8",
953                                secret_name,
954                                secret_store_name
955                            );
956                        }
957                    }
958                    Err(e) => {
959                        px_error!(
960                            "Failed to decrypt secret {:?} in Secret Store {:?}: {}",
961                            secret_name,
962                            secret_store_name,
963                            e
964                        );
965                    }
966                },
967                Ok(None) => {}
968                Err(e) => {
969                    px_error!(
970                        "Failed to lookup secret {:?} in Secret Store {:?}: {}",
971                        secret_name,
972                        secret_store_name,
973                        e
974                    );
975                }
976            }
977        }
978
979        let overlay_refs: Vec<(PXConfigKey, &str)> = overlay
980            .iter()
981            .map(|(key, value)| (*key, value.as_str()))
982            .collect();
983        apply_secret_values(conf, &overlay_refs);
984    }
985}
986
987pub(crate) fn recompute_derived(conf: &mut PXConfig, previous_app_id: &str) {
988    let previous_sapi_host = default_human_sapi_host(previous_app_id);
989    if conf.human_sapi_host.is_empty() || conf.human_sapi_host == previous_sapi_host {
990        conf.human_sapi_host = default_human_sapi_host(&conf.app_id);
991    }
992
993    let previous_collector_host = default_human_collector_host(previous_app_id);
994    if conf.human_collector_host.is_empty() || conf.human_collector_host == previous_collector_host
995    {
996        conf.human_collector_host = default_human_collector_host(&conf.app_id);
997    }
998}
999
1000impl Serialize for PXConfig {
1001    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1002        let entries: Vec<_> = self.fields().filter(|(k, _)| !k.is_local()).collect();
1003        let mut map = s.serialize_map(Some(entries.len()))?;
1004        for (key, value) in entries {
1005            if key.is_sensitive() {
1006                map.serialize_entry(key.as_ref(), &Redacted(&value))?;
1007            } else {
1008                map.serialize_entry(key.as_ref(), &value)?;
1009            }
1010        }
1011        map.end()
1012    }
1013}
1014
1015impl<'de> Deserialize<'de> for PXConfig {
1016    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
1017        struct PXConfigVisitor;
1018
1019        impl<'de> Visitor<'de> for PXConfigVisitor {
1020            type Value = PXConfig;
1021
1022            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1023                f.write_str("a PXConfig map keyed by PXConfigKey names")
1024            }
1025
1026            fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<PXConfig, M::Error> {
1027                let mut out = PXConfig::default();
1028                // TODO: should we keep the default values?
1029                // out.sensitive_headers.clear();
1030                // out.graphql_routes.clear();
1031                let mut seen: HashSet<PXConfigKey> = HashSet::new();
1032
1033                while let Some(key) = map.next_key::<String>()? {
1034                    let variant = PXConfigKey::iter()
1035                        .find(|v| v.as_ref() == key)
1036                        .ok_or_else(|| de::Error::custom(format!("unknown field `{key}`")))?;
1037                    if !seen.insert(variant) {
1038                        return Err(de::Error::custom(format!("duplicate field `{key}`")));
1039                    }
1040                    out.set_from(variant, &mut map)?;
1041                }
1042
1043                if let Some(missing) =
1044                    PXConfigKey::iter().find(|v| !v.is_local() && !seen.contains(v))
1045                {
1046                    return Err(de::Error::custom(format!(
1047                        "missing field `{}`",
1048                        missing.as_ref()
1049                    )));
1050                }
1051
1052                rebuild_prepared_ci_endpoints(&mut out);
1053                recompute_derived(&mut out, "");
1054                Ok(out)
1055            }
1056        }
1057
1058        d.deserialize_map(PXConfigVisitor)
1059    }
1060}
1061
1062impl PXConfig {
1063    pub fn get_module_mode(&self) -> PXModuleMode {
1064        self.module_mode
1065    }
1066
1067    /// Construct a `PXConfig` by reading every [`PXConfigKey`] from the
1068    /// backing config store (or KV store when `kv_store` is enabled), then
1069    /// overlaying `secret_store_keys` from Fastly Secret Store when
1070    /// `secret_store_name` is non-empty.
1071    pub fn new(config_store_name: &str, secret_store_name: &str) -> Self {
1072        let mut conf = PXConfig::default();
1073
1074        #[cfg(feature = "kv_store")]
1075        let confg_store = {
1076            match KVStore::open(config_store_name) {
1077                Ok(Some(kv)) => kv,
1078                Ok(None) => {
1079                    px_error!(
1080                        "KVStore {:?} not found; falling back to default PXConfig",
1081                        config_store_name
1082                    );
1083                    return Self::default();
1084                }
1085                Err(e) => {
1086                    px_error!(
1087                        "Failed to open KVStore {:?}: {:?}; falling back to default PXConfig",
1088                        config_store_name,
1089                        e
1090                    );
1091                    return Self::default();
1092                }
1093            }
1094        };
1095
1096        #[cfg(not(feature = "kv_store"))]
1097        let confg_store = ConfigStore::open(config_store_name);
1098
1099        for key in PXConfigKey::iter() {
1100            if let Some(raw) = confg_store.get(key.as_ref()) {
1101                conf.set_from_raw(key, &raw);
1102            }
1103        }
1104
1105        recompute_derived(&mut conf, "");
1106        conf.store = Some(confg_store);
1107        let app_id_before_secrets = conf.app_id.clone();
1108        load_secrets_from_store(&mut conf, secret_store_name);
1109        rebuild_prepared_ci_endpoints(&mut conf);
1110        recompute_derived(&mut conf, &app_id_before_secrets);
1111        conf.validate_required_fields();
1112        conf
1113    }
1114
1115    /// Partial JSON update — applies values for any [`PXConfigKey`] present
1116    /// (and non-null) in `cfg`. Unknown keys are ignored; type errors are
1117    /// logged and the affected field is left unchanged.
1118    #[doc(hidden)]
1119    pub fn update_from_json(&mut self, cfg: &serde_json::Value) {
1120        let config_obj = cfg.get("configValue").unwrap_or(cfg);
1121
1122        let previous_app_id = self.app_id.clone();
1123        let mut config_found = false;
1124        for key in PXConfigKey::iter() {
1125            let Some(v) = config_obj.get(key.as_ref()).filter(|v| !v.is_null()) else {
1126                continue;
1127            };
1128
1129            config_found = true;
1130            if let Err(e) = self.set_from_value(key, v) {
1131                px_error!("update_from_json: bad value for {}: {}", key.as_ref(), e);
1132            }
1133
1134            #[cfg(feature = "kv_store")]
1135            if let Some(store) = self.store.as_mut() {
1136                let store_value = json_value_to_raw_store(v);
1137                let _ = store.insert(key.as_ref(), store_value);
1138            }
1139        }
1140
1141        if !config_found {
1142            px_error!("update_from_json: config not found in body");
1143        }
1144        rebuild_prepared_ci_endpoints(self);
1145        recompute_derived(self, &previous_app_id);
1146        self.validate_required_fields();
1147    }
1148
1149    /// Look up a field by its [`PXConfigKey`].
1150    pub fn get(&self, key: PXConfigKey) -> PXConfigValue<'_> {
1151        self.get_value(key)
1152    }
1153
1154    /// Iterate `(key, value)` pairs over every configurable field.
1155    pub fn fields(&self) -> impl ExactSizeIterator<Item = (PXConfigKey, PXConfigValue<'_>)> {
1156        PXConfigKey::iter().map(|k| (k, self.get_value(k)))
1157    }
1158
1159    /// Look up a field by its serialized name (e.g. `"px_app_id"`).
1160    pub fn get_field(&self, name: impl AsRef<str>) -> Option<PXConfigValue<'_>> {
1161        let name = name.as_ref();
1162        self.fields()
1163            .find_map(|(k, v)| (k.as_ref() == name).then_some(v))
1164    }
1165
1166    /// Build a JSON object containing only the fields whose [`PXConfigKey`]
1167    /// is marked with `#[strum(props(required = "true"))]`. Sensitive values
1168    /// are redacted the same way as in the full `Serialize` impl.
1169    pub(crate) fn build_static_json(&self) -> serde_json::Value {
1170        let static_fields: Vec<(PXConfigKey, PXConfigValue<'_>)> =
1171            self.fields().filter(|(k, _)| k.is_static()).collect();
1172        let mut map = serde_json::Map::with_capacity(static_fields.len());
1173        for (key, value) in static_fields {
1174            let json_value = if key.is_sensitive() {
1175                serde_json::to_value(Redacted(&value)).unwrap_or_default()
1176            } else {
1177                serde_json::to_value(&value).unwrap_or_default()
1178            };
1179            map.insert(key.as_ref().to_owned(), json_value);
1180        }
1181        serde_json::Value::Object(map)
1182    }
1183
1184    /// Serialize the active enforcer configuration for the telemetry payload.
1185    /// Excludes fields marked `static = "true"` (immutable between restarts)
1186    /// and `local = "true"` (never sent off-box).
1187    pub(crate) fn build_config_json(&self) -> serde_json::Value {
1188        let entries: Vec<(PXConfigKey, PXConfigValue<'_>)> =
1189            self.fields().filter(|(k, _)| !k.is_local()).collect();
1190        let mut map = serde_json::Map::with_capacity(entries.len());
1191        for (key, value) in entries {
1192            let json_value = if key.is_sensitive() {
1193                serde_json::to_value(Redacted(&value)).unwrap_or_default()
1194            } else {
1195                serde_json::to_value(&value).unwrap_or_default()
1196            };
1197            map.insert(key.as_ref().to_owned(), json_value);
1198        }
1199        serde_json::Value::Object(map)
1200    }
1201
1202    fn missing_required_fields(&self) -> Vec<PXConfigKey> {
1203        self.fields()
1204            .filter_map(|(key, value)| {
1205                (key.is_required() && !config_value_is_set(&value)).then_some(key)
1206            })
1207            .collect()
1208    }
1209
1210    fn validate_required_fields(&self) {
1211        for key in self.missing_required_fields() {
1212            px_error!(
1213                "Required PX configuration key {} is missing or empty",
1214                key.as_ref()
1215            );
1216        }
1217    }
1218
1219    fn get_value(&self, key: PXConfigKey) -> PXConfigValue<'_> {
1220        match key {
1221            PXConfigKey::AppId => PXConfigValue::Str(&self.app_id),
1222            PXConfigKey::CookieSecret => PXConfigValue::Str(&self.cookie_secret),
1223            PXConfigKey::AuthToken => PXConfigValue::Str(&self.auth_token),
1224            PXConfigKey::Debug => PXConfigValue::Bool(self.debug),
1225            PXConfigKey::BlockingScore => PXConfigValue::U8(self.blocking_score),
1226            PXConfigKey::ModuleEnabled => PXConfigValue::Bool(self.module_enabled),
1227            PXConfigKey::ModuleMode => PXConfigValue::ModuleMode(self.module_mode),
1228            PXConfigKey::SensitiveHeaders => PXConfigValue::StrVec(&self.sensitive_headers),
1229            PXConfigKey::SensitiveRoutes => PXConfigValue::StrVec(&self.sensitive_routes),
1230            PXConfigKey::SensitiveRoutesRegex => {
1231                PXConfigValue::RegexVec(&self.sensitive_routes_regex)
1232            }
1233            PXConfigKey::FilterByRoute => PXConfigValue::StrVec(&self.filter_by_route),
1234            PXConfigKey::FilterByExtension => PXConfigValue::StrVec(&self.filter_by_extension),
1235            PXConfigKey::FilterByUserAgent => PXConfigValue::StrVec(&self.filter_by_user_agent),
1236            PXConfigKey::FilterByIp => PXConfigValue::StrVec(&self.filter_by_ip),
1237            PXConfigKey::FilterByHttpMethod => PXConfigValue::StrVec(&self.filter_by_http_method),
1238            PXConfigKey::CustomCookieHeader => PXConfigValue::Str(&self.custom_cookie_header),
1239            PXConfigKey::EnforcedRoutes => PXConfigValue::StrVec(&self.enforced_routes),
1240            PXConfigKey::MonitoredRoutes => PXConfigValue::StrVec(&self.monitored_routes),
1241            PXConfigKey::BypassMonitorHeader => PXConfigValue::Str(&self.bypass_monitor_header),
1242            PXConfigKey::FirstPartyEnabled => PXConfigValue::Bool(self.first_party_enabled),
1243            PXConfigKey::CustomLogo => PXConfigValue::Str(&self.custom_logo),
1244            PXConfigKey::JsRef => PXConfigValue::Str(&self.js_ref),
1245            PXConfigKey::CssRef => PXConfigValue::Str(&self.css_ref),
1246            PXConfigKey::HumanSapiHost => PXConfigValue::Str(&self.human_sapi_host),
1247            PXConfigKey::HumanSapiBackend => PXConfigValue::Str(&self.human_sapi_backend),
1248            PXConfigKey::HumanCollectorHost => PXConfigValue::Str(&self.human_collector_host),
1249            PXConfigKey::HumanCollectorBackend => PXConfigValue::Str(&self.human_collector_backend),
1250            PXConfigKey::HumanClientHost => PXConfigValue::Str(&self.human_client_host),
1251            PXConfigKey::HumanClientBackend => PXConfigValue::Str(&self.human_client_backend),
1252            PXConfigKey::HumanCaptchaHost => PXConfigValue::Str(&self.human_captcha_host),
1253            PXConfigKey::HumanCaptchaBackend => PXConfigValue::Str(&self.human_captcha_backend),
1254            PXConfigKey::IpHeaders => PXConfigValue::StrVec(&self.ip_headers),
1255            PXConfigKey::LogEndpoint => PXConfigValue::Str(&self.log_endpoint),
1256            PXConfigKey::DataEnrichmentHeaderName => {
1257                PXConfigValue::Str(&self.data_enrichment_header_name)
1258            }
1259            PXConfigKey::ExtractedCookies => PXConfigValue::StrVec(&self.extracted_cookies),
1260            PXConfigKey::CorsSupportEnabled => PXConfigValue::Bool(self.cors_support_enabled),
1261            PXConfigKey::CorsPreflightRequestFilterEnabled => {
1262                PXConfigValue::Bool(self.cors_preflight_request_filter_enabled)
1263            }
1264            PXConfigKey::GraphqlEnabled => PXConfigValue::Bool(self.graphql_enabled),
1265            PXConfigKey::GraphqlRoutes => PXConfigValue::RegexVec(&self.graphql_routes),
1266            PXConfigKey::SensitiveGraphqlOperationNames => {
1267                PXConfigValue::StrVec(&self.sensitive_graphql_operation_names)
1268            }
1269            PXConfigKey::SensitiveGraphqlOperationTypes => {
1270                PXConfigValue::StrVec(&self.sensitive_graphql_operation_types)
1271            }
1272            PXConfigKey::GraphqlBodyMaxLength => PXConfigValue::USize(self.graphql_body_max_length),
1273            PXConfigKey::GraphqlKeywords => PXConfigValue::RegexVec(&self.graphql_keywords),
1274            PXConfigKey::S2sTimeout => PXConfigValue::U32(self.s2s_timeout),
1275            PXConfigKey::TokenVersion => PXConfigValue::TokenVersion(self.token_version),
1276            PXConfigKey::CustomFirstPartyCaptchaEndpoint => {
1277                PXConfigValue::Str(&self.custom_first_party_captcha_endpoint)
1278            }
1279            PXConfigKey::CustomFirstPartySensorEndpoint => {
1280                PXConfigValue::Str(&self.custom_first_party_sensor_endpoint)
1281            }
1282            PXConfigKey::CustomFirstPartyXhrEndpoint => {
1283                PXConfigValue::Str(&self.custom_first_party_xhr_endpoint)
1284            }
1285            PXConfigKey::UserAgentMaxLength => PXConfigValue::USize(self.user_agent_max_length),
1286            PXConfigKey::RiskCookieMaxLength => PXConfigValue::USize(self.risk_cookie_max_length),
1287            PXConfigKey::RiskCookieMinIterations => {
1288                PXConfigValue::USize(self.risk_cookie_min_iterations)
1289            }
1290            PXConfigKey::RiskCookieMaxIterations => {
1291                PXConfigValue::USize(self.risk_cookie_max_iterations)
1292            }
1293            PXConfigKey::AgenticTrustEnabled => PXConfigValue::Bool(self.agentic_trust_enabled),
1294            PXConfigKey::AgenticTrustMcpEndpointPath => {
1295                PXConfigValue::Str(&self.agentic_trust_mcp_endpoint_path)
1296            }
1297            PXConfigKey::SecuredPxhdEnabled => PXConfigValue::Bool(self.secured_pxhd_enabled),
1298            PXConfigKey::PxhdDomain => PXConfigValue::Str(&self.pxhd_domain),
1299            PXConfigKey::JwtCookieName => PXConfigValue::Str(&self.jwt_cookie_name),
1300            PXConfigKey::JwtCookieUserIdFieldName => {
1301                PXConfigValue::Str(&self.jwt_cookie_user_id_field_name)
1302            }
1303            PXConfigKey::JwtCookieAdditionalFieldNames => {
1304                PXConfigValue::StrVec(&self.jwt_cookie_additional_field_names)
1305            }
1306            PXConfigKey::JwtHeaderName => PXConfigValue::Str(&self.jwt_header_name),
1307            PXConfigKey::JwtHeaderUserIdFieldName => {
1308                PXConfigValue::Str(&self.jwt_header_user_id_field_name)
1309            }
1310            PXConfigKey::JwtHeaderAdditionalFieldNames => {
1311                PXConfigValue::StrVec(&self.jwt_header_additional_field_names)
1312            }
1313            PXConfigKey::LoggerAuthToken => PXConfigValue::Str(&self.logger_auth_token),
1314            PXConfigKey::LoginCredentialsExtractionEnabled => {
1315                PXConfigValue::Bool(self.login_credentials_extraction_enabled)
1316            }
1317            PXConfigKey::LoginCredentialsExtraction => {
1318                PXConfigValue::CiEndpoints(&self.login_credentials_extraction)
1319            }
1320            PXConfigKey::CredentialsIntelligenceVersion => {
1321                PXConfigValue::Str(&self.credentials_intelligence_version)
1322            }
1323            PXConfigKey::CompromisedCredentialsHeader => {
1324                PXConfigValue::Str(&self.compromised_credentials_header)
1325            }
1326            PXConfigKey::SendRawUsernameOnAdditionalS2sActivity => {
1327                PXConfigValue::Bool(self.send_raw_username_on_additional_s2s_activity)
1328            }
1329            PXConfigKey::AdditionalS2sActivityEnabled => {
1330                PXConfigValue::Bool(self.additional_s2s_activity_enabled)
1331            }
1332            PXConfigKey::AdditionalS2sActivityHeaderEnabled => {
1333                PXConfigValue::Bool(self.additional_s2s_activity_header_enabled)
1334            }
1335            PXConfigKey::LoginSuccessfulReportingMethod => {
1336                PXConfigValue::Str(&self.login_successful_reporting_method)
1337            }
1338            PXConfigKey::LoginSuccessfulBodyRegex => {
1339                PXConfigValue::Str(&self.login_successful_body_regex)
1340            }
1341            PXConfigKey::LoginSuccessfulHeaderName => {
1342                PXConfigValue::Str(&self.login_successful_header_name)
1343            }
1344            PXConfigKey::LoginSuccessfulHeaderValue => {
1345                PXConfigValue::Str(&self.login_successful_header_value)
1346            }
1347            PXConfigKey::LoginSuccessfulStatus => {
1348                PXConfigValue::U16Vec(&self.login_successful_status)
1349            }
1350        }
1351    }
1352
1353    fn set_from<'de, M: MapAccess<'de>>(
1354        &mut self,
1355        key: PXConfigKey,
1356        map: &mut M,
1357    ) -> Result<(), M::Error> {
1358        match key {
1359            PXConfigKey::AppId => self.app_id = map.next_value()?,
1360            PXConfigKey::CookieSecret => self.cookie_secret = map.next_value()?,
1361            PXConfigKey::AuthToken => self.auth_token = map.next_value()?,
1362            PXConfigKey::Debug => self.debug = map.next_value()?,
1363            PXConfigKey::BlockingScore => self.blocking_score = map.next_value()?,
1364            PXConfigKey::ModuleEnabled => self.module_enabled = map.next_value()?,
1365            PXConfigKey::ModuleMode => self.module_mode = map.next_value()?,
1366            PXConfigKey::SensitiveHeaders => self.sensitive_headers = map.next_value()?,
1367            PXConfigKey::SensitiveRoutes => self.sensitive_routes = map.next_value()?,
1368            PXConfigKey::SensitiveRoutesRegex => {
1369                let pats: Vec<String> = map.next_value()?;
1370                self.sensitive_routes_regex = compile_regex_vec(pats, key.as_ref());
1371            }
1372            PXConfigKey::FilterByRoute => self.filter_by_route = map.next_value()?,
1373            PXConfigKey::FilterByExtension => self.filter_by_extension = map.next_value()?,
1374            PXConfigKey::FilterByUserAgent => self.filter_by_user_agent = map.next_value()?,
1375            PXConfigKey::FilterByIp => self.filter_by_ip = map.next_value()?,
1376            PXConfigKey::FilterByHttpMethod => self.filter_by_http_method = map.next_value()?,
1377            PXConfigKey::CustomCookieHeader => self.custom_cookie_header = map.next_value()?,
1378            PXConfigKey::EnforcedRoutes => self.enforced_routes = map.next_value()?,
1379            PXConfigKey::MonitoredRoutes => self.monitored_routes = map.next_value()?,
1380            PXConfigKey::BypassMonitorHeader => self.bypass_monitor_header = map.next_value()?,
1381            PXConfigKey::FirstPartyEnabled => self.first_party_enabled = map.next_value()?,
1382            PXConfigKey::CustomLogo => self.custom_logo = map.next_value()?,
1383            PXConfigKey::JsRef => self.js_ref = map.next_value()?,
1384            PXConfigKey::CssRef => self.css_ref = map.next_value()?,
1385            PXConfigKey::HumanSapiHost => self.human_sapi_host = map.next_value()?,
1386            PXConfigKey::HumanSapiBackend => self.human_sapi_backend = map.next_value()?,
1387            PXConfigKey::HumanCollectorHost => self.human_collector_host = map.next_value()?,
1388            PXConfigKey::HumanCollectorBackend => {
1389                self.human_collector_backend = map.next_value()?
1390            }
1391            PXConfigKey::HumanClientHost => self.human_client_host = map.next_value()?,
1392            PXConfigKey::HumanClientBackend => self.human_client_backend = map.next_value()?,
1393            PXConfigKey::HumanCaptchaHost => self.human_captcha_host = map.next_value()?,
1394            PXConfigKey::HumanCaptchaBackend => self.human_captcha_backend = map.next_value()?,
1395            PXConfigKey::IpHeaders => self.ip_headers = map.next_value()?,
1396            PXConfigKey::LogEndpoint => self.log_endpoint = map.next_value()?,
1397            PXConfigKey::DataEnrichmentHeaderName => {
1398                self.data_enrichment_header_name = map.next_value()?
1399            }
1400            PXConfigKey::ExtractedCookies => self.extracted_cookies = map.next_value()?,
1401            PXConfigKey::CorsSupportEnabled => self.cors_support_enabled = map.next_value()?,
1402            PXConfigKey::CorsPreflightRequestFilterEnabled => {
1403                self.cors_preflight_request_filter_enabled = map.next_value()?
1404            }
1405            PXConfigKey::GraphqlEnabled => self.graphql_enabled = map.next_value()?,
1406            PXConfigKey::GraphqlRoutes => {
1407                let pats: Vec<String> = map.next_value()?;
1408                self.graphql_routes = compile_regex_vec(pats, key.as_ref());
1409            }
1410            PXConfigKey::SensitiveGraphqlOperationNames => {
1411                self.sensitive_graphql_operation_names = map.next_value()?
1412            }
1413            PXConfigKey::SensitiveGraphqlOperationTypes => {
1414                self.sensitive_graphql_operation_types = map.next_value()?
1415            }
1416            PXConfigKey::GraphqlBodyMaxLength => self.graphql_body_max_length = map.next_value()?,
1417            PXConfigKey::GraphqlKeywords => {
1418                let pats: Vec<String> = map.next_value()?;
1419                self.graphql_keywords = compile_regex_vec(pats, key.as_ref());
1420            }
1421            PXConfigKey::S2sTimeout => self.s2s_timeout = map.next_value()?,
1422            PXConfigKey::TokenVersion => self.token_version = map.next_value()?,
1423            PXConfigKey::CustomFirstPartyCaptchaEndpoint => {
1424                self.custom_first_party_captcha_endpoint = map.next_value()?
1425            }
1426            PXConfigKey::CustomFirstPartySensorEndpoint => {
1427                self.custom_first_party_sensor_endpoint = map.next_value()?
1428            }
1429            PXConfigKey::CustomFirstPartyXhrEndpoint => {
1430                self.custom_first_party_xhr_endpoint = map.next_value()?
1431            }
1432            PXConfigKey::UserAgentMaxLength => self.user_agent_max_length = map.next_value()?,
1433            PXConfigKey::RiskCookieMaxLength => self.risk_cookie_max_length = map.next_value()?,
1434            PXConfigKey::RiskCookieMinIterations => {
1435                self.risk_cookie_min_iterations = map.next_value()?
1436            }
1437            PXConfigKey::RiskCookieMaxIterations => {
1438                self.risk_cookie_max_iterations = map.next_value()?
1439            }
1440            PXConfigKey::AgenticTrustEnabled => self.agentic_trust_enabled = map.next_value()?,
1441            PXConfigKey::AgenticTrustMcpEndpointPath => {
1442                self.agentic_trust_mcp_endpoint_path = map.next_value()?
1443            }
1444            PXConfigKey::SecuredPxhdEnabled => self.secured_pxhd_enabled = map.next_value()?,
1445            PXConfigKey::PxhdDomain => self.pxhd_domain = map.next_value()?,
1446            PXConfigKey::JwtCookieName => self.jwt_cookie_name = map.next_value()?,
1447            PXConfigKey::JwtCookieUserIdFieldName => {
1448                self.jwt_cookie_user_id_field_name = map.next_value()?
1449            }
1450            PXConfigKey::JwtCookieAdditionalFieldNames => {
1451                self.jwt_cookie_additional_field_names = map.next_value()?
1452            }
1453            PXConfigKey::JwtHeaderName => self.jwt_header_name = map.next_value()?,
1454            PXConfigKey::JwtHeaderUserIdFieldName => {
1455                self.jwt_header_user_id_field_name = map.next_value()?
1456            }
1457            PXConfigKey::JwtHeaderAdditionalFieldNames => {
1458                self.jwt_header_additional_field_names = map.next_value()?
1459            }
1460            PXConfigKey::LoggerAuthToken => self.logger_auth_token = map.next_value()?,
1461            PXConfigKey::LoginCredentialsExtractionEnabled => {
1462                self.login_credentials_extraction_enabled = map.next_value()?
1463            }
1464            PXConfigKey::LoginCredentialsExtraction => {
1465                let v: serde_json::Value = map.next_value()?;
1466                self.login_credentials_extraction =
1467                    parse_ci_endpoints_from_value(&v).map_err(serde::de::Error::custom)?;
1468                rebuild_prepared_ci_endpoints(self);
1469            }
1470            PXConfigKey::CredentialsIntelligenceVersion => {
1471                self.credentials_intelligence_version = map.next_value()?
1472            }
1473            PXConfigKey::CompromisedCredentialsHeader => {
1474                self.compromised_credentials_header = map.next_value()?
1475            }
1476            PXConfigKey::SendRawUsernameOnAdditionalS2sActivity => {
1477                self.send_raw_username_on_additional_s2s_activity = map.next_value()?
1478            }
1479            PXConfigKey::AdditionalS2sActivityEnabled => {
1480                self.additional_s2s_activity_enabled = map.next_value()?
1481            }
1482            PXConfigKey::AdditionalS2sActivityHeaderEnabled => {
1483                self.additional_s2s_activity_header_enabled = map.next_value()?
1484            }
1485            PXConfigKey::LoginSuccessfulReportingMethod => {
1486                self.login_successful_reporting_method = map.next_value()?
1487            }
1488            PXConfigKey::LoginSuccessfulBodyRegex => {
1489                self.login_successful_body_regex = map.next_value()?
1490            }
1491            PXConfigKey::LoginSuccessfulHeaderName => {
1492                self.login_successful_header_name = map.next_value()?
1493            }
1494            PXConfigKey::LoginSuccessfulHeaderValue => {
1495                self.login_successful_header_value = map.next_value()?
1496            }
1497            PXConfigKey::LoginSuccessfulStatus => {
1498                self.login_successful_status = map.next_value()?
1499            }
1500        }
1501        Ok(())
1502    }
1503
1504    pub(crate) fn set_from_value(
1505        &mut self,
1506        key: PXConfigKey,
1507        v: &serde_json::Value,
1508    ) -> Result<(), serde_json::Error> {
1509        match key {
1510            PXConfigKey::AppId => self.app_id = serde_json::from_value(v.clone())?,
1511            PXConfigKey::CookieSecret => self.cookie_secret = serde_json::from_value(v.clone())?,
1512            PXConfigKey::AuthToken => self.auth_token = serde_json::from_value(v.clone())?,
1513            PXConfigKey::Debug => self.debug = serde_json::from_value(v.clone())?,
1514            PXConfigKey::BlockingScore => self.blocking_score = serde_json::from_value(v.clone())?,
1515            PXConfigKey::ModuleEnabled => self.module_enabled = serde_json::from_value(v.clone())?,
1516            PXConfigKey::ModuleMode => self.module_mode = serde_json::from_value(v.clone())?,
1517            PXConfigKey::SensitiveHeaders => {
1518                self.sensitive_headers = serde_json::from_value(v.clone())?
1519            }
1520            PXConfigKey::SensitiveRoutes => {
1521                self.sensitive_routes = serde_json::from_value(v.clone())?
1522            }
1523            PXConfigKey::SensitiveRoutesRegex => {
1524                let pats: Vec<String> = serde_json::from_value(v.clone())?;
1525                self.sensitive_routes_regex = compile_regex_vec(pats, key.as_ref());
1526            }
1527            PXConfigKey::FilterByRoute => self.filter_by_route = serde_json::from_value(v.clone())?,
1528            PXConfigKey::FilterByExtension => {
1529                self.filter_by_extension = serde_json::from_value(v.clone())?
1530            }
1531            PXConfigKey::FilterByUserAgent => {
1532                self.filter_by_user_agent = serde_json::from_value(v.clone())?
1533            }
1534            PXConfigKey::FilterByIp => self.filter_by_ip = serde_json::from_value(v.clone())?,
1535            PXConfigKey::FilterByHttpMethod => {
1536                self.filter_by_http_method = serde_json::from_value(v.clone())?
1537            }
1538            PXConfigKey::CustomCookieHeader => {
1539                self.custom_cookie_header = serde_json::from_value(v.clone())?
1540            }
1541            PXConfigKey::EnforcedRoutes => {
1542                self.enforced_routes = serde_json::from_value(v.clone())?
1543            }
1544            PXConfigKey::MonitoredRoutes => {
1545                self.monitored_routes = serde_json::from_value(v.clone())?
1546            }
1547            PXConfigKey::BypassMonitorHeader => {
1548                self.bypass_monitor_header = serde_json::from_value(v.clone())?
1549            }
1550            PXConfigKey::FirstPartyEnabled => {
1551                self.first_party_enabled = serde_json::from_value(v.clone())?
1552            }
1553            PXConfigKey::CustomLogo => self.custom_logo = serde_json::from_value(v.clone())?,
1554            PXConfigKey::JsRef => self.js_ref = serde_json::from_value(v.clone())?,
1555            PXConfigKey::CssRef => self.css_ref = serde_json::from_value(v.clone())?,
1556            PXConfigKey::HumanSapiHost => self.human_sapi_host = serde_json::from_value(v.clone())?,
1557            PXConfigKey::HumanSapiBackend => {
1558                self.human_sapi_backend = serde_json::from_value(v.clone())?
1559            }
1560            PXConfigKey::HumanCollectorHost => {
1561                self.human_collector_host = serde_json::from_value(v.clone())?
1562            }
1563            PXConfigKey::HumanCollectorBackend => {
1564                self.human_collector_backend = serde_json::from_value(v.clone())?
1565            }
1566            PXConfigKey::HumanClientHost => {
1567                self.human_client_host = serde_json::from_value(v.clone())?
1568            }
1569            PXConfigKey::HumanClientBackend => {
1570                self.human_client_backend = serde_json::from_value(v.clone())?
1571            }
1572            PXConfigKey::HumanCaptchaHost => {
1573                self.human_captcha_host = serde_json::from_value(v.clone())?
1574            }
1575            PXConfigKey::HumanCaptchaBackend => {
1576                self.human_captcha_backend = serde_json::from_value(v.clone())?
1577            }
1578            PXConfigKey::IpHeaders => self.ip_headers = serde_json::from_value(v.clone())?,
1579            PXConfigKey::LogEndpoint => self.log_endpoint = serde_json::from_value(v.clone())?,
1580            PXConfigKey::DataEnrichmentHeaderName => {
1581                self.data_enrichment_header_name = serde_json::from_value(v.clone())?
1582            }
1583            PXConfigKey::ExtractedCookies => {
1584                self.extracted_cookies = serde_json::from_value(v.clone())?
1585            }
1586            PXConfigKey::CorsSupportEnabled => {
1587                self.cors_support_enabled = serde_json::from_value(v.clone())?
1588            }
1589            PXConfigKey::CorsPreflightRequestFilterEnabled => {
1590                self.cors_preflight_request_filter_enabled = serde_json::from_value(v.clone())?
1591            }
1592            PXConfigKey::GraphqlEnabled => {
1593                self.graphql_enabled = serde_json::from_value(v.clone())?
1594            }
1595            PXConfigKey::GraphqlRoutes => {
1596                let pats: Vec<String> = serde_json::from_value(v.clone())?;
1597                self.graphql_routes = compile_regex_vec(pats, key.as_ref());
1598            }
1599            PXConfigKey::SensitiveGraphqlOperationNames => {
1600                self.sensitive_graphql_operation_names = serde_json::from_value(v.clone())?
1601            }
1602            PXConfigKey::SensitiveGraphqlOperationTypes => {
1603                self.sensitive_graphql_operation_types = serde_json::from_value(v.clone())?
1604            }
1605            PXConfigKey::GraphqlBodyMaxLength => {
1606                self.graphql_body_max_length = serde_json::from_value(v.clone())?
1607            }
1608            PXConfigKey::GraphqlKeywords => {
1609                let pats: Vec<String> = serde_json::from_value(v.clone())?;
1610                self.graphql_keywords = compile_regex_vec(pats, key.as_ref());
1611            }
1612            PXConfigKey::S2sTimeout => self.s2s_timeout = serde_json::from_value(v.clone())?,
1613            PXConfigKey::TokenVersion => self.token_version = serde_json::from_value(v.clone())?,
1614            PXConfigKey::CustomFirstPartyCaptchaEndpoint => {
1615                self.custom_first_party_captcha_endpoint = serde_json::from_value(v.clone())?
1616            }
1617            PXConfigKey::CustomFirstPartySensorEndpoint => {
1618                self.custom_first_party_sensor_endpoint = serde_json::from_value(v.clone())?
1619            }
1620            PXConfigKey::CustomFirstPartyXhrEndpoint => {
1621                self.custom_first_party_xhr_endpoint = serde_json::from_value(v.clone())?
1622            }
1623            PXConfigKey::UserAgentMaxLength => {
1624                self.user_agent_max_length = serde_json::from_value(v.clone())?
1625            }
1626            PXConfigKey::RiskCookieMaxLength => {
1627                self.risk_cookie_max_length = serde_json::from_value(v.clone())?
1628            }
1629            PXConfigKey::RiskCookieMinIterations => {
1630                self.risk_cookie_min_iterations = serde_json::from_value(v.clone())?
1631            }
1632            PXConfigKey::RiskCookieMaxIterations => {
1633                self.risk_cookie_max_iterations = serde_json::from_value(v.clone())?
1634            }
1635            PXConfigKey::AgenticTrustEnabled => {
1636                self.agentic_trust_enabled = serde_json::from_value(v.clone())?
1637            }
1638            PXConfigKey::AgenticTrustMcpEndpointPath => {
1639                self.agentic_trust_mcp_endpoint_path = serde_json::from_value(v.clone())?
1640            }
1641            PXConfigKey::SecuredPxhdEnabled => {
1642                self.secured_pxhd_enabled = serde_json::from_value(v.clone())?
1643            }
1644            PXConfigKey::PxhdDomain => self.pxhd_domain = serde_json::from_value(v.clone())?,
1645            PXConfigKey::JwtCookieName => self.jwt_cookie_name = serde_json::from_value(v.clone())?,
1646            PXConfigKey::JwtCookieUserIdFieldName => {
1647                self.jwt_cookie_user_id_field_name = serde_json::from_value(v.clone())?
1648            }
1649            PXConfigKey::JwtCookieAdditionalFieldNames => {
1650                self.jwt_cookie_additional_field_names = serde_json::from_value(v.clone())?
1651            }
1652            PXConfigKey::JwtHeaderName => self.jwt_header_name = serde_json::from_value(v.clone())?,
1653            PXConfigKey::JwtHeaderUserIdFieldName => {
1654                self.jwt_header_user_id_field_name = serde_json::from_value(v.clone())?
1655            }
1656            PXConfigKey::JwtHeaderAdditionalFieldNames => {
1657                self.jwt_header_additional_field_names = serde_json::from_value(v.clone())?
1658            }
1659            PXConfigKey::LoggerAuthToken => {
1660                self.logger_auth_token = serde_json::from_value(v.clone())?
1661            }
1662            PXConfigKey::LoginCredentialsExtractionEnabled => {
1663                self.login_credentials_extraction_enabled = serde_json::from_value(v.clone())?
1664            }
1665            PXConfigKey::LoginCredentialsExtraction => {
1666                self.login_credentials_extraction = parse_ci_endpoints_from_value(v)?;
1667                rebuild_prepared_ci_endpoints(self);
1668            }
1669            PXConfigKey::CredentialsIntelligenceVersion => {
1670                self.credentials_intelligence_version = serde_json::from_value(v.clone())?
1671            }
1672            PXConfigKey::CompromisedCredentialsHeader => {
1673                self.compromised_credentials_header = serde_json::from_value(v.clone())?
1674            }
1675            PXConfigKey::SendRawUsernameOnAdditionalS2sActivity => {
1676                self.send_raw_username_on_additional_s2s_activity =
1677                    serde_json::from_value(v.clone())?
1678            }
1679            PXConfigKey::AdditionalS2sActivityEnabled => {
1680                self.additional_s2s_activity_enabled = serde_json::from_value(v.clone())?
1681            }
1682            PXConfigKey::AdditionalS2sActivityHeaderEnabled => {
1683                self.additional_s2s_activity_header_enabled = serde_json::from_value(v.clone())?
1684            }
1685            PXConfigKey::LoginSuccessfulReportingMethod => {
1686                self.login_successful_reporting_method = serde_json::from_value(v.clone())?
1687            }
1688            PXConfigKey::LoginSuccessfulBodyRegex => {
1689                self.login_successful_body_regex = serde_json::from_value(v.clone())?
1690            }
1691            PXConfigKey::LoginSuccessfulHeaderName => {
1692                self.login_successful_header_name = serde_json::from_value(v.clone())?
1693            }
1694            PXConfigKey::LoginSuccessfulHeaderValue => {
1695                self.login_successful_header_value = serde_json::from_value(v.clone())?
1696            }
1697            PXConfigKey::LoginSuccessfulStatus => {
1698                self.login_successful_status = serde_json::from_value(v.clone())?
1699            }
1700        }
1701        Ok(())
1702    }
1703
1704    /// Apply a single raw string value from the ConfigStore (or KVStore) to
1705    /// the matching field. Non-string types are parsed using the same
1706    /// fallbacks as the previous ad-hoc loader.
1707    pub(crate) fn set_from_raw(&mut self, key: PXConfigKey, raw: &str) {
1708        let raw = strip_json_quotes(raw);
1709        match key {
1710            PXConfigKey::AppId => self.app_id = raw.to_owned(),
1711            PXConfigKey::CookieSecret => self.cookie_secret = raw.to_owned(),
1712            PXConfigKey::AuthToken => self.auth_token = raw.to_owned(),
1713            PXConfigKey::Debug => self.debug = raw.parse().unwrap_or(self.debug),
1714            PXConfigKey::BlockingScore => {
1715                self.blocking_score = raw.parse().unwrap_or(self.blocking_score)
1716            }
1717            PXConfigKey::ModuleEnabled => {
1718                self.module_enabled = raw.parse().unwrap_or(self.module_enabled)
1719            }
1720            PXConfigKey::ModuleMode => self.module_mode = raw.parse().unwrap_or(self.module_mode),
1721            PXConfigKey::SensitiveHeaders => self.sensitive_headers = parse_vec(raw),
1722            PXConfigKey::SensitiveRoutes => self.sensitive_routes = parse_vec(raw),
1723            PXConfigKey::SensitiveRoutesRegex => {
1724                self.sensitive_routes_regex = parse_regex_vec(key.as_ref(), raw)
1725            }
1726            PXConfigKey::FilterByRoute => self.filter_by_route = parse_vec(raw),
1727            PXConfigKey::FilterByExtension => self.filter_by_extension = parse_vec(raw),
1728            PXConfigKey::FilterByUserAgent => self.filter_by_user_agent = parse_vec(raw),
1729            PXConfigKey::FilterByIp => self.filter_by_ip = parse_vec(raw),
1730            PXConfigKey::FilterByHttpMethod => self.filter_by_http_method = parse_vec(raw),
1731            PXConfigKey::CustomCookieHeader => self.custom_cookie_header = parse_string(raw),
1732            PXConfigKey::EnforcedRoutes => self.enforced_routes = parse_vec(raw),
1733            PXConfigKey::MonitoredRoutes => self.monitored_routes = parse_vec(raw),
1734            PXConfigKey::BypassMonitorHeader => self.bypass_monitor_header = parse_string(raw),
1735            PXConfigKey::FirstPartyEnabled => {
1736                self.first_party_enabled = raw.parse().unwrap_or(self.first_party_enabled)
1737            }
1738            PXConfigKey::CustomLogo => self.custom_logo = parse_string(raw),
1739            PXConfigKey::JsRef => self.js_ref = parse_string(raw),
1740            PXConfigKey::CssRef => self.css_ref = parse_string(raw),
1741            PXConfigKey::HumanSapiHost => self.human_sapi_host = parse_string(raw),
1742            PXConfigKey::HumanSapiBackend => self.human_sapi_backend = parse_string(raw),
1743            PXConfigKey::HumanCollectorHost => self.human_collector_host = parse_string(raw),
1744            PXConfigKey::HumanCollectorBackend => self.human_collector_backend = parse_string(raw),
1745            PXConfigKey::HumanClientHost => self.human_client_host = parse_string(raw),
1746            PXConfigKey::HumanClientBackend => self.human_client_backend = parse_string(raw),
1747            PXConfigKey::HumanCaptchaHost => self.human_captcha_host = parse_string(raw),
1748            PXConfigKey::HumanCaptchaBackend => self.human_captcha_backend = parse_string(raw),
1749            PXConfigKey::IpHeaders => self.ip_headers = parse_vec(raw),
1750            PXConfigKey::LogEndpoint => self.log_endpoint = raw.to_owned(),
1751            PXConfigKey::DataEnrichmentHeaderName => {
1752                self.data_enrichment_header_name = parse_string(raw)
1753            }
1754            PXConfigKey::ExtractedCookies => self.extracted_cookies = parse_vec(raw),
1755            PXConfigKey::CorsSupportEnabled => {
1756                self.cors_support_enabled = raw.parse().unwrap_or(self.cors_support_enabled)
1757            }
1758            PXConfigKey::CorsPreflightRequestFilterEnabled => {
1759                self.cors_preflight_request_filter_enabled = raw
1760                    .parse()
1761                    .unwrap_or(self.cors_preflight_request_filter_enabled)
1762            }
1763            PXConfigKey::GraphqlEnabled => {
1764                self.graphql_enabled = raw.parse().unwrap_or(self.graphql_enabled)
1765            }
1766            PXConfigKey::GraphqlRoutes => self.graphql_routes = parse_regex_vec(key.as_ref(), raw),
1767            PXConfigKey::SensitiveGraphqlOperationNames => {
1768                self.sensitive_graphql_operation_names = parse_vec(raw)
1769            }
1770            PXConfigKey::SensitiveGraphqlOperationTypes => {
1771                self.sensitive_graphql_operation_types = parse_vec(raw)
1772            }
1773            PXConfigKey::GraphqlBodyMaxLength => {
1774                self.graphql_body_max_length = raw.parse().unwrap_or(self.graphql_body_max_length)
1775            }
1776            PXConfigKey::GraphqlKeywords => {
1777                self.graphql_keywords = parse_regex_vec(key.as_ref(), raw)
1778            }
1779            PXConfigKey::S2sTimeout => self.s2s_timeout = raw.parse().unwrap_or(self.s2s_timeout),
1780            PXConfigKey::TokenVersion => {
1781                self.token_version = raw.parse().unwrap_or(self.token_version)
1782            }
1783            PXConfigKey::CustomFirstPartyCaptchaEndpoint => {
1784                self.custom_first_party_captcha_endpoint = parse_string(raw)
1785            }
1786            PXConfigKey::CustomFirstPartySensorEndpoint => {
1787                self.custom_first_party_sensor_endpoint = parse_string(raw)
1788            }
1789            PXConfigKey::CustomFirstPartyXhrEndpoint => {
1790                self.custom_first_party_xhr_endpoint = parse_string(raw)
1791            }
1792            PXConfigKey::UserAgentMaxLength => {
1793                self.user_agent_max_length = raw.parse().unwrap_or(self.user_agent_max_length)
1794            }
1795            PXConfigKey::RiskCookieMaxLength => {
1796                self.risk_cookie_max_length = raw.parse().unwrap_or(self.risk_cookie_max_length)
1797            }
1798            PXConfigKey::RiskCookieMinIterations => {
1799                self.risk_cookie_min_iterations =
1800                    raw.parse().unwrap_or(self.risk_cookie_min_iterations)
1801            }
1802            PXConfigKey::RiskCookieMaxIterations => {
1803                self.risk_cookie_max_iterations =
1804                    raw.parse().unwrap_or(self.risk_cookie_max_iterations)
1805            }
1806            PXConfigKey::AgenticTrustEnabled => {
1807                self.agentic_trust_enabled = raw.parse().unwrap_or(self.agentic_trust_enabled)
1808            }
1809            PXConfigKey::AgenticTrustMcpEndpointPath => {
1810                self.agentic_trust_mcp_endpoint_path = parse_string(raw)
1811            }
1812            PXConfigKey::SecuredPxhdEnabled => {
1813                self.secured_pxhd_enabled = raw.parse().unwrap_or(self.secured_pxhd_enabled)
1814            }
1815            PXConfigKey::PxhdDomain => self.pxhd_domain = parse_string(raw),
1816            PXConfigKey::JwtCookieName => self.jwt_cookie_name = parse_string(raw),
1817            PXConfigKey::JwtCookieUserIdFieldName => {
1818                self.jwt_cookie_user_id_field_name = parse_string(raw)
1819            }
1820            PXConfigKey::JwtCookieAdditionalFieldNames => {
1821                self.jwt_cookie_additional_field_names = parse_vec(raw)
1822            }
1823            PXConfigKey::JwtHeaderName => self.jwt_header_name = parse_string(raw),
1824            PXConfigKey::JwtHeaderUserIdFieldName => {
1825                self.jwt_header_user_id_field_name = parse_string(raw)
1826            }
1827            PXConfigKey::JwtHeaderAdditionalFieldNames => {
1828                self.jwt_header_additional_field_names = parse_vec(raw)
1829            }
1830            PXConfigKey::LoggerAuthToken => self.logger_auth_token = raw.to_owned(),
1831            PXConfigKey::LoginCredentialsExtractionEnabled => {
1832                self.login_credentials_extraction_enabled = raw
1833                    .parse()
1834                    .unwrap_or(self.login_credentials_extraction_enabled)
1835            }
1836            PXConfigKey::LoginCredentialsExtraction => {
1837                self.login_credentials_extraction = parse_ci_endpoints(raw);
1838                rebuild_prepared_ci_endpoints(self);
1839            }
1840            PXConfigKey::CredentialsIntelligenceVersion => {
1841                self.credentials_intelligence_version = parse_string(raw)
1842            }
1843            PXConfigKey::CompromisedCredentialsHeader => {
1844                self.compromised_credentials_header = parse_string(raw)
1845            }
1846            PXConfigKey::SendRawUsernameOnAdditionalS2sActivity => {
1847                self.send_raw_username_on_additional_s2s_activity = raw
1848                    .parse()
1849                    .unwrap_or(self.send_raw_username_on_additional_s2s_activity)
1850            }
1851            PXConfigKey::AdditionalS2sActivityEnabled => {
1852                self.additional_s2s_activity_enabled =
1853                    raw.parse().unwrap_or(self.additional_s2s_activity_enabled)
1854            }
1855            PXConfigKey::AdditionalS2sActivityHeaderEnabled => {
1856                self.additional_s2s_activity_header_enabled = raw
1857                    .parse()
1858                    .unwrap_or(self.additional_s2s_activity_header_enabled)
1859            }
1860            PXConfigKey::LoginSuccessfulReportingMethod => {
1861                self.login_successful_reporting_method = parse_string(raw)
1862            }
1863            PXConfigKey::LoginSuccessfulBodyRegex => {
1864                self.login_successful_body_regex = parse_string(raw)
1865            }
1866            PXConfigKey::LoginSuccessfulHeaderName => {
1867                self.login_successful_header_name = parse_string(raw)
1868            }
1869            PXConfigKey::LoginSuccessfulHeaderValue => {
1870                self.login_successful_header_value = parse_string(raw)
1871            }
1872            PXConfigKey::LoginSuccessfulStatus => {
1873                self.login_successful_status = parse_status_vec(raw)
1874            }
1875        }
1876    }
1877
1878    /// Register runtime callback for custom credential extraction.
1879    pub fn set_ci_extract_credentials_fn(&mut self, f: PXExtractCredentialsFn) {
1880        self.ci_extract_credentials_fn = Some(f);
1881    }
1882
1883    /// Register runtime callback for custom login-success reporting.
1884    pub fn set_ci_login_successful_fn(&mut self, f: PXLoginSuccessfulFn) {
1885        self.ci_login_successful_fn = Some(f);
1886    }
1887}