@ezetgalaxy/titan 25.12.3 → 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.
- package/README.md +144 -181
- package/package.json +1 -1
- package/templates/Dockerfile +2 -2
- package/templates/server/Cargo.lock +443 -427
- package/templates/server/Cargo.toml +4 -2
- package/templates/server/src/main.rs +199 -114
|
@@ -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.
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|