use serde_json::{Map, Value, json};
use super::{NODE_KIND, OUTPUT_PIN_OUT};
use crate::language::LanguageEngine;
use crate::pipeline::PipelineError;
use crate::pipeline::nodes::NodeExecutionOutput;
use crate::rwe::{CompiledTemplate, ReactiveWebEngine, ReactiveWebOptions, TemplateSource};
pub struct CompiledPage {
pub node_id: String,
pub template: CompiledTemplate,
}
pub fn compile_page(
node_id: &str,
template: &TemplateSource,
options: &ReactiveWebOptions,
rwe: &dyn ReactiveWebEngine,
language: &dyn LanguageEngine,
) -> Result<CompiledPage, PipelineError> {
let compiled_template = rwe
.compile_template(template, language, options)
.map_err(|e| {
PipelineError::new(
"FW_NODE_WEB_RESPONSE_SEND_COMPILE",
format!("failed compiling node '{}': {}", node_id, e),
)
})?;
Ok(CompiledPage {
node_id: node_id.to_string(),
template: compiled_template,
})
}
fn strip_private_auth_claims(mut payload: Value) -> Value {
if let Some(obj) = payload.as_object_mut() {
public_auth_only(obj);
for source in crate::pipeline::nodes::basic::trigger::SOURCE_KEYS {
if let Some(Value::Object(envelope)) = obj.get_mut(source) {
public_auth_only(envelope);
}
}
}
payload
}
fn public_auth_only(holder: &mut Map<String, Value>) {
let auth = match holder.get("auth") {
Some(Value::Object(m)) => m.clone(),
_ => return,
};
let public_keys: Vec<String> = match auth.get("_zf_public") {
Some(Value::Array(arr)) => arr
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect(),
_ => vec![],
};
if public_keys.is_empty() {
holder.insert("auth".to_string(), Value::Null);
} else {
let mut public_auth = Map::new();
for key in &public_keys {
if let Some(v) = auth.get(key) {
public_auth.insert(key.clone(), v.clone());
}
}
holder.insert("auth".to_string(), Value::Object(public_auth));
}
}
fn inject_trigger_fields(mut state: Value, metadata: &Value) -> Value {
let route = metadata
.get("trigger")
.and_then(|trigger| trigger.get("pathname"))
.and_then(Value::as_str)
.or_else(|| metadata.get("route").and_then(Value::as_str));
if let Some(route) = route {
if let Value::Object(ref mut map) = state {
if !map.contains_key("route") {
map.insert("route".to_string(), Value::String(route.to_string()));
}
}
}
let Some(trigger) = metadata.get("trigger") else {
return state;
};
let Value::Object(ref mut map) = state else {
return state;
};
for key in &["params", "query", "search", "headers"] {
if !map.contains_key(*key) {
if let Some(v) = trigger.get(*key) {
map.insert(key.to_string(), v.clone());
}
}
}
if !map.contains_key("auth") || map.get("auth") == Some(&Value::Null) {
if let Some(auth) = trigger.get("auth") {
map.insert("auth".to_string(), auth.clone());
}
}
state
}
pub fn render_compiled_page(
compiled: &CompiledPage,
state: Value,
metadata: Value,
rwe: &dyn ReactiveWebEngine,
language: &dyn LanguageEngine,
request_id: &str,
enabled_libraries: Vec<String>,
) -> Result<NodeExecutionOutput, PipelineError> {
let route = metadata
.get("route")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.unwrap_or("/")
.to_string();
let state = inject_trigger_fields(state, &metadata);
let state = strip_private_auth_claims(state);
let rendered = rwe
.render(
&compiled.template,
state,
language,
&crate::rwe::RenderContext {
route,
request_id: request_id.to_string(),
metadata,
enabled_libraries,
},
)
.map_err(|e| {
PipelineError::new(
"FW_NODE_WEB_RESPONSE_SEND_RENDER",
format!("failed rendering node '{}': {}", compiled.node_id, e),
)
})?;
let mut trace = vec![
format!("node={}", compiled.node_id),
format!("node_kind={NODE_KIND}"),
format!("output_pin={OUTPUT_PIN_OUT}"),
];
trace.extend(rendered.trace);
Ok(NodeExecutionOutput {
output_pins: vec![OUTPUT_PIN_OUT.to_string()],
payload: json!({
"html": rendered.html,
"compiled_scripts": rendered.compiled_scripts,
"hydration_payload": rendered.hydration_payload,
}),
trace,
})
}
#[cfg(test)]
mod tests {
use serde_json::json;
#[test]
fn page_route_uses_browser_path_without_changing_pipeline_route() {
let metadata = json!({
"route": "/probe",
"trigger": {"pathname": "/wh/owner/project/probe"}
});
let injected = super::inject_trigger_fields(json!({}), &metadata);
assert_eq!(injected["route"], "/wh/owner/project/probe");
assert_eq!(metadata["route"], "/probe");
let explicit = super::inject_trigger_fields(json!({"route": "/custom"}), &metadata);
assert_eq!(explicit["route"], "/custom");
let fallback = super::inject_trigger_fields(json!({}), &json!({"route": "/preview"}));
assert_eq!(fallback["route"], "/preview");
}
#[test]
fn render_boundary_injects_trigger_fields_and_filters_auth_claims() {
let state = json!({
"title": "Hello",
"params": { "slug": "keep-existing" },
"auth": null
});
let metadata = json!({
"trigger": {
"params": { "slug": "from-trigger" },
"query": { "page": "1" },
"search": "?page=1&tag=one&tag=two",
"headers": { "x-request-id": "req-123" },
"auth": {
"sub": "user-1",
"role": "admin",
"secret": "internal-only",
"_zf_public": ["sub", "role"]
}
}
});
let injected = super::inject_trigger_fields(state, &metadata);
assert_eq!(injected["params"]["slug"], "keep-existing");
assert_eq!(injected["query"]["page"], "1");
assert_eq!(injected["search"], "?page=1&tag=one&tag=two");
assert_eq!(injected["headers"]["x-request-id"], "req-123");
assert_eq!(injected["auth"]["secret"], "internal-only");
let filtered = super::strip_private_auth_claims(injected);
assert_eq!(
filtered["auth"],
json!({
"sub": "user-1",
"role": "admin"
})
);
assert!(filtered["auth"].get("secret").is_none());
}
#[test]
fn claims_inside_a_trigger_answer_are_filtered_too() {
let state = json!({
"webhook": { "body": null, "auth": { "sub": "user-1", "team": "staff-only", "_zf_public": ["sub"] } },
"room": { "auth": { "sub": "user-2", "team": "staff-only" } }
});
let filtered = super::strip_private_auth_claims(state);
assert_eq!(filtered["webhook"]["auth"], json!({ "sub": "user-1" }));
assert_eq!(filtered["room"]["auth"], json!(null), "nothing public: null");
}
}