titanpl-sdk 0.1.4 → 0.1.6
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/package.json +1 -1
- package/templates/Dockerfile +17 -4
- package/templates/server/src/extensions.rs +423 -218
- package/templates/server/src/main.rs +134 -68
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "titanpl-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Development SDK for Titan Planet. Provides TypeScript type definitions for the global 't' runtime object and a 'lite' test-harness runtime for building and verifying extensions.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
package/templates/Dockerfile
CHANGED
|
@@ -18,6 +18,20 @@ COPY . .
|
|
|
18
18
|
# Install JS dependencies (needed for Titan DSL + bundler)
|
|
19
19
|
RUN npm install
|
|
20
20
|
|
|
21
|
+
SHELL ["/bin/bash", "-c"]
|
|
22
|
+
|
|
23
|
+
# Extract Titan extensions into .ext
|
|
24
|
+
RUN mkdir -p /app/.ext && \
|
|
25
|
+
find /app/node_modules -maxdepth 5 -type f -name "titan.json" -print0 | \
|
|
26
|
+
while IFS= read -r -d '' file; do \
|
|
27
|
+
pkg_dir="$(dirname "$file")"; \
|
|
28
|
+
pkg_name="$(basename "$pkg_dir")"; \
|
|
29
|
+
echo "Copying Titan extension: $pkg_name from $pkg_dir"; \
|
|
30
|
+
cp -r "$pkg_dir" "/app/.ext/$pkg_name"; \
|
|
31
|
+
done && \
|
|
32
|
+
echo "Extensions in .ext:" && \
|
|
33
|
+
ls -R /app/.ext
|
|
34
|
+
|
|
21
35
|
# Build Titan metadata + bundle JS actions
|
|
22
36
|
RUN titan build
|
|
23
37
|
|
|
@@ -34,7 +48,7 @@ FROM debian:stable-slim
|
|
|
34
48
|
WORKDIR /app
|
|
35
49
|
|
|
36
50
|
# Copy Rust binary from builder stage
|
|
37
|
-
COPY --from=builder /app/server/target/release/server ./titan-server
|
|
51
|
+
COPY --from=builder /app/server/target/release/titan-server ./titan-server
|
|
38
52
|
|
|
39
53
|
# Copy Titan routing metadata
|
|
40
54
|
COPY --from=builder /app/server/routes.json ./routes.json
|
|
@@ -44,10 +58,9 @@ COPY --from=builder /app/server/action_map.json ./action_map.json
|
|
|
44
58
|
RUN mkdir -p /app/actions
|
|
45
59
|
COPY --from=builder /app/server/actions /app/actions
|
|
46
60
|
|
|
47
|
-
|
|
61
|
+
# Copy only Titan extensions
|
|
62
|
+
COPY --from=builder /app/.ext ./.ext
|
|
48
63
|
|
|
49
|
-
# Expose Titan port
|
|
50
64
|
EXPOSE 3000
|
|
51
65
|
|
|
52
|
-
# Start Titan
|
|
53
66
|
CMD ["./titan-server"]
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
#![allow(unused)]
|
|
2
|
-
use
|
|
2
|
+
use bcrypt::{DEFAULT_COST, hash, verify};
|
|
3
|
+
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
|
3
4
|
use reqwest::{
|
|
4
5
|
blocking::Client,
|
|
5
6
|
header::{HeaderMap, HeaderName, HeaderValue},
|
|
6
7
|
};
|
|
7
|
-
use
|
|
8
|
+
use serde_json::Value;
|
|
8
9
|
use std::path::PathBuf;
|
|
10
|
+
use std::sync::Once;
|
|
9
11
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
10
|
-
use
|
|
11
|
-
use jsonwebtoken::{encode, decode, Header, EncodingKey, DecodingKey, Validation};
|
|
12
|
-
use bcrypt::{hash, verify, DEFAULT_COST};
|
|
12
|
+
use v8;
|
|
13
13
|
|
|
14
14
|
use crate::utils::{blue, gray, green, parse_expires_in};
|
|
15
|
-
use libloading::
|
|
16
|
-
use walkdir::WalkDir;
|
|
17
|
-
use std::sync::Mutex;
|
|
15
|
+
use libloading::Library;
|
|
18
16
|
use std::collections::HashMap;
|
|
19
17
|
use std::fs;
|
|
18
|
+
use std::sync::Mutex;
|
|
19
|
+
use walkdir::WalkDir;
|
|
20
20
|
|
|
21
21
|
// ----------------------------------------------------------------------------
|
|
22
22
|
// GLOBAL REGISTRY
|
|
@@ -25,7 +25,7 @@ use std::fs;
|
|
|
25
25
|
static REGISTRY: Mutex<Option<Registry>> = Mutex::new(None);
|
|
26
26
|
#[allow(dead_code)]
|
|
27
27
|
struct Registry {
|
|
28
|
-
_libs: Vec<Library>,
|
|
28
|
+
_libs: Vec<Library>,
|
|
29
29
|
modules: Vec<ModuleDef>,
|
|
30
30
|
natives: Vec<NativeFnEntry>, // Flattened list of all native functions
|
|
31
31
|
}
|
|
@@ -73,80 +73,188 @@ pub fn load_project_extensions(root: PathBuf) {
|
|
|
73
73
|
let mut libs = Vec::new();
|
|
74
74
|
let mut all_natives = Vec::new();
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
// =====================================================
|
|
77
|
+
// 1. Resolve all extension search directories
|
|
78
|
+
// =====================================================
|
|
79
|
+
|
|
80
|
+
let mut search_dirs = Vec::new();
|
|
81
|
+
|
|
82
|
+
let ext_dir = root.join(".ext"); // Production
|
|
83
|
+
let nm_root = root.join("node_modules"); // Dev
|
|
84
|
+
let nm_parent = root.parent().map(|p| p.join("node_modules")); // Monorepo
|
|
85
|
+
|
|
86
|
+
// 1) Production
|
|
87
|
+
if ext_dir.exists() {
|
|
88
|
+
search_dirs.push(ext_dir);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2) Dev: project node_modules
|
|
92
|
+
if nm_root.exists() {
|
|
93
|
+
search_dirs.push(nm_root.clone());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 3) Dev monorepo: parent/node_modules
|
|
97
|
+
if let Some(nmp) = &nm_parent {
|
|
98
|
+
if nmp.exists() {
|
|
99
|
+
search_dirs.push(nmp.clone());
|
|
83
100
|
}
|
|
84
101
|
}
|
|
85
|
-
|
|
86
|
-
if node_modules.exists() {
|
|
87
|
-
for entry in WalkDir::new(&node_modules).follow_links(true).min_depth(1).max_depth(4) {
|
|
88
|
-
let entry = match entry { Ok(e) => e, Err(_) => continue };
|
|
89
|
-
if entry.file_type().is_file() && entry.file_name() == "titan.json" {
|
|
90
|
-
let dir = entry.path().parent().unwrap();
|
|
91
|
-
let config_content = match fs::read_to_string(entry.path()) {
|
|
92
|
-
Ok(c) => c,
|
|
93
|
-
Err(_) => continue,
|
|
94
|
-
};
|
|
95
|
-
let config: TitanConfig = match serde_json::from_str(&config_content) {
|
|
96
|
-
Ok(c) => c,
|
|
97
|
-
Err(_) => continue,
|
|
98
|
-
};
|
|
99
102
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
let sig = if fn_conf.parameters.len() == 2
|
|
109
|
-
&& fn_conf.parameters[0] == "f64"
|
|
110
|
-
&& fn_conf.parameters[1] == "f64"
|
|
111
|
-
&& fn_conf.result == "f64" {
|
|
112
|
-
Signature::F64TwoArgsRetF64
|
|
113
|
-
} else {
|
|
114
|
-
Signature::Unknown
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
if let Ok(symbol) = lib.get::<*const ()>(fn_conf.symbol.as_bytes()) {
|
|
118
|
-
let idx = all_natives.len();
|
|
119
|
-
all_natives.push(NativeFnEntry {
|
|
120
|
-
ptr: *symbol as usize,
|
|
121
|
-
sig
|
|
122
|
-
});
|
|
123
|
-
mod_natives_map.insert(fn_name, idx);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
libs.push(lib);
|
|
127
|
-
},
|
|
128
|
-
Err(e) => println!("Failed to load extension library {}: {}", lib_path.display(), e),
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
103
|
+
// 4) Never return empty — add root/node_modules even if missing
|
|
104
|
+
if search_dirs.is_empty() {
|
|
105
|
+
search_dirs.push(nm_root);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Normalize and dedupe
|
|
109
|
+
search_dirs.sort();
|
|
110
|
+
search_dirs.dedup();
|
|
132
111
|
|
|
133
|
-
|
|
134
|
-
|
|
112
|
+
println!("{} Scanning extension directories:", blue("[Titan]"));
|
|
113
|
+
for d in &search_dirs {
|
|
114
|
+
println!(" • {}", d.display());
|
|
115
|
+
}
|
|
135
116
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
117
|
+
// =====================================================
|
|
118
|
+
// 2. Walk and locate titan.json inside search paths
|
|
119
|
+
// =====================================================
|
|
120
|
+
for dir in &search_dirs {
|
|
121
|
+
if !dir.exists() {
|
|
122
|
+
println!(" {} Skipping non-existent directory: {}", crate::utils::yellow("⚠"), dir.display());
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for entry in WalkDir::new(&dir)
|
|
127
|
+
.min_depth(1)
|
|
128
|
+
.max_depth(5) // Increased depth
|
|
129
|
+
.follow_links(true)
|
|
130
|
+
{
|
|
131
|
+
let entry = match entry {
|
|
132
|
+
Ok(e) => e,
|
|
133
|
+
Err(_) => continue,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Only accept titan.json files
|
|
137
|
+
if entry.file_type().is_file() && entry.file_name() == "titan.json" {
|
|
138
|
+
let path = entry.path();
|
|
139
|
+
// Load config file
|
|
140
|
+
let config_content = match fs::read_to_string(path) {
|
|
141
|
+
Ok(c) => c,
|
|
142
|
+
Err(e) => {
|
|
143
|
+
println!("{} Failed to read {}: {}", crate::utils::red("[Titan]"), path.display(), e);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
let config: TitanConfig = match serde_json::from_str(&config_content) {
|
|
149
|
+
Ok(c) => c,
|
|
150
|
+
Err(e) => {
|
|
151
|
+
println!("{} Failed to parse {}: {}", crate::utils::red("[Titan]"), path.display(), e);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
let pkg_dir = path.parent().unwrap();
|
|
157
|
+
let mut mod_natives_map = HashMap::new();
|
|
158
|
+
|
|
159
|
+
// =====================================================
|
|
160
|
+
// 3. Load native extension (optional)
|
|
161
|
+
// =====================================================
|
|
162
|
+
if let Some(native_conf) = config.native {
|
|
163
|
+
let lib_path = pkg_dir.join(&native_conf.path);
|
|
164
|
+
|
|
165
|
+
unsafe {
|
|
166
|
+
match Library::new(&lib_path) {
|
|
167
|
+
Ok(lib) => {
|
|
168
|
+
for (fn_name, fn_conf) in native_conf.functions {
|
|
169
|
+
let sig = if fn_conf.parameters == ["f64", "f64"]
|
|
170
|
+
&& fn_conf.result == "f64"
|
|
171
|
+
{
|
|
172
|
+
Signature::F64TwoArgsRetF64
|
|
173
|
+
} else {
|
|
174
|
+
Signature::Unknown
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if let Ok(symbol) = lib.get::<*const ()>(fn_conf.symbol.as_bytes())
|
|
178
|
+
{
|
|
179
|
+
let idx = all_natives.len();
|
|
180
|
+
all_natives.push(NativeFnEntry {
|
|
181
|
+
ptr: *symbol as usize,
|
|
182
|
+
sig,
|
|
183
|
+
});
|
|
184
|
+
mod_natives_map.insert(fn_name, idx);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
libs.push(lib);
|
|
188
|
+
}
|
|
189
|
+
Err(e) => println!(
|
|
190
|
+
"{} Failed to load native library {} ({})",
|
|
191
|
+
blue("[Titan]"),
|
|
192
|
+
lib_path.display(),
|
|
193
|
+
e
|
|
194
|
+
),
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// =====================================================
|
|
200
|
+
// 4. Load JS module file
|
|
201
|
+
// =====================================================
|
|
202
|
+
let js_path = pkg_dir.join(&config.main);
|
|
203
|
+
let js_content = match fs::read_to_string(&js_path) {
|
|
204
|
+
Ok(c) => c,
|
|
205
|
+
Err(e) => {
|
|
206
|
+
println!("{} Failed to read JS main {} for extension {}: {}",
|
|
207
|
+
crate::utils::red("[Titan]"),
|
|
208
|
+
js_path.display(),
|
|
209
|
+
config.name,
|
|
210
|
+
e
|
|
211
|
+
);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
modules.push(ModuleDef {
|
|
217
|
+
name: config.name.clone(),
|
|
218
|
+
js: js_content,
|
|
219
|
+
native_indices: mod_natives_map,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
println!(
|
|
223
|
+
"{} {} {}",
|
|
224
|
+
blue("[Titan]"),
|
|
225
|
+
green("Extension loaded:"),
|
|
226
|
+
config.name
|
|
227
|
+
);
|
|
143
228
|
}
|
|
144
229
|
}
|
|
145
230
|
}
|
|
146
231
|
|
|
147
|
-
|
|
148
|
-
|
|
232
|
+
// =====================================================
|
|
233
|
+
// 5. Store registry globally
|
|
234
|
+
// =====================================================
|
|
235
|
+
if modules.is_empty() {
|
|
236
|
+
println!("{} {}", blue("[Titan]"), crate::utils::yellow("No extensions loaded."));
|
|
237
|
+
// Debug: list files in search dirs to assist debugging
|
|
238
|
+
for dir in &search_dirs {
|
|
239
|
+
if dir.exists() {
|
|
240
|
+
println!("{} Listing contents of {}:", blue("[Titan]"), dir.display());
|
|
241
|
+
for entry in WalkDir::new(dir).max_depth(5) {
|
|
242
|
+
if let Ok(e) = entry {
|
|
243
|
+
println!(" - {}", e.path().display());
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
println!("{} Directory not found: {}", blue("[Titan]"), dir.display());
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
149
251
|
|
|
252
|
+
*REGISTRY.lock().unwrap() = Some(Registry {
|
|
253
|
+
_libs: libs,
|
|
254
|
+
modules,
|
|
255
|
+
natives: all_natives,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
150
258
|
|
|
151
259
|
static V8_INIT: Once = Once::new();
|
|
152
260
|
|
|
@@ -176,7 +284,11 @@ fn throw(scope: &mut v8::HandleScope, msg: &str) {
|
|
|
176
284
|
// NATIVE CALLBACKS
|
|
177
285
|
// ----------------------------------------------------------------------------
|
|
178
286
|
|
|
179
|
-
fn native_read(
|
|
287
|
+
fn native_read(
|
|
288
|
+
scope: &mut v8::HandleScope,
|
|
289
|
+
args: v8::FunctionCallbackArguments,
|
|
290
|
+
mut retval: v8::ReturnValue,
|
|
291
|
+
) {
|
|
180
292
|
let path_val = args.get(0);
|
|
181
293
|
// 1. Read argument
|
|
182
294
|
if !path_val.is_string() {
|
|
@@ -195,7 +307,7 @@ fn native_read(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments,
|
|
|
195
307
|
let global = context.global(scope);
|
|
196
308
|
let root_key = v8_str(scope, "__titan_root");
|
|
197
309
|
let root_val = global.get(scope, root_key.into()).unwrap();
|
|
198
|
-
|
|
310
|
+
|
|
199
311
|
let root_str = if root_val.is_string() {
|
|
200
312
|
v8_to_string(scope, root_val)
|
|
201
313
|
} else {
|
|
@@ -226,14 +338,18 @@ fn native_read(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments,
|
|
|
226
338
|
match std::fs::read_to_string(&target) {
|
|
227
339
|
Ok(content) => {
|
|
228
340
|
retval.set(v8_str(scope, &content).into());
|
|
229
|
-
}
|
|
341
|
+
}
|
|
230
342
|
Err(e) => {
|
|
231
343
|
throw(scope, &format!("t.read failed: {}", e));
|
|
232
344
|
}
|
|
233
345
|
}
|
|
234
346
|
}
|
|
235
347
|
|
|
236
|
-
fn native_log(
|
|
348
|
+
fn native_log(
|
|
349
|
+
scope: &mut v8::HandleScope,
|
|
350
|
+
args: v8::FunctionCallbackArguments,
|
|
351
|
+
mut _retval: v8::ReturnValue,
|
|
352
|
+
) {
|
|
237
353
|
let context = scope.get_current_context();
|
|
238
354
|
let global = context.global(scope);
|
|
239
355
|
let action_key = v8_str(scope, "__titan_action");
|
|
@@ -244,30 +360,38 @@ fn native_log(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments,
|
|
|
244
360
|
for i in 0..args.length() {
|
|
245
361
|
let val = args.get(i);
|
|
246
362
|
let mut appended = false;
|
|
247
|
-
|
|
363
|
+
|
|
248
364
|
// Try to JSON stringify objects so they are readable in logs
|
|
249
365
|
if val.is_object() && !val.is_function() {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
366
|
+
if let Some(json) = v8::json::stringify(scope, val) {
|
|
367
|
+
parts.push(json.to_rust_string_lossy(scope));
|
|
368
|
+
appended = true;
|
|
369
|
+
}
|
|
254
370
|
}
|
|
255
|
-
|
|
371
|
+
|
|
256
372
|
if !appended {
|
|
257
373
|
parts.push(v8_to_string(scope, val));
|
|
258
374
|
}
|
|
259
375
|
}
|
|
260
|
-
|
|
376
|
+
|
|
261
377
|
println!(
|
|
262
378
|
"{} {}",
|
|
263
379
|
blue("[Titan]"),
|
|
264
|
-
gray(&format!(
|
|
380
|
+
gray(&format!(
|
|
381
|
+
"\x1b[90mlog({})\x1b[0m\x1b[97m: {}\x1b[0m",
|
|
382
|
+
action_name,
|
|
383
|
+
parts.join(" ")
|
|
384
|
+
))
|
|
265
385
|
);
|
|
266
386
|
}
|
|
267
387
|
|
|
268
|
-
fn native_fetch(
|
|
388
|
+
fn native_fetch(
|
|
389
|
+
scope: &mut v8::HandleScope,
|
|
390
|
+
args: v8::FunctionCallbackArguments,
|
|
391
|
+
mut retval: v8::ReturnValue,
|
|
392
|
+
) {
|
|
269
393
|
let url = v8_to_string(scope, args.get(0));
|
|
270
|
-
|
|
394
|
+
|
|
271
395
|
// Check for options (method, headers, body)
|
|
272
396
|
let mut method = "GET".to_string();
|
|
273
397
|
let mut body_str = None;
|
|
@@ -276,7 +400,7 @@ fn native_fetch(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments
|
|
|
276
400
|
let opts_val = args.get(1);
|
|
277
401
|
if opts_val.is_object() {
|
|
278
402
|
let opts_obj = opts_val.to_object(scope).unwrap();
|
|
279
|
-
|
|
403
|
+
|
|
280
404
|
// method
|
|
281
405
|
let m_key = v8_str(scope, "method");
|
|
282
406
|
if let Some(m_val) = opts_obj.get(scope, m_key.into()) {
|
|
@@ -284,18 +408,18 @@ fn native_fetch(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments
|
|
|
284
408
|
method = v8_to_string(scope, m_val);
|
|
285
409
|
}
|
|
286
410
|
}
|
|
287
|
-
|
|
411
|
+
|
|
288
412
|
// body
|
|
289
413
|
let b_key = v8_str(scope, "body");
|
|
290
414
|
if let Some(b_val) = opts_obj.get(scope, b_key.into()) {
|
|
291
415
|
if b_val.is_string() {
|
|
292
416
|
body_str = Some(v8_to_string(scope, b_val));
|
|
293
417
|
} else if b_val.is_object() {
|
|
294
|
-
|
|
295
|
-
|
|
418
|
+
let json_obj = v8::json::stringify(scope, b_val).unwrap();
|
|
419
|
+
body_str = Some(json_obj.to_rust_string_lossy(scope));
|
|
296
420
|
}
|
|
297
421
|
}
|
|
298
|
-
|
|
422
|
+
|
|
299
423
|
// headers
|
|
300
424
|
let h_key = v8_str(scope, "headers");
|
|
301
425
|
if let Some(h_val) = opts_obj.get(scope, h_key.into()) {
|
|
@@ -305,57 +429,61 @@ fn native_fetch(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments
|
|
|
305
429
|
for i in 0..keys.length() {
|
|
306
430
|
let key = keys.get_index(scope, i).unwrap();
|
|
307
431
|
let val = h_obj.get(scope, key).unwrap();
|
|
308
|
-
headers_vec.push((
|
|
309
|
-
v8_to_string(scope, key),
|
|
310
|
-
v8_to_string(scope, val),
|
|
311
|
-
));
|
|
432
|
+
headers_vec.push((v8_to_string(scope, key), v8_to_string(scope, val)));
|
|
312
433
|
}
|
|
313
434
|
}
|
|
314
435
|
}
|
|
315
436
|
}
|
|
316
437
|
}
|
|
317
438
|
|
|
318
|
-
let client = Client::builder()
|
|
319
|
-
|
|
439
|
+
let client = Client::builder()
|
|
440
|
+
.use_rustls_tls()
|
|
441
|
+
.tcp_nodelay(true)
|
|
442
|
+
.build()
|
|
443
|
+
.unwrap_or(Client::new());
|
|
444
|
+
|
|
320
445
|
let mut req = client.request(method.parse().unwrap_or(reqwest::Method::GET), &url);
|
|
321
|
-
|
|
446
|
+
|
|
322
447
|
for (k, v) in headers_vec {
|
|
323
|
-
if let (Ok(name), Ok(val)) = (
|
|
448
|
+
if let (Ok(name), Ok(val)) = (
|
|
449
|
+
HeaderName::from_bytes(k.as_bytes()),
|
|
450
|
+
HeaderValue::from_str(&v),
|
|
451
|
+
) {
|
|
324
452
|
let mut map = HeaderMap::new();
|
|
325
453
|
map.insert(name, val);
|
|
326
454
|
req = req.headers(map);
|
|
327
455
|
}
|
|
328
456
|
}
|
|
329
|
-
|
|
457
|
+
|
|
330
458
|
if let Some(b) = body_str {
|
|
331
459
|
req = req.body(b);
|
|
332
460
|
}
|
|
333
|
-
|
|
461
|
+
|
|
334
462
|
let res = req.send();
|
|
335
|
-
|
|
463
|
+
|
|
336
464
|
let obj = v8::Object::new(scope);
|
|
337
465
|
match res {
|
|
338
466
|
Ok(r) => {
|
|
339
467
|
let status = r.status().as_u16();
|
|
340
468
|
let text = r.text().unwrap_or_default();
|
|
341
|
-
|
|
469
|
+
|
|
342
470
|
let status_key = v8_str(scope, "status");
|
|
343
471
|
let status_val = v8::Number::new(scope, status as f64);
|
|
344
472
|
obj.set(scope, status_key.into(), status_val.into());
|
|
345
|
-
|
|
473
|
+
|
|
346
474
|
let body_key = v8_str(scope, "body");
|
|
347
475
|
let body_val = v8_str(scope, &text);
|
|
348
476
|
obj.set(scope, body_key.into(), body_val.into());
|
|
349
|
-
|
|
477
|
+
|
|
350
478
|
let ok_key = v8_str(scope, "ok");
|
|
351
479
|
let ok_val = v8::Boolean::new(scope, true);
|
|
352
480
|
obj.set(scope, ok_key.into(), ok_val.into());
|
|
353
|
-
}
|
|
481
|
+
}
|
|
354
482
|
Err(e) => {
|
|
355
483
|
let ok_key = v8_str(scope, "ok");
|
|
356
484
|
let ok_val = v8::Boolean::new(scope, false);
|
|
357
485
|
obj.set(scope, ok_key.into(), ok_val.into());
|
|
358
|
-
|
|
486
|
+
|
|
359
487
|
let err_key = v8_str(scope, "error");
|
|
360
488
|
let err_val = v8_str(scope, &e.to_string());
|
|
361
489
|
obj.set(scope, err_key.into(), err_val.into());
|
|
@@ -364,33 +492,46 @@ fn native_fetch(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments
|
|
|
364
492
|
retval.set(obj.into());
|
|
365
493
|
}
|
|
366
494
|
|
|
367
|
-
fn native_jwt_sign(
|
|
495
|
+
fn native_jwt_sign(
|
|
496
|
+
scope: &mut v8::HandleScope,
|
|
497
|
+
args: v8::FunctionCallbackArguments,
|
|
498
|
+
mut retval: v8::ReturnValue,
|
|
499
|
+
) {
|
|
368
500
|
// payload, secret, options
|
|
369
501
|
let payload_val = args.get(0);
|
|
370
502
|
// Parse payload to serde_json::Map
|
|
371
|
-
let json_str = v8::json::stringify(scope, payload_val)
|
|
372
|
-
|
|
503
|
+
let json_str = v8::json::stringify(scope, payload_val)
|
|
504
|
+
.unwrap()
|
|
505
|
+
.to_rust_string_lossy(scope);
|
|
506
|
+
let mut payload: serde_json::Map<String, Value> =
|
|
507
|
+
serde_json::from_str(&json_str).unwrap_or_default();
|
|
373
508
|
|
|
374
509
|
let secret = v8_to_string(scope, args.get(1));
|
|
375
|
-
|
|
510
|
+
|
|
376
511
|
let opts_val = args.get(2);
|
|
377
512
|
if opts_val.is_object() {
|
|
378
513
|
let opts_obj = opts_val.to_object(scope).unwrap();
|
|
379
514
|
let exp_key = v8_str(scope, "expiresIn");
|
|
380
|
-
|
|
515
|
+
|
|
381
516
|
if let Some(val) = opts_obj.get(scope, exp_key.into()) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
let now = SystemTime::now()
|
|
392
|
-
|
|
393
|
-
|
|
517
|
+
let seconds = if val.is_number() {
|
|
518
|
+
Some(val.to_number(scope).unwrap().value() as u64)
|
|
519
|
+
} else if val.is_string() {
|
|
520
|
+
parse_expires_in(&v8_to_string(scope, val))
|
|
521
|
+
} else {
|
|
522
|
+
None
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
if let Some(sec) = seconds {
|
|
526
|
+
let now = SystemTime::now()
|
|
527
|
+
.duration_since(UNIX_EPOCH)
|
|
528
|
+
.unwrap()
|
|
529
|
+
.as_secs();
|
|
530
|
+
payload.insert(
|
|
531
|
+
"exp".to_string(),
|
|
532
|
+
Value::Number(serde_json::Number::from(now + sec)),
|
|
533
|
+
);
|
|
534
|
+
}
|
|
394
535
|
}
|
|
395
536
|
}
|
|
396
537
|
|
|
@@ -406,33 +547,41 @@ fn native_jwt_sign(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArgume
|
|
|
406
547
|
}
|
|
407
548
|
}
|
|
408
549
|
|
|
409
|
-
fn native_jwt_verify(
|
|
550
|
+
fn native_jwt_verify(
|
|
551
|
+
scope: &mut v8::HandleScope,
|
|
552
|
+
args: v8::FunctionCallbackArguments,
|
|
553
|
+
mut retval: v8::ReturnValue,
|
|
554
|
+
) {
|
|
410
555
|
let token = v8_to_string(scope, args.get(0));
|
|
411
556
|
let secret = v8_to_string(scope, args.get(1));
|
|
412
|
-
|
|
557
|
+
|
|
413
558
|
let mut validation = Validation::default();
|
|
414
559
|
validation.validate_exp = true;
|
|
415
|
-
|
|
560
|
+
|
|
416
561
|
let data = decode::<Value>(
|
|
417
562
|
&token,
|
|
418
563
|
&DecodingKey::from_secret(secret.as_bytes()),
|
|
419
564
|
&validation,
|
|
420
565
|
);
|
|
421
|
-
|
|
566
|
+
|
|
422
567
|
match data {
|
|
423
568
|
Ok(d) => {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
569
|
+
// Convert claim back to V8 object via JSON
|
|
570
|
+
let json_str = serde_json::to_string(&d.claims).unwrap();
|
|
571
|
+
let v8_json_str = v8_str(scope, &json_str);
|
|
572
|
+
if let Some(val) = v8::json::parse(scope, v8_json_str) {
|
|
573
|
+
retval.set(val);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
431
576
|
Err(e) => throw(scope, &format!("Invalid or expired JWT: {}", e)),
|
|
432
577
|
}
|
|
433
578
|
}
|
|
434
579
|
|
|
435
|
-
fn native_password_hash(
|
|
580
|
+
fn native_password_hash(
|
|
581
|
+
scope: &mut v8::HandleScope,
|
|
582
|
+
args: v8::FunctionCallbackArguments,
|
|
583
|
+
mut retval: v8::ReturnValue,
|
|
584
|
+
) {
|
|
436
585
|
let pw = v8_to_string(scope, args.get(0));
|
|
437
586
|
match hash(pw, DEFAULT_COST) {
|
|
438
587
|
Ok(h) => retval.set(v8_str(scope, &h).into()),
|
|
@@ -440,15 +589,23 @@ fn native_password_hash(scope: &mut v8::HandleScope, args: v8::FunctionCallbackA
|
|
|
440
589
|
}
|
|
441
590
|
}
|
|
442
591
|
|
|
443
|
-
fn native_password_verify(
|
|
592
|
+
fn native_password_verify(
|
|
593
|
+
scope: &mut v8::HandleScope,
|
|
594
|
+
args: v8::FunctionCallbackArguments,
|
|
595
|
+
mut retval: v8::ReturnValue,
|
|
596
|
+
) {
|
|
444
597
|
let pw = v8_to_string(scope, args.get(0));
|
|
445
598
|
let hash_str = v8_to_string(scope, args.get(1));
|
|
446
|
-
|
|
599
|
+
|
|
447
600
|
let ok = verify(pw, &hash_str).unwrap_or(false);
|
|
448
601
|
retval.set(v8::Boolean::new(scope, ok).into());
|
|
449
602
|
}
|
|
450
603
|
|
|
451
|
-
fn native_define_action(
|
|
604
|
+
fn native_define_action(
|
|
605
|
+
_scope: &mut v8::HandleScope,
|
|
606
|
+
args: v8::FunctionCallbackArguments,
|
|
607
|
+
mut retval: v8::ReturnValue,
|
|
608
|
+
) {
|
|
452
609
|
retval.set(args.get(0));
|
|
453
610
|
}
|
|
454
611
|
|
|
@@ -458,13 +615,17 @@ fn native_define_action(_scope: &mut v8::HandleScope, args: v8::FunctionCallback
|
|
|
458
615
|
|
|
459
616
|
// generic wrappers could go here if needed
|
|
460
617
|
|
|
461
|
-
fn native_invoke_extension(
|
|
618
|
+
fn native_invoke_extension(
|
|
619
|
+
scope: &mut v8::HandleScope,
|
|
620
|
+
args: v8::FunctionCallbackArguments,
|
|
621
|
+
mut retval: v8::ReturnValue,
|
|
622
|
+
) {
|
|
462
623
|
let fn_idx = args.get(0).to_integer(scope).unwrap().value() as usize;
|
|
463
624
|
|
|
464
625
|
// Get pointer from registry
|
|
465
626
|
let mut ptr = 0;
|
|
466
627
|
let mut sig = Signature::Unknown;
|
|
467
|
-
|
|
628
|
+
|
|
468
629
|
if let Ok(guard) = REGISTRY.lock() {
|
|
469
630
|
if let Some(registry) = &*guard {
|
|
470
631
|
if let Some(entry) = registry.natives.get(fn_idx) {
|
|
@@ -473,33 +634,39 @@ fn native_invoke_extension(scope: &mut v8::HandleScope, args: v8::FunctionCallba
|
|
|
473
634
|
}
|
|
474
635
|
}
|
|
475
636
|
}
|
|
476
|
-
|
|
637
|
+
|
|
477
638
|
if ptr == 0 {
|
|
478
|
-
|
|
479
|
-
|
|
639
|
+
throw(scope, "Native function not found");
|
|
640
|
+
return;
|
|
480
641
|
}
|
|
481
642
|
|
|
482
643
|
match sig {
|
|
483
644
|
Signature::F64TwoArgsRetF64 => {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
645
|
+
let a = args
|
|
646
|
+
.get(1)
|
|
647
|
+
.to_number(scope)
|
|
648
|
+
.unwrap_or(v8::Number::new(scope, 0.0))
|
|
649
|
+
.value();
|
|
650
|
+
let b = args
|
|
651
|
+
.get(2)
|
|
652
|
+
.to_number(scope)
|
|
653
|
+
.unwrap_or(v8::Number::new(scope, 0.0))
|
|
654
|
+
.value();
|
|
655
|
+
|
|
656
|
+
unsafe {
|
|
657
|
+
let func: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(ptr);
|
|
658
|
+
let res = func(a, b);
|
|
659
|
+
retval.set(v8::Number::new(scope, res).into());
|
|
660
|
+
}
|
|
661
|
+
}
|
|
493
662
|
_ => throw(scope, "Unsupported signature"),
|
|
494
663
|
}
|
|
495
664
|
}
|
|
496
665
|
|
|
497
|
-
|
|
498
666
|
// ----------------------------------------------------------------------------
|
|
499
667
|
// INJECTOR
|
|
500
668
|
// ----------------------------------------------------------------------------
|
|
501
669
|
|
|
502
|
-
|
|
503
670
|
pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Object>) {
|
|
504
671
|
// Ensure globalThis reference
|
|
505
672
|
let gt_key = v8_str(scope, "globalThis");
|
|
@@ -508,13 +675,15 @@ pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Obje
|
|
|
508
675
|
let t_obj = v8::Object::new(scope);
|
|
509
676
|
let t_key = v8_str(scope, "t");
|
|
510
677
|
// Use create_data_property to guarantee definition
|
|
511
|
-
global
|
|
678
|
+
global
|
|
679
|
+
.create_data_property(scope, t_key.into(), t_obj.into())
|
|
680
|
+
.unwrap();
|
|
512
681
|
|
|
513
682
|
// defineAction (identity function for clean typing)
|
|
514
683
|
let def_fn = v8::Function::new(scope, native_define_action).unwrap();
|
|
515
684
|
let def_key = v8_str(scope, "defineAction");
|
|
516
685
|
global.set(scope, def_key.into(), def_fn.into());
|
|
517
|
-
|
|
686
|
+
|
|
518
687
|
// t.read
|
|
519
688
|
let read_fn = v8::Function::new(scope, native_read).unwrap();
|
|
520
689
|
let read_key = v8_str(scope, "read");
|
|
@@ -524,7 +693,7 @@ pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Obje
|
|
|
524
693
|
let log_fn = v8::Function::new(scope, native_log).unwrap();
|
|
525
694
|
let log_key = v8_str(scope, "log");
|
|
526
695
|
t_obj.set(scope, log_key.into(), log_fn.into());
|
|
527
|
-
|
|
696
|
+
|
|
528
697
|
// t.fetch
|
|
529
698
|
let fetch_fn = v8::Function::new(scope, native_fetch).unwrap();
|
|
530
699
|
let fetch_key = v8_str(scope, "fetch");
|
|
@@ -534,12 +703,12 @@ pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Obje
|
|
|
534
703
|
let jwt_obj = v8::Object::new(scope);
|
|
535
704
|
let sign_fn = v8::Function::new(scope, native_jwt_sign).unwrap();
|
|
536
705
|
let verify_fn = v8::Function::new(scope, native_jwt_verify).unwrap();
|
|
537
|
-
|
|
706
|
+
|
|
538
707
|
let sign_key = v8_str(scope, "sign");
|
|
539
708
|
jwt_obj.set(scope, sign_key.into(), sign_fn.into());
|
|
540
709
|
let verify_key = v8_str(scope, "verify");
|
|
541
710
|
jwt_obj.set(scope, verify_key.into(), verify_fn.into());
|
|
542
|
-
|
|
711
|
+
|
|
543
712
|
let jwt_key = v8_str(scope, "jwt");
|
|
544
713
|
t_obj.set(scope, jwt_key.into(), jwt_obj.into());
|
|
545
714
|
|
|
@@ -547,16 +716,15 @@ pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Obje
|
|
|
547
716
|
let pw_obj = v8::Object::new(scope);
|
|
548
717
|
let hash_fn = v8::Function::new(scope, native_password_hash).unwrap();
|
|
549
718
|
let pw_verify_fn = v8::Function::new(scope, native_password_verify).unwrap();
|
|
550
|
-
|
|
719
|
+
|
|
551
720
|
let hash_key = v8_str(scope, "hash");
|
|
552
721
|
pw_obj.set(scope, hash_key.into(), hash_fn.into());
|
|
553
722
|
let pw_verify_key = v8_str(scope, "verify");
|
|
554
723
|
pw_obj.set(scope, pw_verify_key.into(), pw_verify_fn.into());
|
|
555
|
-
|
|
724
|
+
|
|
556
725
|
let pw_key = v8_str(scope, "password");
|
|
557
726
|
t_obj.set(scope, pw_key.into(), pw_obj.into());
|
|
558
727
|
|
|
559
|
-
|
|
560
728
|
// Inject __titan_invoke_native
|
|
561
729
|
let invoke_fn = v8::Function::new(scope, native_invoke_extension).unwrap();
|
|
562
730
|
let invoke_key = v8_str(scope, "__titan_invoke_native");
|
|
@@ -574,60 +742,97 @@ pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Obje
|
|
|
574
742
|
};
|
|
575
743
|
|
|
576
744
|
for module in modules {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
745
|
+
// 1. Prepare Native Wrappers
|
|
746
|
+
let natives_obj = v8::Object::new(scope);
|
|
747
|
+
for (fn_name, &idx) in &module.native_indices {
|
|
748
|
+
let code = format!(
|
|
749
|
+
"(function(a, b) {{ return __titan_invoke_native({}, a, b); }})",
|
|
750
|
+
idx
|
|
751
|
+
);
|
|
752
|
+
let source = v8_str(scope, &code);
|
|
753
|
+
// Compile wrappers
|
|
754
|
+
if let Some(script) = v8::Script::compile(scope, source, None) {
|
|
755
|
+
if let Some(val) = script.run(scope) {
|
|
756
|
+
let key = v8_str(scope, fn_name);
|
|
757
|
+
natives_obj.set(scope, key.into(), val);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// 2. Prepare JS Wrapper (CommonJS shim)
|
|
763
|
+
// We pass 't' and 'native' (the object we just made) to the module.
|
|
764
|
+
let wrapper_src = format!(
|
|
765
|
+
r#"(function(t, native) {{
|
|
766
|
+
var module = {{ exports: {{}} }};
|
|
767
|
+
var exports = module.exports;
|
|
768
|
+
{}
|
|
769
|
+
return module.exports;
|
|
770
|
+
}})"#,
|
|
771
|
+
module.js
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
let source = v8_str(scope, &wrapper_src);
|
|
775
|
+
let tc = &mut v8::TryCatch::new(scope);
|
|
776
|
+
|
|
777
|
+
// 3. Compile and Run
|
|
778
|
+
if let Some(script) = v8::Script::compile(tc, source, None) {
|
|
779
|
+
if let Some(factory_val) = script.run(tc) {
|
|
780
|
+
if let Ok(factory) = v8::Local::<v8::Function>::try_from(factory_val) {
|
|
781
|
+
let recv = v8::undefined(&mut *tc).into();
|
|
782
|
+
// Pass t_obj and natives_obj
|
|
783
|
+
let args = [t_obj.into(), natives_obj.into()];
|
|
784
|
+
|
|
785
|
+
if let Some(exports_val) = factory.call(&mut *tc, recv, &args) {
|
|
786
|
+
// 4. Assign exports to t.<extName>
|
|
787
|
+
let mod_key = v8_str(&mut *tc, &module.name);
|
|
788
|
+
t_obj.set(&mut *tc, mod_key.into(), exports_val);
|
|
789
|
+
|
|
790
|
+
// println!(
|
|
791
|
+
// "{} {} {}",
|
|
792
|
+
// crate::utils::blue("[Titan]"),
|
|
793
|
+
// crate::utils::green("Injected extension:"),
|
|
794
|
+
// module.name
|
|
795
|
+
// );
|
|
796
|
+
} else {
|
|
797
|
+
// Execution error
|
|
798
|
+
if let Some(msg) = tc.message() {
|
|
799
|
+
let text = msg.get(&mut *tc).to_rust_string_lossy(&mut *tc);
|
|
800
|
+
println!(
|
|
801
|
+
"{} {} {} -> {}",
|
|
802
|
+
crate::utils::blue("[Titan]"),
|
|
803
|
+
crate::utils::red("Error running extension"),
|
|
804
|
+
module.name,
|
|
805
|
+
text
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
} else {
|
|
811
|
+
// Runtime error during script run
|
|
812
|
+
if let Some(msg) = tc.message() {
|
|
813
|
+
let text = msg.get(&mut *tc).to_rust_string_lossy(&mut *tc);
|
|
814
|
+
println!(
|
|
815
|
+
"{} {} {} -> {}",
|
|
816
|
+
crate::utils::blue("[Titan]"),
|
|
817
|
+
crate::utils::red("Error evaluating extension wrapper"),
|
|
818
|
+
module.name,
|
|
819
|
+
text
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
} else {
|
|
824
|
+
// Compile error
|
|
825
|
+
if let Some(msg) = tc.message() {
|
|
826
|
+
let text = msg.get(&mut *tc).to_rust_string_lossy(&mut *tc);
|
|
827
|
+
println!(
|
|
828
|
+
"{} {} {} -> {}",
|
|
829
|
+
crate::utils::blue("[Titan]"),
|
|
830
|
+
crate::utils::red("Syntax Error in extension"),
|
|
831
|
+
module.name,
|
|
832
|
+
text
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
631
836
|
}
|
|
632
837
|
|
|
633
838
|
// t.db (Stub for now)
|
|
@@ -1,28 +1,27 @@
|
|
|
1
|
-
use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
|
|
2
1
|
use anyhow::Result;
|
|
3
2
|
use axum::{
|
|
4
|
-
|
|
3
|
+
Router,
|
|
4
|
+
body::{Body, to_bytes},
|
|
5
5
|
extract::State,
|
|
6
6
|
http::{Request, StatusCode},
|
|
7
7
|
response::{IntoResponse, Json},
|
|
8
8
|
routing::any,
|
|
9
|
-
Router,
|
|
10
9
|
};
|
|
11
10
|
use serde_json::Value;
|
|
12
|
-
use tokio::net::TcpListener;
|
|
13
11
|
use std::time::Instant;
|
|
12
|
+
use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
|
|
13
|
+
use tokio::net::TcpListener;
|
|
14
14
|
|
|
15
15
|
mod utils;
|
|
16
16
|
|
|
17
|
-
mod extensions;
|
|
18
17
|
mod action_management;
|
|
18
|
+
mod extensions;
|
|
19
19
|
|
|
20
|
-
use utils::{blue, white, yellow, green, gray, red};
|
|
21
|
-
use extensions::{init_v8, inject_extensions};
|
|
22
20
|
use action_management::{
|
|
23
|
-
|
|
24
|
-
DynamicRoute, RouteVal
|
|
21
|
+
DynamicRoute, RouteVal, find_actions_dir, match_dynamic_route, resolve_actions_dir,
|
|
25
22
|
};
|
|
23
|
+
use extensions::{init_v8, inject_extensions};
|
|
24
|
+
use utils::{blue, gray, green, red, white, yellow};
|
|
26
25
|
|
|
27
26
|
#[derive(Clone)]
|
|
28
27
|
struct AppState {
|
|
@@ -45,7 +44,6 @@ async fn dynamic_handler_inner(
|
|
|
45
44
|
State(state): State<AppState>,
|
|
46
45
|
req: Request<Body>,
|
|
47
46
|
) -> impl IntoResponse {
|
|
48
|
-
|
|
49
47
|
// ---------------------------
|
|
50
48
|
// BASIC REQUEST INFO
|
|
51
49
|
// ---------------------------
|
|
@@ -70,10 +68,7 @@ async fn dynamic_handler_inner(
|
|
|
70
68
|
q.split('&')
|
|
71
69
|
.filter_map(|pair| {
|
|
72
70
|
let mut it = pair.splitn(2, '=');
|
|
73
|
-
Some((
|
|
74
|
-
it.next()?.to_string(),
|
|
75
|
-
it.next().unwrap_or("").to_string(),
|
|
76
|
-
))
|
|
71
|
+
Some((it.next()?.to_string(), it.next().unwrap_or("").to_string()))
|
|
77
72
|
})
|
|
78
73
|
.collect()
|
|
79
74
|
})
|
|
@@ -83,7 +78,7 @@ async fn dynamic_handler_inner(
|
|
|
83
78
|
// HEADERS & BODY
|
|
84
79
|
// ---------------------------
|
|
85
80
|
let (parts, body) = req.into_parts();
|
|
86
|
-
|
|
81
|
+
|
|
87
82
|
let headers = parts
|
|
88
83
|
.headers
|
|
89
84
|
.iter()
|
|
@@ -92,9 +87,7 @@ async fn dynamic_handler_inner(
|
|
|
92
87
|
|
|
93
88
|
let body_bytes = match to_bytes(body, usize::MAX).await {
|
|
94
89
|
Ok(b) => b,
|
|
95
|
-
Err(_) =>
|
|
96
|
-
return (StatusCode::BAD_REQUEST, "Failed to read request body").into_response()
|
|
97
|
-
}
|
|
90
|
+
Err(_) => return (StatusCode::BAD_REQUEST, "Failed to read request body").into_response(),
|
|
98
91
|
};
|
|
99
92
|
|
|
100
93
|
let body_str = String::from_utf8_lossy(&body_bytes).to_string();
|
|
@@ -119,18 +112,32 @@ async fn dynamic_handler_inner(
|
|
|
119
112
|
action_name = Some(name);
|
|
120
113
|
} else if route.r#type == "json" {
|
|
121
114
|
let elapsed = start.elapsed();
|
|
122
|
-
println!(
|
|
115
|
+
println!(
|
|
116
|
+
"{} {} {} {}",
|
|
117
|
+
blue("[Titan]"),
|
|
118
|
+
white(&format!("{} {}", method, path)),
|
|
119
|
+
white("→ json"),
|
|
120
|
+
gray(&format!("in {:.2?}", elapsed))
|
|
121
|
+
);
|
|
123
122
|
return Json(route.value.clone()).into_response();
|
|
124
123
|
} else if let Some(s) = route.value.as_str() {
|
|
125
124
|
let elapsed = start.elapsed();
|
|
126
|
-
println!(
|
|
125
|
+
println!(
|
|
126
|
+
"{} {} {} {}",
|
|
127
|
+
blue("[Titan]"),
|
|
128
|
+
white(&format!("{} {}", method, path)),
|
|
129
|
+
white("→ reply"),
|
|
130
|
+
gray(&format!("in {:.2?}", elapsed))
|
|
131
|
+
);
|
|
127
132
|
return s.to_string().into_response();
|
|
128
133
|
}
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
// Dynamic route
|
|
132
137
|
if action_name.is_none() {
|
|
133
|
-
if let Some((action, p)) =
|
|
138
|
+
if let Some((action, p)) =
|
|
139
|
+
match_dynamic_route(&method, &path, state.dynamic_routes.as_slice())
|
|
140
|
+
{
|
|
134
141
|
route_kind = "dynamic";
|
|
135
142
|
route_label = action.clone();
|
|
136
143
|
action_name = Some(action);
|
|
@@ -142,7 +149,13 @@ async fn dynamic_handler_inner(
|
|
|
142
149
|
Some(a) => a,
|
|
143
150
|
None => {
|
|
144
151
|
let elapsed = start.elapsed();
|
|
145
|
-
println!(
|
|
152
|
+
println!(
|
|
153
|
+
"{} {} {} {}",
|
|
154
|
+
blue("[Titan]"),
|
|
155
|
+
white(&format!("{} {}", method, path)),
|
|
156
|
+
white("→ 404"),
|
|
157
|
+
gray(&format!("in {:.2?}", elapsed))
|
|
158
|
+
);
|
|
146
159
|
return (StatusCode::NOT_FOUND, "Not Found").into_response();
|
|
147
160
|
}
|
|
148
161
|
};
|
|
@@ -151,7 +164,8 @@ async fn dynamic_handler_inner(
|
|
|
151
164
|
// LOAD ACTION
|
|
152
165
|
// ---------------------------
|
|
153
166
|
let resolved = resolve_actions_dir();
|
|
154
|
-
let actions_dir = resolved
|
|
167
|
+
let actions_dir = resolved
|
|
168
|
+
.exists()
|
|
155
169
|
.then(|| resolved)
|
|
156
170
|
.or_else(|| find_actions_dir(&state.project_root))
|
|
157
171
|
.unwrap();
|
|
@@ -164,17 +178,24 @@ async fn dynamic_handler_inner(
|
|
|
164
178
|
}
|
|
165
179
|
}
|
|
166
180
|
|
|
167
|
-
let js_code =
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
181
|
+
let js_code =
|
|
182
|
+
match fs::read_to_string(&action_path) {
|
|
183
|
+
Ok(c) => c,
|
|
184
|
+
Err(_) => return (
|
|
185
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
186
|
+
Json(
|
|
187
|
+
serde_json::json!({"error": "Action bundle not found", "action": action_name}),
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
.into_response(),
|
|
191
|
+
};
|
|
173
192
|
|
|
174
193
|
// ---------------------------
|
|
175
194
|
// EXECUTE IN V8
|
|
176
195
|
// ---------------------------
|
|
177
|
-
let env_json = std::env::vars()
|
|
196
|
+
let env_json = std::env::vars()
|
|
197
|
+
.map(|(k, v)| (k, Value::String(v)))
|
|
198
|
+
.collect::<serde_json::Map<_, _>>();
|
|
178
199
|
|
|
179
200
|
let injected = format!(
|
|
180
201
|
r#"
|
|
@@ -215,43 +236,42 @@ async fn dynamic_handler_inner(
|
|
|
215
236
|
// We can use `task::spawn_blocking`.
|
|
216
237
|
let root = state.project_root.clone();
|
|
217
238
|
let action_name_for_v8 = action_name.clone();
|
|
218
|
-
|
|
239
|
+
|
|
219
240
|
let result_json: Value = tokio::task::spawn_blocking(move || {
|
|
220
241
|
let isolate = &mut v8::Isolate::new(v8::CreateParams::default());
|
|
221
242
|
let handle_scope = &mut v8::HandleScope::new(isolate);
|
|
222
243
|
let context = v8::Context::new(handle_scope, v8::ContextOptions::default());
|
|
223
244
|
let scope = &mut v8::ContextScope::new(handle_scope, context);
|
|
224
|
-
|
|
245
|
+
|
|
225
246
|
let global = context.global(scope);
|
|
226
247
|
|
|
227
248
|
// Inject extensions (t.read, etc)
|
|
228
249
|
inject_extensions(scope, global);
|
|
229
|
-
|
|
230
|
-
// Set metadata globals
|
|
250
|
+
|
|
231
251
|
// Set metadata globals
|
|
232
252
|
let root_str = v8::String::new(scope, root.to_str().unwrap_or(".")).unwrap();
|
|
233
253
|
let root_key = v8::String::new(scope, "__titan_root").unwrap();
|
|
234
254
|
global.set(scope, root_key.into(), root_str.into());
|
|
235
|
-
|
|
255
|
+
|
|
236
256
|
let action_str = v8::String::new(scope, &action_name_for_v8).unwrap();
|
|
237
257
|
let action_key = v8::String::new(scope, "__titan_action").unwrap();
|
|
238
258
|
global.set(scope, action_key.into(), action_str.into());
|
|
239
|
-
|
|
259
|
+
|
|
240
260
|
let source = v8::String::new(scope, &injected).unwrap();
|
|
241
|
-
|
|
261
|
+
|
|
242
262
|
let try_catch = &mut v8::TryCatch::new(scope);
|
|
243
|
-
|
|
263
|
+
|
|
244
264
|
let script = match v8::Script::compile(try_catch, source, None) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
265
|
+
Some(s) => s,
|
|
266
|
+
None => {
|
|
267
|
+
let err = try_catch.message().unwrap();
|
|
268
|
+
let msg = err.get(try_catch).to_rust_string_lossy(try_catch);
|
|
269
|
+
return serde_json::json!({ "error": msg, "phase": "compile" });
|
|
270
|
+
}
|
|
251
271
|
};
|
|
252
|
-
|
|
272
|
+
|
|
253
273
|
let result = script.run(try_catch);
|
|
254
|
-
|
|
274
|
+
|
|
255
275
|
match result {
|
|
256
276
|
Some(val) => {
|
|
257
277
|
// Convert v8 Value to Serde JSON
|
|
@@ -259,37 +279,59 @@ async fn dynamic_handler_inner(
|
|
|
259
279
|
let json_obj = v8::json::stringify(try_catch, val).unwrap();
|
|
260
280
|
let json_str = json_obj.to_rust_string_lossy(try_catch);
|
|
261
281
|
serde_json::from_str(&json_str).unwrap_or(Value::Null)
|
|
262
|
-
}
|
|
282
|
+
}
|
|
263
283
|
None => {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
284
|
+
let err = try_catch.message().unwrap();
|
|
285
|
+
let msg = err.get(try_catch).to_rust_string_lossy(try_catch);
|
|
286
|
+
serde_json::json!({ "error": msg, "phase": "execution" })
|
|
267
287
|
}
|
|
268
288
|
}
|
|
269
|
-
})
|
|
289
|
+
})
|
|
290
|
+
.await
|
|
291
|
+
.unwrap_or(serde_json::json!({"error": "V8 task failed"}));
|
|
270
292
|
|
|
271
293
|
// ---------------------------
|
|
272
294
|
// FINAL LOG
|
|
273
295
|
// ---------------------------
|
|
274
296
|
let elapsed = start.elapsed();
|
|
275
|
-
|
|
297
|
+
|
|
276
298
|
// Check for errors in result
|
|
277
299
|
if let Some(err) = result_json.get("error") {
|
|
278
|
-
println!(
|
|
300
|
+
println!(
|
|
301
|
+
"{} {} {} {}",
|
|
302
|
+
blue("[Titan]"),
|
|
303
|
+
red(&format!("{} {}", method, path)),
|
|
304
|
+
red("→ error"),
|
|
305
|
+
gray(&format!("in {:.2?}", elapsed))
|
|
306
|
+
);
|
|
279
307
|
println!("{}", red(err.as_str().unwrap_or("Unknown")));
|
|
280
308
|
return (StatusCode::INTERNAL_SERVER_ERROR, Json(result_json)).into_response();
|
|
281
309
|
}
|
|
282
310
|
|
|
283
311
|
match route_kind {
|
|
284
|
-
"dynamic" => println!(
|
|
285
|
-
|
|
312
|
+
"dynamic" => println!(
|
|
313
|
+
"{} {} {} {} {} {}",
|
|
314
|
+
blue("[Titan]"),
|
|
315
|
+
green(&format!("{} {}", method, path)),
|
|
316
|
+
white("→"),
|
|
317
|
+
green(&route_label),
|
|
318
|
+
white("(dynamic)"),
|
|
319
|
+
gray(&format!("in {:.2?}", elapsed))
|
|
320
|
+
),
|
|
321
|
+
"exact" => println!(
|
|
322
|
+
"{} {} {} {} {}",
|
|
323
|
+
blue("[Titan]"),
|
|
324
|
+
white(&format!("{} {}", method, path)),
|
|
325
|
+
white("→"),
|
|
326
|
+
yellow(&route_label),
|
|
327
|
+
gray(&format!("in {:.2?}", elapsed))
|
|
328
|
+
),
|
|
286
329
|
_ => {}
|
|
287
330
|
}
|
|
288
331
|
|
|
289
332
|
Json(result_json).into_response()
|
|
290
333
|
}
|
|
291
334
|
|
|
292
|
-
|
|
293
335
|
// Entrypoint ---------------------------------------------------------------
|
|
294
336
|
|
|
295
337
|
#[tokio::main]
|
|
@@ -304,28 +346,22 @@ async fn main() -> Result<()> {
|
|
|
304
346
|
let port = json["__config"]["port"].as_u64().unwrap_or(3000);
|
|
305
347
|
let routes_json = json["routes"].clone();
|
|
306
348
|
let map: HashMap<String, RouteVal> = serde_json::from_value(routes_json).unwrap_or_default();
|
|
307
|
-
let dynamic_routes: Vec<DynamicRoute> =
|
|
349
|
+
let dynamic_routes: Vec<DynamicRoute> =
|
|
350
|
+
serde_json::from_value(json["__dynamic_routes"].clone()).unwrap_or_default();
|
|
308
351
|
|
|
309
|
-
//
|
|
310
|
-
let
|
|
311
|
-
// Ensure we find the 'app' directory as sibling if running from 'server'
|
|
312
|
-
let project_root = if cwd.join("../app").exists() {
|
|
313
|
-
cwd.join("../app")
|
|
314
|
-
} else {
|
|
315
|
-
cwd
|
|
316
|
-
};
|
|
352
|
+
// Identify project root (where .ext or node_modules lives)
|
|
353
|
+
let project_root = resolve_project_root();
|
|
317
354
|
|
|
318
355
|
let state = AppState {
|
|
319
356
|
routes: Arc::new(map),
|
|
320
357
|
dynamic_routes: Arc::new(dynamic_routes),
|
|
321
358
|
project_root: project_root.clone(),
|
|
322
359
|
};
|
|
323
|
-
|
|
360
|
+
|
|
324
361
|
// Load extensions
|
|
325
362
|
extensions::load_project_extensions(project_root.clone());
|
|
326
363
|
|
|
327
364
|
let app = Router::new()
|
|
328
|
-
|
|
329
365
|
.route("/", any(root_route))
|
|
330
366
|
.fallback(any(dynamic_route))
|
|
331
367
|
.with_state(state);
|
|
@@ -338,8 +374,38 @@ async fn main() -> Result<()> {
|
|
|
338
374
|
println!(" ██║ ██║ ██║ ██╔══██║██║╚██╗██║");
|
|
339
375
|
println!(" ██║ ██║ ██║ ██║ ██║██║ ╚████║");
|
|
340
376
|
println!(" ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝\x1b[0m\n");
|
|
341
|
-
println!(
|
|
377
|
+
println!(
|
|
378
|
+
"\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}",
|
|
379
|
+
port
|
|
380
|
+
);
|
|
342
381
|
|
|
343
382
|
axum::serve(listener, app).await?;
|
|
344
383
|
Ok(())
|
|
345
384
|
}
|
|
385
|
+
|
|
386
|
+
fn resolve_project_root() -> PathBuf {
|
|
387
|
+
// 1. Check CWD (preferred for local dev/tooling)
|
|
388
|
+
if let Ok(cwd) = std::env::current_dir() {
|
|
389
|
+
if cwd.join("node_modules").exists()
|
|
390
|
+
|| cwd.join("package.json").exists()
|
|
391
|
+
|| cwd.join(".ext").exists()
|
|
392
|
+
{
|
|
393
|
+
return cwd;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// 2. Check executable persistence (Docker / Production)
|
|
398
|
+
// Walk up from the executable to find .ext or node_modules
|
|
399
|
+
if let Ok(exe) = std::env::current_exe() {
|
|
400
|
+
let mut current = exe.parent();
|
|
401
|
+
while let Some(dir) = current {
|
|
402
|
+
if dir.join(".ext").exists() || dir.join("node_modules").exists() {
|
|
403
|
+
return dir.to_path_buf();
|
|
404
|
+
}
|
|
405
|
+
current = dir.parent();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 3. Fallback to CWD
|
|
410
|
+
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
|
411
|
+
}
|