@ezetgalaxy/titan 25.12.4 → 25.12.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@ edition = "2024"
7
7
  [dependencies]
8
8
  axum = "0.8.7"
9
9
  dotenv = "0.15.0"
10
- reqwest = { version = "0.12.24", features = ["json", "rustls-tls"] }
10
+ reqwest = { version = "0.12.24", features = ["json", "rustls-tls", "gzip", "brotli", "blocking"] }
11
11
  serde = { version = "1.0.228", features = ["derive"] }
12
12
  serde_json = "1.0.145"
13
13
  thiserror = "2.0.17"
@@ -16,5 +16,7 @@ tower-http = { version = "0.6.7", features = ["cors"] }
16
16
  tracing = "0.1.43"
17
17
  tracing-subscriber = "0.3.22"
18
18
  anyhow = "1"
19
- boa_engine = "0.21.0"
19
+ boa_engine = "0.20.0"
20
20
  dotenvy = "0.15"
21
+ base64 = "0.21"
22
+ regex = "1.10"
@@ -1,3 +1,4 @@
1
+ // src/main.rs
1
2
  use std::{collections::HashMap, fs, sync::Arc, path::PathBuf};
2
3
 
3
4
  use anyhow::Result;
@@ -9,21 +10,24 @@ use axum::{
9
10
  routing::any,
10
11
  Router,
11
12
  };
13
+
12
14
  use boa_engine::{
13
- Context,
14
- Source
15
+ Context, JsValue, Source,
16
+ native_function::NativeFunction,
17
+ object::ObjectInitializer,
18
+ property::Attribute,
15
19
  };
20
+ use boa_engine::js_string;
16
21
 
17
22
  use serde::Deserialize;
18
23
  use serde_json::Value;
19
24
  use tokio::net::TcpListener;
25
+ use tokio::task;
20
26
 
27
+ use reqwest::blocking::Client;
28
+ use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
21
29
 
22
-
23
-
24
- // ----------------------
25
- // Route structures
26
- // ----------------------
30
+ /// Route configuration entry parsed from routes.json
27
31
  #[derive(Debug, Deserialize)]
28
32
  struct RouteVal {
29
33
  r#type: String,
@@ -33,12 +37,121 @@ struct RouteVal {
33
37
  #[derive(Clone)]
34
38
  struct AppState {
35
39
  routes: Arc<HashMap<String, RouteVal>>,
36
- project_root: PathBuf, // FIXED: replaces server_dir
40
+ project_root: PathBuf,
41
+ }
42
+
43
+ /// Inject a synchronous `t.fetch(url, opts?)` into the Boa context.
44
+ /// This `t.fetch` runs the blocking HTTP call inside `tokio::task::block_in_place`
45
+ /// so it is safe to call while inside an async Tokio context.
46
+ fn inject_t_fetch(ctx: &mut Context) {
47
+ // Create native Rust function (Boa v0.20)
48
+ let t_fetch_native = NativeFunction::from_fn_ptr(|_this, args, ctx| {
49
+ // Extract arguments (safely convert JS strings to owned Rust Strings)
50
+ let url = args
51
+ .get(0)
52
+ .and_then(|v| v.as_string().map(|s| s.to_std_string_escaped()))
53
+ .unwrap_or_default();
54
+
55
+ // opts may be undefined. Convert to serde_json::Value for thread-safety.
56
+ let opts_js = args.get(1).cloned().unwrap_or(JsValue::undefined());
57
+ let opts_json: Value = match opts_js.to_json(ctx) {
58
+ Ok(v) => v,
59
+ Err(_) => Value::Object(serde_json::Map::new()),
60
+ };
61
+
62
+ // Extract method, body, headers from opts_json (owned data, Send)
63
+ let method = opts_json
64
+ .get("method")
65
+ .and_then(|m| m.as_str())
66
+ .map(|s| s.to_string())
67
+ .unwrap_or_else(|| "GET".to_string());
68
+
69
+ let body_opt = match opts_json.get("body") {
70
+ Some(Value::String(s)) => Some(s.clone()),
71
+ Some(other) => Some(other.to_string()),
72
+ None => None,
73
+ };
74
+
75
+ // Build header map from opts_json["headers"] if present
76
+ let mut header_pairs: Vec<(String, String)> = Vec::new();
77
+ if let Some(Value::Object(map)) = opts_json.get("headers") {
78
+ for (k, v) in map.iter() {
79
+ let v_str = match v {
80
+ Value::String(s) => s.clone(),
81
+ other => other.to_string(),
82
+ };
83
+ header_pairs.push((k.clone(), v_str));
84
+ }
85
+ }
86
+
87
+ // Perform blocking HTTP request inside block_in_place so we don't drop a blocking runtime
88
+ let out_json = task::block_in_place(move || {
89
+ // Create blocking client
90
+ let client = Client::new();
91
+
92
+ // Build request
93
+ let method_parsed = method.parse().unwrap_or(reqwest::Method::GET);
94
+ let mut req = client.request(method_parsed, &url);
95
+
96
+ // Attach headers
97
+ if !header_pairs.is_empty() {
98
+ let mut headers = HeaderMap::new();
99
+ for (k, v) in header_pairs.into_iter() {
100
+ if let (Ok(name), Ok(val)) =
101
+ (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(&v))
102
+ {
103
+ headers.insert(name, val);
104
+ }
105
+ }
106
+ req = req.headers(headers);
107
+ }
108
+
109
+ if let Some(body) = body_opt {
110
+ req = req.body(body);
111
+ }
112
+
113
+ // Send request
114
+ match req.send() {
115
+ Ok(resp) => {
116
+ let status = resp.status().as_u16();
117
+ // Try to read text, fallback to empty string on error
118
+ let text = resp.text().unwrap_or_default();
119
+ serde_json::json!({
120
+ "ok": true,
121
+ "status": status,
122
+ "body": text
123
+ })
124
+ }
125
+ Err(e) => {
126
+ serde_json::json!({
127
+ "ok": false,
128
+ "error": e.to_string()
129
+ })
130
+ }
131
+ }
132
+ });
133
+
134
+ // Convert serde_json::Value -> JsValue for return to JS
135
+ Ok(JsValue::from_json(&out_json, ctx).unwrap_or(JsValue::undefined()))
136
+ });
137
+
138
+ // Convert the native function into a JS function object (requires Realm in Boa 0.20)
139
+ let realm = ctx.realm();
140
+ let t_fetch_js_fn = t_fetch_native.to_js_function(realm);
141
+
142
+ // Build `t` object with `.fetch` property
143
+ let t_obj = ObjectInitializer::new(ctx)
144
+ .property(js_string!("fetch"), t_fetch_js_fn, Attribute::all())
145
+ .build();
146
+
147
+ // Attach to globalThis.t
148
+ ctx.global_object()
149
+ .set(js_string!("t"), JsValue::from(t_obj), false, ctx)
150
+ .unwrap();
37
151
  }
38
152
 
39
- // ----------------------
40
- // Root / Dynamic handler
41
- // ----------------------
153
+ // Axum handlers --------------------------------------------------------------
154
+
42
155
  async fn root_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
43
156
  dynamic_handler_inner(state, req).await
44
157
  }
@@ -46,9 +159,7 @@ async fn dynamic_route(state: State<AppState>, req: Request<Body>) -> impl IntoR
46
159
  dynamic_handler_inner(state, req).await
47
160
  }
48
161
 
49
- // ----------------------
50
- // Main dynamic handler
51
- // ----------------------
162
+ /// Main handler that evaluates JS actions from bundles using Boa
52
163
  async fn dynamic_handler_inner(
53
164
  State(state): State<AppState>,
54
165
  req: Request<Body>,
@@ -65,102 +176,81 @@ async fn dynamic_handler_inner(
65
176
 
66
177
  if let Some(route) = state.routes.get(&key) {
67
178
  match route.r#type.as_str() {
68
-
69
- // --------------------------
70
- // ACTION ROUTE
71
- // --------------------------
72
179
  "action" => {
73
- let action_name = route.value.as_str().unwrap_or("").trim();
74
- if action_name.is_empty() {
75
- return (
76
- StatusCode::INTERNAL_SERVER_ERROR,
77
- "Invalid action name",
78
- )
79
- .into_response();
80
- }
81
-
82
- // correct action path
83
- let action_path = state
84
- .project_root
85
- .join("server")
86
- .join("actions")
87
- .join(format!("{}.jsbundle", action_name));
88
-
89
- if !action_path.exists() {
90
- return (
91
- StatusCode::NOT_FOUND,
92
- format!("Action bundle not found: {:?}", action_path),
93
- )
94
- .into_response();
95
- }
96
-
97
- // read JS bundle
98
- let js_code = match fs::read_to_string(action_path) {
99
- Ok(v) => v,
100
- Err(e) => {
101
- return (
102
- StatusCode::INTERNAL_SERVER_ERROR,
103
- format!("Failed reading action bundle: {}", e),
104
- )
105
- .into_response();
106
- }
107
- };
108
-
109
- // ---------------------------
110
- // ENV injection (correct way)
111
- // ---------------------------
112
- let mut env_map = serde_json::Map::new();
113
- for (k, v) in std::env::vars() {
114
- env_map.insert(k, Value::String(v));
115
- }
116
-
117
- let env_json = serde_json::Value::Object(env_map);
118
-
119
- // Final JS code to execute in Boa
120
- let injected = format!(
121
- r#"
122
- globalThis.process = {{
123
- env: {}
124
- }};
125
-
126
- const __titan_req = {};
127
- {};
128
-
129
- {}(__titan_req);
130
- "#,
131
- env_json.to_string(),
132
- body_str,
133
- js_code,
134
- action_name
135
- );
136
-
137
- // Execute JS safely
138
- let mut ctx = Context::default();
139
- let result = match ctx.eval(Source::from_bytes(&injected)) {
140
- Ok(v) => v,
141
- Err(e) => {
142
- return Json(json_error(e.to_string())).into_response();
143
- }
144
- };
180
+ let action_name = route.value.as_str().unwrap_or("").trim();
181
+ if action_name.is_empty() {
182
+ return (
183
+ StatusCode::INTERNAL_SERVER_ERROR,
184
+ "Invalid action name",
185
+ )
186
+ .into_response();
187
+ }
145
188
 
146
- // Convert Boa -> JSON
147
- let result_json: Value = match result.to_json(&mut ctx) {
148
- Ok(Some(v)) => v,
149
- Ok(None) => serde_json::json!({ "error": "JS returned undefined" }),
150
- Err(e) => json_error(e.to_string()),
151
- };
189
+ let action_path = state
190
+ .project_root
191
+ .join("server")
192
+ .join("actions")
193
+ .join(format!("{}.jsbundle", action_name));
194
+
195
+ if !action_path.exists() {
196
+ return (
197
+ StatusCode::NOT_FOUND,
198
+ format!("Action bundle not found: {:?}", action_path),
199
+ )
200
+ .into_response();
201
+ }
152
202
 
153
- return Json(result_json).into_response();
154
- }
203
+ let js_code = match fs::read_to_string(action_path) {
204
+ Ok(v) => v,
205
+ Err(e) => {
206
+ return (
207
+ StatusCode::INTERNAL_SERVER_ERROR,
208
+ format!("Failed reading action bundle: {}", e),
209
+ )
210
+ .into_response();
211
+ }
212
+ };
213
+
214
+ // Build env
215
+ let mut env_map = serde_json::Map::new();
216
+ for (k, v) in std::env::vars() {
217
+ env_map.insert(k, Value::String(v));
218
+ }
219
+ let env_json = Value::Object(env_map);
220
+
221
+ // Injected JS: set process.env, provide request payload, then eval bundle and call action
222
+ let injected = format!(
223
+ r#"
224
+ globalThis.process = {{ env: {} }};
225
+ const __titan_req = {};
226
+ {};
227
+ {}(__titan_req);
228
+ "#,
229
+ env_json.to_string(),
230
+ body_str,
231
+ js_code,
232
+ action_name
233
+ );
234
+
235
+ // Create Boa context, inject t.fetch, evaluate
236
+ let mut ctx = Context::default();
237
+ inject_t_fetch(&mut ctx);
238
+
239
+ let result = match ctx.eval(Source::from_bytes(&injected)) {
240
+ Ok(v) => v,
241
+ Err(e) => return Json(json_error(e.to_string())).into_response(),
242
+ };
243
+
244
+ // to_json returns Result<Value, JsError> in Boa 0.20
245
+ let result_json: Value = match result.to_json(&mut ctx) {
246
+ Ok(v) => v,
247
+ Err(e) => json_error(e.to_string()),
248
+ };
249
+
250
+ return Json(result_json).into_response();
251
+ }
155
252
 
156
- // --------------------------
157
- // STATIC JSON
158
- // --------------------------
159
253
  "json" => return Json(route.value.clone()).into_response(),
160
-
161
- // --------------------------
162
- // TEXT
163
- // --------------------------
164
254
  _ => {
165
255
  if let Some(s) = route.value.as_str() {
166
256
  return s.to_string().into_response();
@@ -177,11 +267,8 @@ fn json_error(msg: String) -> Value {
177
267
  serde_json::json!({ "error": msg })
178
268
  }
179
269
 
270
+ // Entrypoint -----------------------------------------------------------------
180
271
 
181
-
182
- // ----------------------
183
- // MAIN
184
- // ----------------------
185
272
  #[tokio::main]
186
273
  async fn main() -> Result<()> {
187
274
  dotenvy::dotenv().ok();
@@ -195,7 +282,7 @@ async fn main() -> Result<()> {
195
282
  let map: HashMap<String, RouteVal> =
196
283
  serde_json::from_value(routes_json).unwrap_or_default();
197
284
 
198
- let project_root = std::env::current_dir()?
285
+ let project_root = std::env::current_dir()?
199
286
  .parent()
200
287
  .unwrap()
201
288
  .to_path_buf();
@@ -205,16 +292,14 @@ async fn main() -> Result<()> {
205
292
  project_root,
206
293
  };
207
294
 
208
- // router
209
295
  let app = Router::new()
210
296
  .route("/", any(root_route))
211
297
  .fallback(any(dynamic_route))
212
298
  .with_state(state);
213
299
 
214
- // run
215
300
  let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
216
301
 
217
- //
302
+ //
218
303
  // TITAN BANNER
219
304
  //
220
305
  println!("\n\x1b[38;5;208m████████╗██╗████████╗ █████╗ ███╗ ██╗");
@@ -225,7 +310,7 @@ async fn main() -> Result<()> {
225
310
  println!(" ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝\x1b[0m\n");
226
311
 
227
312
  println!("\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}", port);
228
-
313
+
229
314
  axum::serve(listener, app).await?;
230
315
  Ok(())
231
316
  }