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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "titanpl-sdk",
3
- "version": "0.1.4",
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",
@@ -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
- COPY --from=builder /app/db /app/assets
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 v8;
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 std::sync::Once;
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 serde_json::Value;
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::{Library};
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
- let mut node_modules = root.join("node_modules");
77
- if !node_modules.exists() {
78
- if let Some(parent) = root.parent() {
79
- let parent_modules = parent.join("node_modules");
80
- if parent_modules.exists() {
81
- node_modules = parent_modules;
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
- let mut mod_natives_map = HashMap::new();
101
-
102
- if let Some(native_conf) = config.native {
103
- let lib_path = dir.join(&native_conf.path);
104
- unsafe {
105
- match Library::new(&lib_path) {
106
- Ok(lib) => {
107
- for (fn_name, fn_conf) in native_conf.functions {
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
- let js_path = dir.join(&config.main);
134
- let js_content = fs::read_to_string(js_path).unwrap_or_default();
112
+ println!("{} Scanning extension directories:", blue("[Titan]"));
113
+ for d in &search_dirs {
114
+ println!(" • {}", d.display());
115
+ }
135
116
 
136
- modules.push(ModuleDef {
137
- name: config.name.clone(),
138
- js: js_content,
139
- native_indices: mod_natives_map,
140
- });
141
-
142
- println!("{} {} {}", blue("[Titan]"), green("Extension loaded:"), config.name);
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
- *REGISTRY.lock().unwrap() = Some(Registry { _libs: libs, modules, natives: all_natives });
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(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
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(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut _retval: v8::ReturnValue) {
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
- if let Some(json) = v8::json::stringify(scope, val) {
251
- parts.push(json.to_rust_string_lossy(scope));
252
- appended = true;
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!("\x1b[90mlog({})\x1b[0m\x1b[97m: {}\x1b[0m", action_name, parts.join(" ")))
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(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
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
- let json_obj = v8::json::stringify(scope, b_val).unwrap();
295
- body_str = Some(json_obj.to_rust_string_lossy(scope));
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().use_rustls_tls().tcp_nodelay(true).build().unwrap_or(Client::new());
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)) = (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(&v)) {
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(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
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).unwrap().to_rust_string_lossy(scope);
372
- let mut payload: serde_json::Map<String, Value> = serde_json::from_str(&json_str).unwrap_or_default();
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
- let seconds = if val.is_number() {
383
- Some(val.to_number(scope).unwrap().value() as u64)
384
- } else if val.is_string() {
385
- parse_expires_in(&v8_to_string(scope, val))
386
- } else {
387
- None
388
- };
389
-
390
- if let Some(sec) = seconds {
391
- let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
392
- payload.insert("exp".to_string(), Value::Number(serde_json::Number::from(now + sec)));
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(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
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
- // Convert claim back to V8 object via JSON
425
- let json_str = serde_json::to_string(&d.claims).unwrap();
426
- let v8_json_str = v8_str(scope, &json_str);
427
- if let Some(val) = v8::json::parse(scope, v8_json_str) {
428
- retval.set(val);
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(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
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(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
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(_scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
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(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
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
- throw(scope, "Native function not found");
479
- return;
639
+ throw(scope, "Native function not found");
640
+ return;
480
641
  }
481
642
 
482
643
  match sig {
483
644
  Signature::F64TwoArgsRetF64 => {
484
- let a = args.get(1).to_number(scope).unwrap_or(v8::Number::new(scope, 0.0)).value();
485
- let b = args.get(2).to_number(scope).unwrap_or(v8::Number::new(scope, 0.0)).value();
486
-
487
- unsafe {
488
- let func: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(ptr);
489
- let res = func(a, b);
490
- retval.set(v8::Number::new(scope, res).into());
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.create_data_property(scope, t_key.into(), t_obj.into()).unwrap();
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
- let mod_obj = v8::Object::new(scope);
578
-
579
- // Generate JS wrappers
580
- for (fn_name, &idx) in &module.native_indices {
581
- let code = format!("(function(a, b) {{ return __titan_invoke_native({}, a, b); }})", idx);
582
- let source = v8_str(scope, &code);
583
- if let Some(script) = v8::Script::compile(scope, source, None) {
584
- if let Some(val) = script.run(scope) {
585
- let key = v8_str(scope, fn_name);
586
- mod_obj.set(scope, key.into(), val);
587
- }
588
- }
589
- }
590
-
591
- // Inject t.<module_name>
592
- let mod_key = v8_str(scope, &module.name);
593
- t_obj.set(scope, mod_key.into(), mod_obj.into());
594
-
595
- // Set context for logging
596
- let action_key = v8_str(scope, "__titan_action");
597
- let action_val = v8_str(scope, &module.name);
598
- global.set(scope, action_key.into(), action_val.into());
599
-
600
- // Execute JS
601
- // Wrap in IIFE passing 't' to ensure visibility
602
- let wrapped_js = format!("(function(t) {{ {} }})", module.js);
603
- let source = v8_str(scope, &wrapped_js);
604
- let tc = &mut v8::TryCatch::new(scope);
605
-
606
- if let Some(script) = v8::Script::compile(tc, source, None) {
607
- if let Some(func_val) = script.run(tc) {
608
- // func_val is the function. Call it with [t_obj]
609
- if let Ok(func) = v8::Local::<v8::Function>::try_from(func_val) {
610
- let receiver = v8::undefined(&mut *tc).into();
611
- let args = [t_obj.into()];
612
- // Pass tc (which is a scope)
613
- if func.call(&mut *tc, receiver, &args).is_none() {
614
- println!("{} {}", crate::utils::blue("[Titan]"), crate::utils::red("Extension Execution Failed"));
615
- if let Some(msg) = tc.message() {
616
- let text = msg.get(&mut *tc).to_rust_string_lossy(&mut *tc);
617
- println!("{} {}", crate::utils::red("Error details:"), text);
618
- }
619
- }
620
- }
621
- } else {
622
- let msg = tc.message().unwrap();
623
- let text = msg.get(&mut *tc).to_rust_string_lossy(&mut *tc);
624
- println!("{} {} {}", crate::utils::blue("[Titan]"), crate::utils::red("Extension JS Error:"), text);
625
- }
626
- } else {
627
- let msg = tc.message().unwrap();
628
- let text = msg.get(&mut *tc).to_rust_string_lossy(&mut *tc);
629
- println!("{} {} {}", crate::utils::blue("[Titan]"), crate::utils::red("Extension Compile Error:"), text);
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
- body::{to_bytes, Body},
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
- resolve_actions_dir, find_actions_dir, match_dynamic_route,
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!("{} {} {} {}", blue("[Titan]"), white(&format!("{} {}", method, path)), white("→ json"), gray(&format!("in {:.2?}", elapsed)));
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!("{} {} {} {}", blue("[Titan]"), white(&format!("{} {}", method, path)), white("→ reply"), gray(&format!("in {:.2?}", elapsed)));
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)) = match_dynamic_route(&method, &path, state.dynamic_routes.as_slice()) {
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!("{} {} {} {}", blue("[Titan]"), white(&format!("{} {}", method, path)), white("→ 404"), gray(&format!("in {:.2?}", elapsed)));
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.exists()
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 = match fs::read_to_string(&action_path) {
168
- Ok(c) => c,
169
- Err(_) => {
170
- return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": "Action bundle not found", "action": action_name}))).into_response()
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().map(|(k, v)| (k, Value::String(v))).collect::<serde_json::Map<_, _>>();
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
- Some(s) => s,
246
- None => {
247
- let err = try_catch.message().unwrap();
248
- let msg = err.get(try_catch).to_rust_string_lossy(try_catch);
249
- return serde_json::json!({ "error": msg, "phase": "compile" });
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
- let err = try_catch.message().unwrap();
265
- let msg = err.get(try_catch).to_rust_string_lossy(try_catch);
266
- serde_json::json!({ "error": msg, "phase": "execution" })
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
- }).await.unwrap_or(serde_json::json!({"error": "V8 task failed"}));
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!("{} {} {} {}", blue("[Titan]"), red(&format!("{} {}", method, path)), red("→ error"), gray(&format!("in {:.2?}", elapsed)));
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!("{} {} {} {} {} {}", blue("[Titan]"), green(&format!("{} {}", method, path)), white("→"), green(&route_label), white("(dynamic)"), gray(&format!("in {:.2?}", elapsed))),
285
- "exact" => println!("{} {} {} {} {}", blue("[Titan]"), white(&format!("{} {}", method, path)), white("→"), yellow(&route_label), gray(&format!("in {:.2?}", elapsed))),
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> = serde_json::from_value(json["__dynamic_routes"].clone()).unwrap_or_default();
349
+ let dynamic_routes: Vec<DynamicRoute> =
350
+ serde_json::from_value(json["__dynamic_routes"].clone()).unwrap_or_default();
308
351
 
309
- // Project root heuristics
310
- let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
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!("\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{}", port);
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
+ }