Skip to main content

px_native/profile/
schema.rs

1//! TOML-backed tenant profile. Profiles live in
2//! `px-native/profiles/<app_id>.toml`; the runtime loads them from a
3//! directory passed via configuration.
4
5use std::path::Path;
6
7use px_errors::AppError;
8use serde::{Deserialize, Serialize};
9
10/// XOR keys for the two cipher passes the eT15wiaE family of tenants
11/// uses. Other tenants can override either or both at load time.
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct XorKeys {
14    /// Payload-cipher byte XOR key (JS `iS`). Default 50.
15    pub payload: u8,
16    /// Secret-feed byte XOR key (JS `vJ`). Default 10.
17    pub secret_feed: u8,
18}
19
20impl Default for XorKeys {
21    fn default() -> Self {
22        Self {
23            payload: 50,
24            secret_feed: 10,
25        }
26    }
27}
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30pub struct TenantProfile {
31    /// `_pxAppId` value (with `PX` prefix), e.g. `PXeT15wiaE`.
32    pub app_id: String,
33    /// Lower-cased tenant tag used in URL paths (the appId stripped
34    /// of the `PX` prefix), e.g. `eT15wiaE`.
35    pub app_id_tag: String,
36    /// Sensor POST endpoint, relative to the chosen origin. Joined
37    /// after the tenant tag at runtime to form
38    /// `${origin}/${app_id_tag}${sensor_path}`.
39    pub sensor_path: String,
40    /// String the JS reference uses when `pf()` is empty
41    /// (decoded `gC(365)` in the captured init.js).
42    pub pf_fallback: String,
43    /// XOR keys. See [`XorKeys`].
44    #[serde(default)]
45    pub xor_keys: XorKeys,
46}
47
48impl TenantProfile {
49    pub fn from_toml(src: &str) -> Result<Self, AppError> {
50        toml::from_str::<Self>(src)
51            .map_err(|e| AppError::InternalError(format!("tenant profile parse: {e}")))
52    }
53
54    pub fn load(path: &Path) -> Result<Self, AppError> {
55        let bytes = std::fs::read_to_string(path)
56            .map_err(|e| AppError::InternalError(format!("read {}: {e}", path.display())))?;
57        Self::from_toml(&bytes)
58    }
59
60    /// Build the absolute sensor URL given the page origin.
61    pub fn sensor_url(&self, origin: &str) -> String {
62        format!(
63            "{}/{}{}",
64            origin.trim_end_matches('/'),
65            self.app_id_tag,
66            self.sensor_path,
67        )
68    }
69}
70
71#[cfg(test)]
72#[allow(clippy::expect_used)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn parse_et15wiae_profile() {
78        let src = r#"
79app_id      = "PXeT15wiaE"
80app_id_tag  = "eT15wiaE"
81sensor_path = "/xhr/b/s"
82pf_fallback = "fallback-pf"
83
84[xor_keys]
85payload     = 50
86secret_feed = 10
87"#;
88        let p = TenantProfile::from_toml(src).expect("parse");
89        assert_eq!(p.app_id, "PXeT15wiaE");
90        assert_eq!(
91            p.sensor_url("https://www.pedidosya.com.ar"),
92            "https://www.pedidosya.com.ar/eT15wiaE/xhr/b/s"
93        );
94        assert_eq!(p.xor_keys, XorKeys::default());
95    }
96
97    #[test]
98    fn xor_keys_default_when_section_omitted() {
99        let src = r#"
100app_id      = "PXabc"
101app_id_tag  = "abc"
102sensor_path = "/xhr/b/s"
103pf_fallback = ""
104"#;
105        let p = TenantProfile::from_toml(src).expect("parse");
106        assert_eq!(p.xor_keys, XorKeys::default());
107    }
108}