1use super::{architect_http, database};
3use pharos_runtime_architect::{
4 AgentErrorKind,
5 database::ArchitectStore,
6 project_identity::{IdentityCandidate, IdentityPreview},
7};
8use pharos_runtime_database::{
9 DbBackendConfig,
10 contract::AppDataContract,
11 operations::{DatabaseOperations, operation_fingerprint},
12 snapshot::DatabaseSnapshot,
13};
14use serde_json::Value;
15use std::sync::Arc;
16
17struct OriginalPreview {
19 adapter: Arc<dyn DatabaseOperations>,
20 config: DbBackendConfig,
21 contract: AppDataContract,
22 fingerprint: String,
23}
24
25pub(super) struct NativeIdentityPreview<'a> {
27 original: Option<OriginalPreview>,
28 app: &'a crate::config::AppRuntime,
29 request: &'a super::http::HttpRequest,
30 actor_id: i64,
31}
32
33impl<'a> NativeIdentityPreview<'a> {
34 pub(super) fn new(
36 app: &'a crate::config::AppRuntime,
37 request: &'a super::http::HttpRequest,
38 actor_id: i64,
39 ) -> Self {
40 Self {
41 original: None,
42 app,
43 request,
44 actor_id,
45 }
46 }
47}
48
49impl IdentityPreview for NativeIdentityPreview<'_> {
50 fn prepare(
52 &mut self,
53 store: &ArchitectStore,
54 candidate: &IdentityCandidate,
55 ) -> Result<(), AgentErrorKind> {
56 let old_workspace =
57 architect_http::architect_editor_preview_workspace(&candidate.old_source);
58 if !architect_http::architect_sync_editor_preview_config(
59 store,
60 candidate.task_id,
61 &old_workspace,
62 ) {
63 return Err(AgentErrorKind::InvalidPath);
64 }
65 let old_artifact = architect_http::architect_materialized_preview_root(
66 &candidate.old_source,
67 &old_workspace,
68 &candidate.old_app_id,
69 )
70 .ok_or(AgentErrorKind::InvalidPath)?;
71 let old_conf = old_workspace.join("runtime.conf");
72 let old =
73 database::logical_app_store(&old_artifact, Some(&old_conf), &candidate.old_app_id)
74 .map_err(|_| AgentErrorKind::InvalidPath)?;
75 let old_contract =
76 database::selected_logical_contract(&old_artifact, &candidate.old_app_id)
77 .map_err(|_| AgentErrorKind::InvalidEnvelope)?
78 .ok_or(AgentErrorKind::InvalidEnvelope)?;
79 let snapshot = DatabaseSnapshot::capture(old.adapter.as_ref(), &old.config, &old_contract)
80 .map_err(|_| AgentErrorKind::InvalidPath)?;
81 let fingerprint =
82 operation_fingerprint(&snapshot).map_err(|_| AgentErrorKind::CapacityExceeded)?;
83 pharos_runtime_architect::lifecycle_validation::initialize_editor_preview_config(
84 &candidate.preview_workspace,
85 &candidate.new_app_id,
86 store.config(),
87 )?;
88 let new_conf = candidate.preview_workspace.join("runtime.conf");
89 let build_dir = candidate.preview_workspace.join("dist");
90 super::build_app_from_root(&candidate.new_source, &build_dir)
91 .map_err(|_| AgentErrorKind::InvalidEnvelope)?;
92 let new_artifact = build_dir.join("apps").join(&candidate.new_app_id);
93 if database::dynamic_runtime_prepare(
94 &new_artifact,
95 Some(&new_conf),
96 Some(&candidate.new_app_id),
97 ) != 0
98 {
99 return Err(AgentErrorKind::InvalidEnvelope);
100 }
101 let target =
102 database::logical_app_store(&new_artifact, Some(&new_conf), &candidate.new_app_id)
103 .map_err(|_| AgentErrorKind::InvalidPath)?;
104 let contract = database::selected_logical_contract(&new_artifact, &candidate.new_app_id)
105 .map_err(|_| AgentErrorKind::InvalidEnvelope)?
106 .ok_or(AgentErrorKind::InvalidEnvelope)?;
107 let old_mount = format!("/{}", candidate.old_app_id.replace('_', "-"));
108 let new_mount = format!("/{}", candidate.new_app_id.replace('_', "-"));
109 let mut copied = snapshot;
110 copied.app_id = candidate.new_app_id.clone();
111 copied.contract_fingerprint =
112 operation_fingerprint(&contract).map_err(|_| AgentErrorKind::InvalidEnvelope)?;
113 copied.records.retain(|record| {
115 !matches!(
116 record.entity.as_str(),
117 "auth_session" | "social_connection" | "social_collection" | "social_observation"
118 )
119 });
120 for record in &mut copied.records {
121 for value in record.fields.values_mut() {
122 rewrite_mount(value, &old_mount, &new_mount, 0)?;
123 }
124 }
125 copied
126 .restore(target.adapter.as_ref(), &target.config, &contract, &[])
127 .map_err(|_| AgentErrorKind::InvalidPath)?;
128 self.original = Some(OriginalPreview {
129 adapter: old.adapter,
130 config: old.config,
131 contract: old_contract,
132 fingerprint,
133 });
134 Ok(())
135 }
136 fn check_unchanged(&self) -> Result<(), AgentErrorKind> {
138 if super::active_session_user_id(self.app, self.request.cookie.as_deref())
139 .map_err(|_| AgentErrorKind::PolicyViolation)?
140 != self.actor_id
141 || !super::actor_has_route_policy(self.app, self.actor_id, "review_builder_plan")
142 .map_err(|_| AgentErrorKind::PolicyViolation)?
143 {
144 return Err(AgentErrorKind::PolicyViolation);
145 }
146 let old = self.original.as_ref().ok_or(AgentErrorKind::InvalidPath)?;
147 let snapshot = DatabaseSnapshot::capture(old.adapter.as_ref(), &old.config, &old.contract)
148 .map_err(|_| AgentErrorKind::InvalidPath)?;
149 if operation_fingerprint(&snapshot).map_err(|_| AgentErrorKind::CapacityExceeded)?
150 != old.fingerprint
151 {
152 return Err(AgentErrorKind::StalePlan);
153 }
154 Ok(())
155 }
156}
157
158fn rewrite_mount(
160 value: &mut Value,
161 old: &str,
162 new: &str,
163 depth: usize,
164) -> Result<(), AgentErrorKind> {
165 if depth > 64 {
166 return Err(AgentErrorKind::CapacityExceeded);
167 }
168 match value {
169 Value::String(text)
170 if text
171 .strip_prefix(old)
172 .is_some_and(|tail| tail.is_empty() || tail.starts_with(['/', '?', '#'])) =>
173 {
174 *text = format!("{new}{}", &text[old.len()..]);
175 }
176 Value::Array(values) => {
177 for value in values {
178 rewrite_mount(value, old, new, depth + 1)?;
179 }
180 }
181 Value::Object(values) => {
182 for value in values.values_mut() {
183 rewrite_mount(value, old, new, depth + 1)?;
184 }
185 }
186 _ => {}
187 }
188 Ok(())
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 #[test]
196 fn mount_rewrite_preserves_external_and_prefix_values() {
197 let mut value = serde_json::json!(["/original-app/static/image.png","https://example.com/original-app/","/original-application/",{"url":"/original-app?tab=1"}]);
198 rewrite_mount(&mut value, "/original-app", "/renamed-app", 0).unwrap();
199 assert_eq!(
200 value,
201 serde_json::json!(["/renamed-app/static/image.png","https://example.com/original-app/","/original-application/",{"url":"/renamed-app?tab=1"}])
202 );
203 }
204}