@ezetgalaxy/titan 26.8.3 → 26.9.0

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.
Files changed (97) hide show
  1. package/README.md +63 -14
  2. package/index.js +62 -15
  3. package/package.json +1 -1
  4. package/templates/extension/README.md +104 -104
  5. package/templates/extension/index.js +27 -27
  6. package/templates/extension/jsconfig.json +12 -12
  7. package/templates/extension/native/Cargo.toml +9 -9
  8. package/templates/extension/native/src/lib.rs +5 -5
  9. package/templates/extension/package.json +20 -20
  10. package/templates/extension/titan.json +17 -17
  11. package/templates/js/Dockerfile +66 -66
  12. package/templates/js/_dockerignore +3 -3
  13. package/templates/js/_gitignore +1 -0
  14. package/templates/js/app/actions/hello.js +5 -5
  15. package/templates/js/app/titan.d.ts +87 -87
  16. package/templates/js/jsconfig.json +18 -18
  17. package/templates/js/server/src/action_management.rs +131 -131
  18. package/templates/js/server/src/errors.rs +10 -10
  19. package/templates/js/server/src/extensions.rs +989 -989
  20. package/templates/js/server/src/utils.rs +33 -33
  21. package/templates/js/titan/bundle.js +78 -78
  22. package/templates/js/titan/dev.js +9 -1
  23. package/templates/js/titan/titan.js +122 -122
  24. package/templates/rust/Dockerfile +66 -66
  25. package/templates/rust/_dockerignore +3 -3
  26. package/templates/rust/_gitignore +1 -0
  27. package/templates/rust/app/actions/hello.js +5 -5
  28. package/templates/rust/app/actions/rust_hello.rs +14 -14
  29. package/templates/rust/app/titan.d.ts +101 -101
  30. package/templates/rust/jsconfig.json +18 -18
  31. package/templates/rust/server/src/action_management.rs +131 -131
  32. package/templates/rust/server/src/errors.rs +10 -10
  33. package/templates/rust/server/src/extensions.rs +989 -989
  34. package/templates/rust/server/src/utils.rs +33 -33
  35. package/templates/rust/titan/dev.js +9 -1
  36. package/templates/rust-ts/Dockerfile +66 -0
  37. package/templates/rust-ts/_dockerignore +3 -0
  38. package/templates/rust-ts/_gitignore +38 -0
  39. package/templates/rust-ts/app/actions/hello.ts +5 -0
  40. package/templates/rust-ts/app/actions/rust_hello.rs +14 -0
  41. package/templates/rust-ts/app/app.ts +11 -0
  42. package/templates/rust-ts/app/titan.d.ts +101 -0
  43. package/templates/rust-ts/package.json +14 -0
  44. package/templates/rust-ts/server/Cargo.lock +2869 -0
  45. package/templates/rust-ts/server/Cargo.toml +39 -0
  46. package/templates/rust-ts/server/src/action_management.rs +131 -0
  47. package/templates/rust-ts/server/src/errors.rs +51 -0
  48. package/templates/rust-ts/server/src/extensions.rs +989 -0
  49. package/templates/rust-ts/server/src/main.rs +468 -0
  50. package/templates/rust-ts/server/src/utils.rs +33 -0
  51. package/templates/rust-ts/titan/bundle.js +157 -0
  52. package/templates/rust-ts/titan/dev.js +402 -0
  53. package/templates/rust-ts/titan/titan.js +122 -0
  54. package/templates/rust-ts/tsconfig.json +21 -0
  55. package/templates/ts/Dockerfile +66 -0
  56. package/templates/ts/_dockerignore +3 -0
  57. package/templates/ts/_gitignore +38 -0
  58. package/templates/ts/app/actions/hello.ts +9 -0
  59. package/templates/ts/app/app.ts +10 -0
  60. package/templates/ts/app/titan.d.ts +102 -0
  61. package/templates/ts/package.json +26 -0
  62. package/templates/ts/server/Cargo.lock +2869 -0
  63. package/templates/ts/server/Cargo.toml +27 -0
  64. package/templates/ts/server/src/action_management.rs +131 -0
  65. package/templates/ts/server/src/errors.rs +51 -0
  66. package/templates/ts/server/src/extensions.rs +989 -0
  67. package/templates/ts/server/src/main.rs +437 -0
  68. package/templates/ts/server/src/utils.rs +33 -0
  69. package/templates/ts/titan/bundle.js +78 -0
  70. package/templates/ts/titan/dev.js +402 -0
  71. package/templates/ts/titan/titan.js +122 -0
  72. package/templates/ts/tsconfig.json +16 -0
  73. package/titanpl-sdk/README.md +109 -109
  74. package/titanpl-sdk/bin/run.js +254 -254
  75. package/titanpl-sdk/index.d.ts +46 -46
  76. package/titanpl-sdk/index.js +5 -5
  77. package/titanpl-sdk/package.json +32 -32
  78. package/titanpl-sdk/templates/.dockerignore +3 -3
  79. package/titanpl-sdk/templates/Dockerfile +53 -53
  80. package/titanpl-sdk/templates/app/actions/hello.js +5 -5
  81. package/titanpl-sdk/templates/app/titan.d.ts +87 -87
  82. package/titanpl-sdk/templates/jsconfig.json +18 -18
  83. package/titanpl-sdk/templates/server/src/action_management.rs +131 -131
  84. package/titanpl-sdk/templates/server/src/errors.rs +10 -10
  85. package/titanpl-sdk/templates/server/src/extensions.rs +640 -640
  86. package/titanpl-sdk/templates/server/src/utils.rs +33 -33
  87. package/titanpl-sdk/templates/titan/bundle.js +65 -65
  88. package/titanpl-sdk/templates/titan/dev.js +113 -113
  89. package/titanpl-sdk/templates/titan/titan.js +98 -98
  90. package/templates/js/server/action_map.json +0 -3
  91. package/templates/js/server/actions/hello.jsbundle +0 -48
  92. package/templates/js/server/routes.json +0 -16
  93. package/templates/rust/server/action_map.json +0 -3
  94. package/templates/rust/server/actions/hello.jsbundle +0 -47
  95. package/templates/rust/server/routes.json +0 -22
  96. package/templates/rust/server/src/actions_rust/mod.rs +0 -19
  97. package/templates/rust/server/src/actions_rust/rust_hello.rs +0 -14
@@ -1,989 +1,989 @@
1
- #![allow(unused)]
2
- use bcrypt::{DEFAULT_COST, hash, verify};
3
- use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
4
- use reqwest::{
5
- blocking::Client,
6
- Method,
7
- header::{HeaderMap, HeaderName, HeaderValue},
8
- };
9
- use serde_json::Value;
10
- use std::path::PathBuf;
11
- use std::sync::Once;
12
- use std::time::{SystemTime, UNIX_EPOCH};
13
- use v8;
14
-
15
- use crate::utils::{blue, gray, green, parse_expires_in};
16
- use libloading::Library;
17
- use std::collections::HashMap;
18
- use std::fs;
19
- use std::sync::Mutex;
20
- use walkdir::WalkDir;
21
-
22
-
23
- // ----------------------------------------------------------------------------
24
- // RUST ACTION API
25
- // ----------------------------------------------------------------------------
26
-
27
- pub struct T {
28
- pub jwt: Jwt,
29
- pub password: Password,
30
- }
31
-
32
- #[allow(non_upper_case_globals)]
33
- pub static t: T = T {
34
- jwt: Jwt,
35
- password: Password,
36
- };
37
-
38
- pub struct Jwt;
39
- impl Jwt {
40
- pub fn sign(&self, payload: Value, secret: &str, options: Option<Value>) -> anyhow::Result<String> {
41
- let mut final_payload = match payload {
42
- Value::Object(map) => map,
43
- _ => serde_json::Map::new(), // Should probably error or handle string payload like JS
44
- };
45
-
46
- if let Some(opts) = options {
47
- if let Some(exp_val) = opts.get("expiresIn") {
48
- // Handle both number (seconds) and string ("1h")
49
- let seconds = if let Some(n) = exp_val.as_u64() {
50
- Some(n)
51
- } else if let Some(s) = exp_val.as_str() {
52
- parse_expires_in(s)
53
- } else {
54
- None
55
- };
56
-
57
- if let Some(sec) = seconds {
58
- let now = SystemTime::now()
59
- .duration_since(UNIX_EPOCH)
60
- .unwrap()
61
- .as_secs();
62
- final_payload.insert("exp".to_string(), Value::Number(serde_json::Number::from(now + sec)));
63
- }
64
- }
65
- }
66
-
67
- let token = encode(
68
- &Header::default(),
69
- &Value::Object(final_payload),
70
- &EncodingKey::from_secret(secret.as_bytes()),
71
- )?;
72
- Ok(token)
73
- }
74
-
75
- pub fn verify(&self, token: &str, secret: &str) -> anyhow::Result<Value> {
76
- let mut validation = Validation::default();
77
- validation.validate_exp = true;
78
-
79
- let data = decode::<Value>(
80
- token,
81
- &DecodingKey::from_secret(secret.as_bytes()),
82
- &validation,
83
- )?;
84
- Ok(data.claims)
85
- }
86
- }
87
-
88
- pub struct Password;
89
- impl Password {
90
- pub fn hash(&self, password: &str) -> anyhow::Result<String> {
91
- let h = hash(password, DEFAULT_COST)?;
92
- Ok(h)
93
- }
94
-
95
- pub fn verify(&self, password: &str, hash_str: &str) -> bool {
96
- verify(password, hash_str).unwrap_or(false)
97
- }
98
- }
99
-
100
- impl T {
101
- pub fn log(&self, msg: impl std::fmt::Display) {
102
- println!(
103
- "{} {}",
104
- blue("[Titan]"),
105
- gray(&format!("\x1b[90mlog(rust)\x1b[0m\x1b[97m: {}\x1b[0m", msg))
106
- );
107
- }
108
-
109
- pub fn read(&self, path: &str) -> anyhow::Result<String> {
110
- let root = std::env::current_dir()?;
111
- let target = root.join(path);
112
- let target = target.canonicalize()?;
113
- Ok(fs::read_to_string(target)?)
114
- }
115
-
116
- pub async fn fetch(&self, url: &str, options: Option<FetchOptions>) -> anyhow::Result<FetchResponse> {
117
- let client = reqwest::Client::new();
118
- let opts = options.unwrap_or_default();
119
-
120
- let mut req = client.request(opts.method.parse().unwrap_or(Method::GET), url);
121
-
122
- if let Some(headers) = opts.headers {
123
- let mut map = HeaderMap::new();
124
- for (k, v) in headers {
125
- if let (Ok(name), Ok(val)) = (
126
- HeaderName::from_bytes(k.as_bytes()),
127
- HeaderValue::from_str(&v),
128
- ) {
129
- map.insert(name, val);
130
- }
131
- }
132
- req = req.headers(map);
133
- }
134
-
135
- if let Some(body) = opts.body {
136
- req = req.body(body);
137
- }
138
-
139
- let res = req.send().await?;
140
- let status = res.status().as_u16();
141
- let text = res.text().await?;
142
-
143
- Ok(FetchResponse {
144
- status,
145
- body: text,
146
- ok: status >= 200 && status < 300
147
- })
148
- }
149
- }
150
-
151
- #[derive(Default)]
152
- pub struct FetchOptions {
153
- pub method: String,
154
- pub headers: Option<std::collections::HashMap<String, String>>,
155
- pub body: Option<String>,
156
- }
157
-
158
- pub struct FetchResponse {
159
- pub status: u16,
160
- pub body: String,
161
- pub ok: bool,
162
- }
163
-
164
- // ----------------------------------------------------------------------------
165
- // GLOBAL REGISTRY
166
- // ----------------------------------------------------------------------------
167
-
168
- static REGISTRY: Mutex<Option<Registry>> = Mutex::new(None);
169
- #[allow(dead_code)]
170
- struct Registry {
171
- _libs: Vec<Library>,
172
- modules: Vec<ModuleDef>,
173
- natives: Vec<NativeFnEntry>, // Flattened list of all native functions
174
- }
175
-
176
- #[derive(Clone)]
177
- struct ModuleDef {
178
- name: String,
179
- js: String,
180
- native_indices: HashMap<String, usize>, // Function Name -> Index in REGISTRY.natives
181
- }
182
-
183
- struct NativeFnEntry {
184
- ptr: usize,
185
- sig: Signature,
186
- }
187
-
188
- #[derive(Clone, Copy)]
189
- enum Signature {
190
- F64TwoArgsRetF64,
191
- Unknown,
192
- }
193
-
194
- #[derive(serde::Deserialize)]
195
- struct TitanConfig {
196
- name: String,
197
- main: String,
198
- native: Option<TitanNativeConfig>,
199
- }
200
- #[derive(serde::Deserialize)]
201
- struct TitanNativeConfig {
202
- path: String,
203
- functions: HashMap<String, TitanNativeFunc>,
204
- }
205
- #[derive(serde::Deserialize)]
206
- struct TitanNativeFunc {
207
- symbol: String,
208
- #[serde(default)]
209
- parameters: Vec<String>,
210
- #[serde(default)]
211
- result: String,
212
- }
213
-
214
- pub fn load_project_extensions(root: PathBuf) {
215
- let mut modules = Vec::new();
216
- let mut libs = Vec::new();
217
- let mut all_natives = Vec::new();
218
-
219
- // =====================================================
220
- // 1. Resolve all extension search directories
221
- // =====================================================
222
-
223
- let mut search_dirs = Vec::new();
224
-
225
- let ext_dir = root.join(".ext"); // Production
226
- let nm_root = root.join("node_modules"); // Dev
227
- let nm_parent = root.parent().map(|p| p.join("node_modules")); // Monorepo
228
-
229
- // 1) Production
230
- if ext_dir.exists() {
231
- search_dirs.push(ext_dir);
232
- }
233
-
234
- // 2) Dev: project node_modules
235
- if nm_root.exists() {
236
- search_dirs.push(nm_root.clone());
237
- }
238
-
239
- // 3) Dev monorepo: parent/node_modules
240
- if let Some(nmp) = &nm_parent {
241
- if nmp.exists() {
242
- search_dirs.push(nmp.clone());
243
- }
244
- }
245
-
246
- // 4) Never return empty — add root/node_modules even if missing
247
- if search_dirs.is_empty() {
248
- search_dirs.push(nm_root);
249
- }
250
-
251
- // Normalize and dedupe
252
- search_dirs.sort();
253
- search_dirs.dedup();
254
-
255
- // println!("{} Scanning extension directories:", blue("[Titan]"));
256
- for d in &search_dirs {
257
-
258
- // let label = if d.to_string_lossy().contains(".ext") {
259
- // crate::utils::green("(Production)")
260
- // } else {
261
- // crate::utils::yellow("(Development)")
262
- // };
263
- // println!(" • {} {}", d.display(), label);
264
-
265
- }
266
-
267
- // =====================================================
268
- // 2. Walk and locate titan.json inside search paths
269
- // =====================================================
270
- for dir in &search_dirs {
271
- if !dir.exists() {
272
- println!(" {} Skipping non-existent directory: {}", crate::utils::yellow("⚠"), dir.display());
273
- continue;
274
- }
275
-
276
- for entry in WalkDir::new(&dir)
277
- .min_depth(1)
278
- .max_depth(5) // Increased depth
279
- .follow_links(true)
280
- {
281
- let entry = match entry {
282
- Ok(e) => e,
283
- Err(_) => continue,
284
- };
285
-
286
- // Only accept titan.json files
287
- if entry.file_type().is_file() && entry.file_name() == "titan.json" {
288
- let path = entry.path();
289
- // Load config file
290
- let config_content = match fs::read_to_string(path) {
291
- Ok(c) => c,
292
- Err(e) => {
293
- println!("{} Failed to read {}: {}", crate::utils::red("[Titan]"), path.display(), e);
294
- continue;
295
- }
296
- };
297
-
298
- let config: TitanConfig = match serde_json::from_str(&config_content) {
299
- Ok(c) => c,
300
- Err(e) => {
301
- println!("{} Failed to parse {}: {}", crate::utils::red("[Titan]"), path.display(), e);
302
- continue;
303
- }
304
- };
305
-
306
- let pkg_dir = path.parent().unwrap();
307
- let mut mod_natives_map = HashMap::new();
308
-
309
- // =====================================================
310
- // 3. Load native extension (optional)
311
- // =====================================================
312
- if let Some(native_conf) = config.native {
313
- let lib_path = pkg_dir.join(&native_conf.path);
314
-
315
- unsafe {
316
- match Library::new(&lib_path) {
317
- Ok(lib) => {
318
- for (fn_name, fn_conf) in native_conf.functions {
319
- let sig = if fn_conf.parameters == ["f64", "f64"]
320
- && fn_conf.result == "f64"
321
- {
322
- Signature::F64TwoArgsRetF64
323
- } else {
324
- Signature::Unknown
325
- };
326
-
327
- if let Ok(symbol) = lib.get::<*const ()>(fn_conf.symbol.as_bytes())
328
- {
329
- let idx = all_natives.len();
330
- all_natives.push(NativeFnEntry {
331
- ptr: *symbol as usize,
332
- sig,
333
- });
334
- mod_natives_map.insert(fn_name, idx);
335
- }
336
- }
337
- libs.push(lib);
338
- }
339
- Err(e) => println!(
340
- "{} Failed to load native library {} ({})",
341
- blue("[Titan]"),
342
- lib_path.display(),
343
- e
344
- ),
345
- }
346
- }
347
- }
348
-
349
- // =====================================================
350
- // 4. Load JS module file
351
- // =====================================================
352
- let js_path = pkg_dir.join(&config.main);
353
- let js_content = match fs::read_to_string(&js_path) {
354
- Ok(c) => c,
355
- Err(e) => {
356
- println!("{} Failed to read JS main {} for extension {}: {}",
357
- crate::utils::red("[Titan]"),
358
- js_path.display(),
359
- config.name,
360
- e
361
- );
362
- continue;
363
- }
364
- };
365
-
366
- modules.push(ModuleDef {
367
- name: config.name.clone(),
368
- js: js_content,
369
- native_indices: mod_natives_map,
370
- });
371
-
372
- let source_label = if dir.to_string_lossy().contains(".ext") {
373
- "Production"
374
- } else {
375
- "Development"
376
- };
377
-
378
- println!(
379
- "{} {} {} ({})",
380
- blue("[Titan]"),
381
- green("Extension loaded:"),
382
- config.name,
383
- source_label
384
- );
385
- }
386
- }
387
- }
388
-
389
- // =====================================================
390
- // 5. Store registry globally
391
- // =====================================================
392
- if modules.is_empty() {
393
- // println!("{} {}", blue("[Titan]"), crate::utils::yellow("No extensions loaded."));
394
- }
395
-
396
- *REGISTRY.lock().unwrap() = Some(Registry {
397
- _libs: libs,
398
- modules,
399
- natives: all_natives,
400
- });
401
- }
402
-
403
- static V8_INIT: Once = Once::new();
404
-
405
- pub fn init_v8() {
406
- V8_INIT.call_once(|| {
407
- let platform = v8::new_default_platform(0, false).make_shared();
408
- v8::V8::initialize_platform(platform);
409
- v8::V8::initialize();
410
- });
411
- }
412
-
413
- fn v8_str<'s>(scope: &mut v8::HandleScope<'s>, s: &str) -> v8::Local<'s, v8::String> {
414
- v8::String::new(scope, s).unwrap()
415
- }
416
-
417
- fn v8_to_string(scope: &mut v8::HandleScope, value: v8::Local<v8::Value>) -> String {
418
- value.to_string(scope).unwrap().to_rust_string_lossy(scope)
419
- }
420
-
421
- fn throw(scope: &mut v8::HandleScope, msg: &str) {
422
- let message = v8_str(scope, msg);
423
- let exception = v8::Exception::error(scope, message);
424
- scope.throw_exception(exception);
425
- }
426
-
427
- // ----------------------------------------------------------------------------
428
- // NATIVE CALLBACKS
429
- // ----------------------------------------------------------------------------
430
-
431
- fn native_read(
432
- scope: &mut v8::HandleScope,
433
- args: v8::FunctionCallbackArguments,
434
- mut retval: v8::ReturnValue,
435
- ) {
436
- let path_val = args.get(0);
437
- // 1. Read argument
438
- if !path_val.is_string() {
439
- throw(scope, "t.read(path): path is required");
440
- return;
441
- }
442
- let path_str = v8_to_string(scope, path_val);
443
-
444
- // 2. Check if absolute
445
- if std::path::Path::new(&path_str).is_absolute() {
446
- throw(scope, "t.read expects a relative path like 'db/file.sql'");
447
- return;
448
- }
449
-
450
- let context = scope.get_current_context();
451
- let global = context.global(scope);
452
- let root_key = v8_str(scope, "__titan_root");
453
- let root_val = global.get(scope, root_key.into()).unwrap();
454
-
455
- let root_str = if root_val.is_string() {
456
- v8_to_string(scope, root_val)
457
- } else {
458
- throw(scope, "Internal Error: __titan_root not set");
459
- return;
460
- };
461
-
462
- let root_path = PathBuf::from(root_str);
463
- let root_path = root_path.canonicalize().unwrap_or(root_path);
464
- let joined = root_path.join(&path_str);
465
-
466
- // 3. Canonicalize (resolves ../)
467
- let target = match joined.canonicalize() {
468
- Ok(target) => target,
469
- Err(_) => {
470
- throw(scope, &format!("t.read: file not found: {}", path_str));
471
- return;
472
- }
473
- };
474
-
475
- // 4. Enforce root boundary
476
- if !target.starts_with(&root_path) {
477
- throw(scope, "t.read: path escapes allowed root");
478
- return;
479
- }
480
-
481
- // 5. Read file
482
- match std::fs::read_to_string(&target) {
483
- Ok(content) => {
484
- retval.set(v8_str(scope, &content).into());
485
- }
486
- Err(e) => {
487
- throw(scope, &format!("t.read failed: {}", e));
488
- }
489
- }
490
- }
491
-
492
- fn native_log(
493
- scope: &mut v8::HandleScope,
494
- args: v8::FunctionCallbackArguments,
495
- mut _retval: v8::ReturnValue,
496
- ) {
497
- let context = scope.get_current_context();
498
- let global = context.global(scope);
499
- let action_key = v8_str(scope, "__titan_action");
500
- let action_val = global.get(scope, action_key.into()).unwrap();
501
- let action_name = v8_to_string(scope, action_val);
502
-
503
- let mut parts = Vec::new();
504
- for i in 0..args.length() {
505
- let val = args.get(i);
506
- let mut appended = false;
507
-
508
- // Try to JSON stringify objects so they are readable in logs
509
- if val.is_object() && !val.is_function() {
510
- if let Some(json) = v8::json::stringify(scope, val) {
511
- parts.push(json.to_rust_string_lossy(scope));
512
- appended = true;
513
- }
514
- }
515
-
516
- if !appended {
517
- parts.push(v8_to_string(scope, val));
518
- }
519
- }
520
-
521
- println!(
522
- "{} {}",
523
- blue("[Titan]"),
524
- gray(&format!(
525
- "\x1b[90mlog({})\x1b[0m\x1b[97m: {}\x1b[0m",
526
- action_name,
527
- parts.join(" ")
528
- ))
529
- );
530
- }
531
-
532
- fn native_fetch(
533
- scope: &mut v8::HandleScope,
534
- args: v8::FunctionCallbackArguments,
535
- mut retval: v8::ReturnValue,
536
- ) {
537
- let url = v8_to_string(scope, args.get(0));
538
-
539
- // Check for options (method, headers, body)
540
- let mut method = "GET".to_string();
541
- let mut body_str = None;
542
- let mut headers_vec = Vec::new();
543
-
544
- let opts_val = args.get(1);
545
- if opts_val.is_object() {
546
- let opts_obj = opts_val.to_object(scope).unwrap();
547
-
548
- // method
549
- let m_key = v8_str(scope, "method");
550
- if let Some(m_val) = opts_obj.get(scope, m_key.into()) {
551
- if m_val.is_string() {
552
- method = v8_to_string(scope, m_val);
553
- }
554
- }
555
-
556
- // body
557
- let b_key = v8_str(scope, "body");
558
- if let Some(b_val) = opts_obj.get(scope, b_key.into()) {
559
- if b_val.is_string() {
560
- body_str = Some(v8_to_string(scope, b_val));
561
- } else if b_val.is_object() {
562
- let json_obj = v8::json::stringify(scope, b_val).unwrap();
563
- body_str = Some(json_obj.to_rust_string_lossy(scope));
564
- }
565
- }
566
-
567
- // headers
568
- let h_key = v8_str(scope, "headers");
569
- if let Some(h_val) = opts_obj.get(scope, h_key.into()) {
570
- if h_val.is_object() {
571
- let h_obj = h_val.to_object(scope).unwrap();
572
- if let Some(keys) = h_obj.get_own_property_names(scope, Default::default()) {
573
- for i in 0..keys.length() {
574
- let key = keys.get_index(scope, i).unwrap();
575
- let val = h_obj.get(scope, key).unwrap();
576
- headers_vec.push((v8_to_string(scope, key), v8_to_string(scope, val)));
577
- }
578
- }
579
- }
580
- }
581
- }
582
-
583
- let client = Client::builder()
584
- .use_rustls_tls()
585
- .tcp_nodelay(true)
586
- .build()
587
- .unwrap_or(Client::new());
588
-
589
- let mut req = client.request(method.parse().unwrap_or(reqwest::Method::GET), &url);
590
-
591
- for (k, v) in headers_vec {
592
- if let (Ok(name), Ok(val)) = (
593
- HeaderName::from_bytes(k.as_bytes()),
594
- HeaderValue::from_str(&v),
595
- ) {
596
- let mut map = HeaderMap::new();
597
- map.insert(name, val);
598
- req = req.headers(map);
599
- }
600
- }
601
-
602
- if let Some(b) = body_str {
603
- req = req.body(b);
604
- }
605
-
606
- let res = req.send();
607
-
608
- let obj = v8::Object::new(scope);
609
- match res {
610
- Ok(r) => {
611
- let status = r.status().as_u16();
612
- let text = r.text().unwrap_or_default();
613
-
614
- let status_key = v8_str(scope, "status");
615
- let status_val = v8::Number::new(scope, status as f64);
616
- obj.set(scope, status_key.into(), status_val.into());
617
-
618
- let body_key = v8_str(scope, "body");
619
- let body_val = v8_str(scope, &text);
620
- obj.set(scope, body_key.into(), body_val.into());
621
-
622
- let ok_key = v8_str(scope, "ok");
623
- let ok_val = v8::Boolean::new(scope, true);
624
- obj.set(scope, ok_key.into(), ok_val.into());
625
- }
626
- Err(e) => {
627
- let ok_key = v8_str(scope, "ok");
628
- let ok_val = v8::Boolean::new(scope, false);
629
- obj.set(scope, ok_key.into(), ok_val.into());
630
-
631
- let err_key = v8_str(scope, "error");
632
- let err_val = v8_str(scope, &e.to_string());
633
- obj.set(scope, err_key.into(), err_val.into());
634
- }
635
- }
636
- retval.set(obj.into());
637
- }
638
-
639
- fn native_jwt_sign(
640
- scope: &mut v8::HandleScope,
641
- args: v8::FunctionCallbackArguments,
642
- mut retval: v8::ReturnValue,
643
- ) {
644
- // payload, secret, options
645
- let payload_val = args.get(0);
646
- // Parse payload to serde_json::Map
647
- let json_str = v8::json::stringify(scope, payload_val)
648
- .unwrap()
649
- .to_rust_string_lossy(scope);
650
- let mut payload: serde_json::Map<String, Value> =
651
- serde_json::from_str(&json_str).unwrap_or_default();
652
-
653
- let secret = v8_to_string(scope, args.get(1));
654
-
655
- let opts_val = args.get(2);
656
- if opts_val.is_object() {
657
- let opts_obj = opts_val.to_object(scope).unwrap();
658
- let exp_key = v8_str(scope, "expiresIn");
659
-
660
- if let Some(val) = opts_obj.get(scope, exp_key.into()) {
661
- let seconds = if val.is_number() {
662
- Some(val.to_number(scope).unwrap().value() as u64)
663
- } else if val.is_string() {
664
- parse_expires_in(&v8_to_string(scope, val))
665
- } else {
666
- None
667
- };
668
-
669
- if let Some(sec) = seconds {
670
- let now = SystemTime::now()
671
- .duration_since(UNIX_EPOCH)
672
- .unwrap()
673
- .as_secs();
674
- payload.insert(
675
- "exp".to_string(),
676
- Value::Number(serde_json::Number::from(now + sec)),
677
- );
678
- }
679
- }
680
- }
681
-
682
- let token = encode(
683
- &Header::default(),
684
- &Value::Object(payload),
685
- &EncodingKey::from_secret(secret.as_bytes()),
686
- );
687
-
688
- match token {
689
- Ok(tok) => retval.set(v8_str(scope, &tok).into()),
690
- Err(e) => throw(scope, &e.to_string()),
691
- }
692
- }
693
-
694
- fn native_jwt_verify(
695
- scope: &mut v8::HandleScope,
696
- args: v8::FunctionCallbackArguments,
697
- mut retval: v8::ReturnValue,
698
- ) {
699
- let token = v8_to_string(scope, args.get(0));
700
- let secret = v8_to_string(scope, args.get(1));
701
-
702
- let mut validation = Validation::default();
703
- validation.validate_exp = true;
704
-
705
- let data = decode::<Value>(
706
- &token,
707
- &DecodingKey::from_secret(secret.as_bytes()),
708
- &validation,
709
- );
710
-
711
- match data {
712
- Ok(d) => {
713
- // Convert claim back to V8 object via JSON
714
- let json_str = serde_json::to_string(&d.claims).unwrap();
715
- let v8_json_str = v8_str(scope, &json_str);
716
- if let Some(val) = v8::json::parse(scope, v8_json_str) {
717
- retval.set(val);
718
- }
719
- }
720
- Err(e) => throw(scope, &format!("Invalid or expired JWT: {}", e)),
721
- }
722
- }
723
-
724
- fn native_password_hash(
725
- scope: &mut v8::HandleScope,
726
- args: v8::FunctionCallbackArguments,
727
- mut retval: v8::ReturnValue,
728
- ) {
729
- let pw = v8_to_string(scope, args.get(0));
730
- match hash(pw, DEFAULT_COST) {
731
- Ok(h) => retval.set(v8_str(scope, &h).into()),
732
- Err(e) => throw(scope, &e.to_string()),
733
- }
734
- }
735
-
736
- fn native_password_verify(
737
- scope: &mut v8::HandleScope,
738
- args: v8::FunctionCallbackArguments,
739
- mut retval: v8::ReturnValue,
740
- ) {
741
- let pw = v8_to_string(scope, args.get(0));
742
- let hash_str = v8_to_string(scope, args.get(1));
743
-
744
- let ok = verify(pw, &hash_str).unwrap_or(false);
745
- retval.set(v8::Boolean::new(scope, ok).into());
746
- }
747
-
748
- fn native_define_action(
749
- _scope: &mut v8::HandleScope,
750
- args: v8::FunctionCallbackArguments,
751
- mut retval: v8::ReturnValue,
752
- ) {
753
- retval.set(args.get(0));
754
- }
755
-
756
- // ----------------------------------------------------------------------------
757
- // NATIVE CALLBACKS (EXTENSIONS)
758
- // ----------------------------------------------------------------------------
759
-
760
- // generic wrappers could go here if needed
761
-
762
- fn native_invoke_extension(
763
- scope: &mut v8::HandleScope,
764
- args: v8::FunctionCallbackArguments,
765
- mut retval: v8::ReturnValue,
766
- ) {
767
- let fn_idx = args.get(0).to_integer(scope).unwrap().value() as usize;
768
-
769
- // Get pointer from registry
770
- let mut ptr = 0;
771
- let mut sig = Signature::Unknown;
772
-
773
- if let Ok(guard) = REGISTRY.lock() {
774
- if let Some(registry) = &*guard {
775
- if let Some(entry) = registry.natives.get(fn_idx) {
776
- ptr = entry.ptr;
777
- sig = entry.sig;
778
- }
779
- }
780
- }
781
-
782
- if ptr == 0 {
783
- throw(scope, "Native function not found");
784
- return;
785
- }
786
-
787
- match sig {
788
- Signature::F64TwoArgsRetF64 => {
789
- let a = args
790
- .get(1)
791
- .to_number(scope)
792
- .unwrap_or(v8::Number::new(scope, 0.0))
793
- .value();
794
- let b = args
795
- .get(2)
796
- .to_number(scope)
797
- .unwrap_or(v8::Number::new(scope, 0.0))
798
- .value();
799
-
800
- unsafe {
801
- let func: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(ptr);
802
- let res = func(a, b);
803
- retval.set(v8::Number::new(scope, res).into());
804
- }
805
- }
806
- _ => throw(scope, "Unsupported signature"),
807
- }
808
- }
809
-
810
- // ----------------------------------------------------------------------------
811
- // INJECTOR
812
- // ----------------------------------------------------------------------------
813
-
814
- pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Object>) {
815
- // Ensure globalThis reference
816
- let gt_key = v8_str(scope, "globalThis");
817
- global.set(scope, gt_key.into(), global.into());
818
-
819
- let t_obj = v8::Object::new(scope);
820
- let t_key = v8_str(scope, "t");
821
- // Use create_data_property to guarantee definition
822
- global
823
- .create_data_property(scope, t_key.into(), t_obj.into())
824
- .unwrap();
825
-
826
- // defineAction (identity function for clean typing)
827
- let def_fn = v8::Function::new(scope, native_define_action).unwrap();
828
- let def_key = v8_str(scope, "defineAction");
829
- global.set(scope, def_key.into(), def_fn.into());
830
-
831
- // t.read
832
- let read_fn = v8::Function::new(scope, native_read).unwrap();
833
- let read_key = v8_str(scope, "read");
834
- t_obj.set(scope, read_key.into(), read_fn.into());
835
-
836
- // t.log
837
- let log_fn = v8::Function::new(scope, native_log).unwrap();
838
- let log_key = v8_str(scope, "log");
839
- t_obj.set(scope, log_key.into(), log_fn.into());
840
-
841
- // t.fetch
842
- let fetch_fn = v8::Function::new(scope, native_fetch).unwrap();
843
- let fetch_key = v8_str(scope, "fetch");
844
- t_obj.set(scope, fetch_key.into(), fetch_fn.into());
845
-
846
- // t.jwt
847
- let jwt_obj = v8::Object::new(scope);
848
- let sign_fn = v8::Function::new(scope, native_jwt_sign).unwrap();
849
- let verify_fn = v8::Function::new(scope, native_jwt_verify).unwrap();
850
-
851
- let sign_key = v8_str(scope, "sign");
852
- jwt_obj.set(scope, sign_key.into(), sign_fn.into());
853
- let verify_key = v8_str(scope, "verify");
854
- jwt_obj.set(scope, verify_key.into(), verify_fn.into());
855
-
856
- let jwt_key = v8_str(scope, "jwt");
857
- t_obj.set(scope, jwt_key.into(), jwt_obj.into());
858
-
859
- // t.password
860
- let pw_obj = v8::Object::new(scope);
861
- let hash_fn = v8::Function::new(scope, native_password_hash).unwrap();
862
- let pw_verify_fn = v8::Function::new(scope, native_password_verify).unwrap();
863
-
864
- let hash_key = v8_str(scope, "hash");
865
- pw_obj.set(scope, hash_key.into(), hash_fn.into());
866
- let pw_verify_key = v8_str(scope, "verify");
867
- pw_obj.set(scope, pw_verify_key.into(), pw_verify_fn.into());
868
-
869
- let pw_key = v8_str(scope, "password");
870
- t_obj.set(scope, pw_key.into(), pw_obj.into());
871
-
872
- // Inject __titan_invoke_native
873
- let invoke_fn = v8::Function::new(scope, native_invoke_extension).unwrap();
874
- let invoke_key = v8_str(scope, "__titan_invoke_native");
875
- global.set(scope, invoke_key.into(), invoke_fn.into());
876
-
877
- // Inject Loaded Extensions
878
- let modules = if let Ok(guard) = REGISTRY.lock() {
879
- if let Some(registry) = &*guard {
880
- registry.modules.clone()
881
- } else {
882
- Vec::new()
883
- }
884
- } else {
885
- Vec::new()
886
- };
887
-
888
- for module in modules {
889
- // 1. Prepare Native Wrappers
890
- let natives_obj = v8::Object::new(scope);
891
- for (fn_name, &idx) in &module.native_indices {
892
- let code = format!(
893
- "(function(a, b) {{ return __titan_invoke_native({}, a, b); }})",
894
- idx
895
- );
896
- let source = v8_str(scope, &code);
897
- // Compile wrappers
898
- if let Some(script) = v8::Script::compile(scope, source, None) {
899
- if let Some(val) = script.run(scope) {
900
- let key = v8_str(scope, fn_name);
901
- natives_obj.set(scope, key.into(), val);
902
- }
903
- }
904
- }
905
-
906
- // 2. Prepare JS Wrapper (CommonJS shim)
907
- // We pass 't' and 'native' (the object we just made) to the module.
908
- let wrapper_src = format!(
909
- r#"(function(t, native) {{
910
- var module = {{ exports: {{}} }};
911
- var exports = module.exports;
912
- {}
913
- return module.exports;
914
- }})"#,
915
- module.js
916
- );
917
-
918
- let source = v8_str(scope, &wrapper_src);
919
- let tc = &mut v8::TryCatch::new(scope);
920
-
921
- // 3. Compile and Run
922
- if let Some(script) = v8::Script::compile(tc, source, None) {
923
- if let Some(factory_val) = script.run(tc) {
924
- if let Ok(factory) = v8::Local::<v8::Function>::try_from(factory_val) {
925
- let recv = v8::undefined(&mut *tc).into();
926
- // Pass t_obj and natives_obj
927
- let args = [t_obj.into(), natives_obj.into()];
928
-
929
- if let Some(exports_val) = factory.call(&mut *tc, recv, &args) {
930
- // 4. Assign exports to t.<extName>
931
- let mod_key = v8_str(&mut *tc, &module.name);
932
- t_obj.set(&mut *tc, mod_key.into(), exports_val);
933
-
934
- // println!(
935
- // "{} {} {}",
936
- // crate::utils::blue("[Titan]"),
937
- // crate::utils::green("Injected extension:"),
938
- // module.name
939
- // );
940
- } else {
941
- // Execution error
942
- if let Some(msg) = tc.message() {
943
- let text = msg.get(&mut *tc).to_rust_string_lossy(&mut *tc);
944
- println!(
945
- "{} {} {} -> {}",
946
- crate::utils::blue("[Titan]"),
947
- crate::utils::red("Error running extension"),
948
- module.name,
949
- text
950
- );
951
- }
952
- }
953
- }
954
- } else {
955
- // Runtime error during script run
956
- if let Some(msg) = tc.message() {
957
- let text = msg.get(&mut *tc).to_rust_string_lossy(&mut *tc);
958
- println!(
959
- "{} {} {} -> {}",
960
- crate::utils::blue("[Titan]"),
961
- crate::utils::red("Error evaluating extension wrapper"),
962
- module.name,
963
- text
964
- );
965
- }
966
- }
967
- } else {
968
- // Compile error
969
- if let Some(msg) = tc.message() {
970
- let text = msg.get(&mut *tc).to_rust_string_lossy(&mut *tc);
971
- println!(
972
- "{} {} {} -> {}",
973
- crate::utils::blue("[Titan]"),
974
- crate::utils::red("Syntax Error in extension"),
975
- module.name,
976
- text
977
- );
978
- }
979
- }
980
- }
981
-
982
- // t.db (Stub for now)
983
- let db_obj = v8::Object::new(scope);
984
- let db_key = v8_str(scope, "db");
985
- t_obj.set(scope, db_key.into(), db_obj.into());
986
-
987
- let t_key = v8_str(scope, "t");
988
- global.set(scope, t_key.into(), t_obj.into());
989
- }
1
+ #![allow(unused)]
2
+ use bcrypt::{DEFAULT_COST, hash, verify};
3
+ use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
4
+ use reqwest::{
5
+ blocking::Client,
6
+ Method,
7
+ header::{HeaderMap, HeaderName, HeaderValue},
8
+ };
9
+ use serde_json::Value;
10
+ use std::path::PathBuf;
11
+ use std::sync::Once;
12
+ use std::time::{SystemTime, UNIX_EPOCH};
13
+ use v8;
14
+
15
+ use crate::utils::{blue, gray, green, parse_expires_in};
16
+ use libloading::Library;
17
+ use std::collections::HashMap;
18
+ use std::fs;
19
+ use std::sync::Mutex;
20
+ use walkdir::WalkDir;
21
+
22
+
23
+ // ----------------------------------------------------------------------------
24
+ // RUST ACTION API
25
+ // ----------------------------------------------------------------------------
26
+
27
+ pub struct T {
28
+ pub jwt: Jwt,
29
+ pub password: Password,
30
+ }
31
+
32
+ #[allow(non_upper_case_globals)]
33
+ pub static t: T = T {
34
+ jwt: Jwt,
35
+ password: Password,
36
+ };
37
+
38
+ pub struct Jwt;
39
+ impl Jwt {
40
+ pub fn sign(&self, payload: Value, secret: &str, options: Option<Value>) -> anyhow::Result<String> {
41
+ let mut final_payload = match payload {
42
+ Value::Object(map) => map,
43
+ _ => serde_json::Map::new(), // Should probably error or handle string payload like JS
44
+ };
45
+
46
+ if let Some(opts) = options {
47
+ if let Some(exp_val) = opts.get("expiresIn") {
48
+ // Handle both number (seconds) and string ("1h")
49
+ let seconds = if let Some(n) = exp_val.as_u64() {
50
+ Some(n)
51
+ } else if let Some(s) = exp_val.as_str() {
52
+ parse_expires_in(s)
53
+ } else {
54
+ None
55
+ };
56
+
57
+ if let Some(sec) = seconds {
58
+ let now = SystemTime::now()
59
+ .duration_since(UNIX_EPOCH)
60
+ .unwrap()
61
+ .as_secs();
62
+ final_payload.insert("exp".to_string(), Value::Number(serde_json::Number::from(now + sec)));
63
+ }
64
+ }
65
+ }
66
+
67
+ let token = encode(
68
+ &Header::default(),
69
+ &Value::Object(final_payload),
70
+ &EncodingKey::from_secret(secret.as_bytes()),
71
+ )?;
72
+ Ok(token)
73
+ }
74
+
75
+ pub fn verify(&self, token: &str, secret: &str) -> anyhow::Result<Value> {
76
+ let mut validation = Validation::default();
77
+ validation.validate_exp = true;
78
+
79
+ let data = decode::<Value>(
80
+ token,
81
+ &DecodingKey::from_secret(secret.as_bytes()),
82
+ &validation,
83
+ )?;
84
+ Ok(data.claims)
85
+ }
86
+ }
87
+
88
+ pub struct Password;
89
+ impl Password {
90
+ pub fn hash(&self, password: &str) -> anyhow::Result<String> {
91
+ let h = hash(password, DEFAULT_COST)?;
92
+ Ok(h)
93
+ }
94
+
95
+ pub fn verify(&self, password: &str, hash_str: &str) -> bool {
96
+ verify(password, hash_str).unwrap_or(false)
97
+ }
98
+ }
99
+
100
+ impl T {
101
+ pub fn log(&self, msg: impl std::fmt::Display) {
102
+ println!(
103
+ "{} {}",
104
+ blue("[Titan]"),
105
+ gray(&format!("\x1b[90mlog(rust)\x1b[0m\x1b[97m: {}\x1b[0m", msg))
106
+ );
107
+ }
108
+
109
+ pub fn read(&self, path: &str) -> anyhow::Result<String> {
110
+ let root = std::env::current_dir()?;
111
+ let target = root.join(path);
112
+ let target = target.canonicalize()?;
113
+ Ok(fs::read_to_string(target)?)
114
+ }
115
+
116
+ pub async fn fetch(&self, url: &str, options: Option<FetchOptions>) -> anyhow::Result<FetchResponse> {
117
+ let client = reqwest::Client::new();
118
+ let opts = options.unwrap_or_default();
119
+
120
+ let mut req = client.request(opts.method.parse().unwrap_or(Method::GET), url);
121
+
122
+ if let Some(headers) = opts.headers {
123
+ let mut map = HeaderMap::new();
124
+ for (k, v) in headers {
125
+ if let (Ok(name), Ok(val)) = (
126
+ HeaderName::from_bytes(k.as_bytes()),
127
+ HeaderValue::from_str(&v),
128
+ ) {
129
+ map.insert(name, val);
130
+ }
131
+ }
132
+ req = req.headers(map);
133
+ }
134
+
135
+ if let Some(body) = opts.body {
136
+ req = req.body(body);
137
+ }
138
+
139
+ let res = req.send().await?;
140
+ let status = res.status().as_u16();
141
+ let text = res.text().await?;
142
+
143
+ Ok(FetchResponse {
144
+ status,
145
+ body: text,
146
+ ok: status >= 200 && status < 300
147
+ })
148
+ }
149
+ }
150
+
151
+ #[derive(Default)]
152
+ pub struct FetchOptions {
153
+ pub method: String,
154
+ pub headers: Option<std::collections::HashMap<String, String>>,
155
+ pub body: Option<String>,
156
+ }
157
+
158
+ pub struct FetchResponse {
159
+ pub status: u16,
160
+ pub body: String,
161
+ pub ok: bool,
162
+ }
163
+
164
+ // ----------------------------------------------------------------------------
165
+ // GLOBAL REGISTRY
166
+ // ----------------------------------------------------------------------------
167
+
168
+ static REGISTRY: Mutex<Option<Registry>> = Mutex::new(None);
169
+ #[allow(dead_code)]
170
+ struct Registry {
171
+ _libs: Vec<Library>,
172
+ modules: Vec<ModuleDef>,
173
+ natives: Vec<NativeFnEntry>, // Flattened list of all native functions
174
+ }
175
+
176
+ #[derive(Clone)]
177
+ struct ModuleDef {
178
+ name: String,
179
+ js: String,
180
+ native_indices: HashMap<String, usize>, // Function Name -> Index in REGISTRY.natives
181
+ }
182
+
183
+ struct NativeFnEntry {
184
+ ptr: usize,
185
+ sig: Signature,
186
+ }
187
+
188
+ #[derive(Clone, Copy)]
189
+ enum Signature {
190
+ F64TwoArgsRetF64,
191
+ Unknown,
192
+ }
193
+
194
+ #[derive(serde::Deserialize)]
195
+ struct TitanConfig {
196
+ name: String,
197
+ main: String,
198
+ native: Option<TitanNativeConfig>,
199
+ }
200
+ #[derive(serde::Deserialize)]
201
+ struct TitanNativeConfig {
202
+ path: String,
203
+ functions: HashMap<String, TitanNativeFunc>,
204
+ }
205
+ #[derive(serde::Deserialize)]
206
+ struct TitanNativeFunc {
207
+ symbol: String,
208
+ #[serde(default)]
209
+ parameters: Vec<String>,
210
+ #[serde(default)]
211
+ result: String,
212
+ }
213
+
214
+ pub fn load_project_extensions(root: PathBuf) {
215
+ let mut modules = Vec::new();
216
+ let mut libs = Vec::new();
217
+ let mut all_natives = Vec::new();
218
+
219
+ // =====================================================
220
+ // 1. Resolve all extension search directories
221
+ // =====================================================
222
+
223
+ let mut search_dirs = Vec::new();
224
+
225
+ let ext_dir = root.join(".ext"); // Production
226
+ let nm_root = root.join("node_modules"); // Dev
227
+ let nm_parent = root.parent().map(|p| p.join("node_modules")); // Monorepo
228
+
229
+ // 1) Production
230
+ if ext_dir.exists() {
231
+ search_dirs.push(ext_dir);
232
+ }
233
+
234
+ // 2) Dev: project node_modules
235
+ if nm_root.exists() {
236
+ search_dirs.push(nm_root.clone());
237
+ }
238
+
239
+ // 3) Dev monorepo: parent/node_modules
240
+ if let Some(nmp) = &nm_parent {
241
+ if nmp.exists() {
242
+ search_dirs.push(nmp.clone());
243
+ }
244
+ }
245
+
246
+ // 4) Never return empty — add root/node_modules even if missing
247
+ if search_dirs.is_empty() {
248
+ search_dirs.push(nm_root);
249
+ }
250
+
251
+ // Normalize and dedupe
252
+ search_dirs.sort();
253
+ search_dirs.dedup();
254
+
255
+ // println!("{} Scanning extension directories:", blue("[Titan]"));
256
+ for d in &search_dirs {
257
+
258
+ // let label = if d.to_string_lossy().contains(".ext") {
259
+ // crate::utils::green("(Production)")
260
+ // } else {
261
+ // crate::utils::yellow("(Development)")
262
+ // };
263
+ // println!(" • {} {}", d.display(), label);
264
+
265
+ }
266
+
267
+ // =====================================================
268
+ // 2. Walk and locate titan.json inside search paths
269
+ // =====================================================
270
+ for dir in &search_dirs {
271
+ if !dir.exists() {
272
+ println!(" {} Skipping non-existent directory: {}", crate::utils::yellow("⚠"), dir.display());
273
+ continue;
274
+ }
275
+
276
+ for entry in WalkDir::new(&dir)
277
+ .min_depth(1)
278
+ .max_depth(5) // Increased depth
279
+ .follow_links(true)
280
+ {
281
+ let entry = match entry {
282
+ Ok(e) => e,
283
+ Err(_) => continue,
284
+ };
285
+
286
+ // Only accept titan.json files
287
+ if entry.file_type().is_file() && entry.file_name() == "titan.json" {
288
+ let path = entry.path();
289
+ // Load config file
290
+ let config_content = match fs::read_to_string(path) {
291
+ Ok(c) => c,
292
+ Err(e) => {
293
+ println!("{} Failed to read {}: {}", crate::utils::red("[Titan]"), path.display(), e);
294
+ continue;
295
+ }
296
+ };
297
+
298
+ let config: TitanConfig = match serde_json::from_str(&config_content) {
299
+ Ok(c) => c,
300
+ Err(e) => {
301
+ println!("{} Failed to parse {}: {}", crate::utils::red("[Titan]"), path.display(), e);
302
+ continue;
303
+ }
304
+ };
305
+
306
+ let pkg_dir = path.parent().unwrap();
307
+ let mut mod_natives_map = HashMap::new();
308
+
309
+ // =====================================================
310
+ // 3. Load native extension (optional)
311
+ // =====================================================
312
+ if let Some(native_conf) = config.native {
313
+ let lib_path = pkg_dir.join(&native_conf.path);
314
+
315
+ unsafe {
316
+ match Library::new(&lib_path) {
317
+ Ok(lib) => {
318
+ for (fn_name, fn_conf) in native_conf.functions {
319
+ let sig = if fn_conf.parameters == ["f64", "f64"]
320
+ && fn_conf.result == "f64"
321
+ {
322
+ Signature::F64TwoArgsRetF64
323
+ } else {
324
+ Signature::Unknown
325
+ };
326
+
327
+ if let Ok(symbol) = lib.get::<*const ()>(fn_conf.symbol.as_bytes())
328
+ {
329
+ let idx = all_natives.len();
330
+ all_natives.push(NativeFnEntry {
331
+ ptr: *symbol as usize,
332
+ sig,
333
+ });
334
+ mod_natives_map.insert(fn_name, idx);
335
+ }
336
+ }
337
+ libs.push(lib);
338
+ }
339
+ Err(e) => println!(
340
+ "{} Failed to load native library {} ({})",
341
+ blue("[Titan]"),
342
+ lib_path.display(),
343
+ e
344
+ ),
345
+ }
346
+ }
347
+ }
348
+
349
+ // =====================================================
350
+ // 4. Load JS module file
351
+ // =====================================================
352
+ let js_path = pkg_dir.join(&config.main);
353
+ let js_content = match fs::read_to_string(&js_path) {
354
+ Ok(c) => c,
355
+ Err(e) => {
356
+ println!("{} Failed to read JS main {} for extension {}: {}",
357
+ crate::utils::red("[Titan]"),
358
+ js_path.display(),
359
+ config.name,
360
+ e
361
+ );
362
+ continue;
363
+ }
364
+ };
365
+
366
+ modules.push(ModuleDef {
367
+ name: config.name.clone(),
368
+ js: js_content,
369
+ native_indices: mod_natives_map,
370
+ });
371
+
372
+ let source_label = if dir.to_string_lossy().contains(".ext") {
373
+ "Production"
374
+ } else {
375
+ "Development"
376
+ };
377
+
378
+ println!(
379
+ "{} {} {} ({})",
380
+ blue("[Titan]"),
381
+ green("Extension loaded:"),
382
+ config.name,
383
+ source_label
384
+ );
385
+ }
386
+ }
387
+ }
388
+
389
+ // =====================================================
390
+ // 5. Store registry globally
391
+ // =====================================================
392
+ if modules.is_empty() {
393
+ // println!("{} {}", blue("[Titan]"), crate::utils::yellow("No extensions loaded."));
394
+ }
395
+
396
+ *REGISTRY.lock().unwrap() = Some(Registry {
397
+ _libs: libs,
398
+ modules,
399
+ natives: all_natives,
400
+ });
401
+ }
402
+
403
+ static V8_INIT: Once = Once::new();
404
+
405
+ pub fn init_v8() {
406
+ V8_INIT.call_once(|| {
407
+ let platform = v8::new_default_platform(0, false).make_shared();
408
+ v8::V8::initialize_platform(platform);
409
+ v8::V8::initialize();
410
+ });
411
+ }
412
+
413
+ fn v8_str<'s>(scope: &mut v8::HandleScope<'s>, s: &str) -> v8::Local<'s, v8::String> {
414
+ v8::String::new(scope, s).unwrap()
415
+ }
416
+
417
+ fn v8_to_string(scope: &mut v8::HandleScope, value: v8::Local<v8::Value>) -> String {
418
+ value.to_string(scope).unwrap().to_rust_string_lossy(scope)
419
+ }
420
+
421
+ fn throw(scope: &mut v8::HandleScope, msg: &str) {
422
+ let message = v8_str(scope, msg);
423
+ let exception = v8::Exception::error(scope, message);
424
+ scope.throw_exception(exception);
425
+ }
426
+
427
+ // ----------------------------------------------------------------------------
428
+ // NATIVE CALLBACKS
429
+ // ----------------------------------------------------------------------------
430
+
431
+ fn native_read(
432
+ scope: &mut v8::HandleScope,
433
+ args: v8::FunctionCallbackArguments,
434
+ mut retval: v8::ReturnValue,
435
+ ) {
436
+ let path_val = args.get(0);
437
+ // 1. Read argument
438
+ if !path_val.is_string() {
439
+ throw(scope, "t.read(path): path is required");
440
+ return;
441
+ }
442
+ let path_str = v8_to_string(scope, path_val);
443
+
444
+ // 2. Check if absolute
445
+ if std::path::Path::new(&path_str).is_absolute() {
446
+ throw(scope, "t.read expects a relative path like 'db/file.sql'");
447
+ return;
448
+ }
449
+
450
+ let context = scope.get_current_context();
451
+ let global = context.global(scope);
452
+ let root_key = v8_str(scope, "__titan_root");
453
+ let root_val = global.get(scope, root_key.into()).unwrap();
454
+
455
+ let root_str = if root_val.is_string() {
456
+ v8_to_string(scope, root_val)
457
+ } else {
458
+ throw(scope, "Internal Error: __titan_root not set");
459
+ return;
460
+ };
461
+
462
+ let root_path = PathBuf::from(root_str);
463
+ let root_path = root_path.canonicalize().unwrap_or(root_path);
464
+ let joined = root_path.join(&path_str);
465
+
466
+ // 3. Canonicalize (resolves ../)
467
+ let target = match joined.canonicalize() {
468
+ Ok(target) => target,
469
+ Err(_) => {
470
+ throw(scope, &format!("t.read: file not found: {}", path_str));
471
+ return;
472
+ }
473
+ };
474
+
475
+ // 4. Enforce root boundary
476
+ if !target.starts_with(&root_path) {
477
+ throw(scope, "t.read: path escapes allowed root");
478
+ return;
479
+ }
480
+
481
+ // 5. Read file
482
+ match std::fs::read_to_string(&target) {
483
+ Ok(content) => {
484
+ retval.set(v8_str(scope, &content).into());
485
+ }
486
+ Err(e) => {
487
+ throw(scope, &format!("t.read failed: {}", e));
488
+ }
489
+ }
490
+ }
491
+
492
+ fn native_log(
493
+ scope: &mut v8::HandleScope,
494
+ args: v8::FunctionCallbackArguments,
495
+ mut _retval: v8::ReturnValue,
496
+ ) {
497
+ let context = scope.get_current_context();
498
+ let global = context.global(scope);
499
+ let action_key = v8_str(scope, "__titan_action");
500
+ let action_val = global.get(scope, action_key.into()).unwrap();
501
+ let action_name = v8_to_string(scope, action_val);
502
+
503
+ let mut parts = Vec::new();
504
+ for i in 0..args.length() {
505
+ let val = args.get(i);
506
+ let mut appended = false;
507
+
508
+ // Try to JSON stringify objects so they are readable in logs
509
+ if val.is_object() && !val.is_function() {
510
+ if let Some(json) = v8::json::stringify(scope, val) {
511
+ parts.push(json.to_rust_string_lossy(scope));
512
+ appended = true;
513
+ }
514
+ }
515
+
516
+ if !appended {
517
+ parts.push(v8_to_string(scope, val));
518
+ }
519
+ }
520
+
521
+ println!(
522
+ "{} {}",
523
+ blue("[Titan]"),
524
+ gray(&format!(
525
+ "\x1b[90mlog({})\x1b[0m\x1b[97m: {}\x1b[0m",
526
+ action_name,
527
+ parts.join(" ")
528
+ ))
529
+ );
530
+ }
531
+
532
+ fn native_fetch(
533
+ scope: &mut v8::HandleScope,
534
+ args: v8::FunctionCallbackArguments,
535
+ mut retval: v8::ReturnValue,
536
+ ) {
537
+ let url = v8_to_string(scope, args.get(0));
538
+
539
+ // Check for options (method, headers, body)
540
+ let mut method = "GET".to_string();
541
+ let mut body_str = None;
542
+ let mut headers_vec = Vec::new();
543
+
544
+ let opts_val = args.get(1);
545
+ if opts_val.is_object() {
546
+ let opts_obj = opts_val.to_object(scope).unwrap();
547
+
548
+ // method
549
+ let m_key = v8_str(scope, "method");
550
+ if let Some(m_val) = opts_obj.get(scope, m_key.into()) {
551
+ if m_val.is_string() {
552
+ method = v8_to_string(scope, m_val);
553
+ }
554
+ }
555
+
556
+ // body
557
+ let b_key = v8_str(scope, "body");
558
+ if let Some(b_val) = opts_obj.get(scope, b_key.into()) {
559
+ if b_val.is_string() {
560
+ body_str = Some(v8_to_string(scope, b_val));
561
+ } else if b_val.is_object() {
562
+ let json_obj = v8::json::stringify(scope, b_val).unwrap();
563
+ body_str = Some(json_obj.to_rust_string_lossy(scope));
564
+ }
565
+ }
566
+
567
+ // headers
568
+ let h_key = v8_str(scope, "headers");
569
+ if let Some(h_val) = opts_obj.get(scope, h_key.into()) {
570
+ if h_val.is_object() {
571
+ let h_obj = h_val.to_object(scope).unwrap();
572
+ if let Some(keys) = h_obj.get_own_property_names(scope, Default::default()) {
573
+ for i in 0..keys.length() {
574
+ let key = keys.get_index(scope, i).unwrap();
575
+ let val = h_obj.get(scope, key).unwrap();
576
+ headers_vec.push((v8_to_string(scope, key), v8_to_string(scope, val)));
577
+ }
578
+ }
579
+ }
580
+ }
581
+ }
582
+
583
+ let client = Client::builder()
584
+ .use_rustls_tls()
585
+ .tcp_nodelay(true)
586
+ .build()
587
+ .unwrap_or(Client::new());
588
+
589
+ let mut req = client.request(method.parse().unwrap_or(reqwest::Method::GET), &url);
590
+
591
+ for (k, v) in headers_vec {
592
+ if let (Ok(name), Ok(val)) = (
593
+ HeaderName::from_bytes(k.as_bytes()),
594
+ HeaderValue::from_str(&v),
595
+ ) {
596
+ let mut map = HeaderMap::new();
597
+ map.insert(name, val);
598
+ req = req.headers(map);
599
+ }
600
+ }
601
+
602
+ if let Some(b) = body_str {
603
+ req = req.body(b);
604
+ }
605
+
606
+ let res = req.send();
607
+
608
+ let obj = v8::Object::new(scope);
609
+ match res {
610
+ Ok(r) => {
611
+ let status = r.status().as_u16();
612
+ let text = r.text().unwrap_or_default();
613
+
614
+ let status_key = v8_str(scope, "status");
615
+ let status_val = v8::Number::new(scope, status as f64);
616
+ obj.set(scope, status_key.into(), status_val.into());
617
+
618
+ let body_key = v8_str(scope, "body");
619
+ let body_val = v8_str(scope, &text);
620
+ obj.set(scope, body_key.into(), body_val.into());
621
+
622
+ let ok_key = v8_str(scope, "ok");
623
+ let ok_val = v8::Boolean::new(scope, true);
624
+ obj.set(scope, ok_key.into(), ok_val.into());
625
+ }
626
+ Err(e) => {
627
+ let ok_key = v8_str(scope, "ok");
628
+ let ok_val = v8::Boolean::new(scope, false);
629
+ obj.set(scope, ok_key.into(), ok_val.into());
630
+
631
+ let err_key = v8_str(scope, "error");
632
+ let err_val = v8_str(scope, &e.to_string());
633
+ obj.set(scope, err_key.into(), err_val.into());
634
+ }
635
+ }
636
+ retval.set(obj.into());
637
+ }
638
+
639
+ fn native_jwt_sign(
640
+ scope: &mut v8::HandleScope,
641
+ args: v8::FunctionCallbackArguments,
642
+ mut retval: v8::ReturnValue,
643
+ ) {
644
+ // payload, secret, options
645
+ let payload_val = args.get(0);
646
+ // Parse payload to serde_json::Map
647
+ let json_str = v8::json::stringify(scope, payload_val)
648
+ .unwrap()
649
+ .to_rust_string_lossy(scope);
650
+ let mut payload: serde_json::Map<String, Value> =
651
+ serde_json::from_str(&json_str).unwrap_or_default();
652
+
653
+ let secret = v8_to_string(scope, args.get(1));
654
+
655
+ let opts_val = args.get(2);
656
+ if opts_val.is_object() {
657
+ let opts_obj = opts_val.to_object(scope).unwrap();
658
+ let exp_key = v8_str(scope, "expiresIn");
659
+
660
+ if let Some(val) = opts_obj.get(scope, exp_key.into()) {
661
+ let seconds = if val.is_number() {
662
+ Some(val.to_number(scope).unwrap().value() as u64)
663
+ } else if val.is_string() {
664
+ parse_expires_in(&v8_to_string(scope, val))
665
+ } else {
666
+ None
667
+ };
668
+
669
+ if let Some(sec) = seconds {
670
+ let now = SystemTime::now()
671
+ .duration_since(UNIX_EPOCH)
672
+ .unwrap()
673
+ .as_secs();
674
+ payload.insert(
675
+ "exp".to_string(),
676
+ Value::Number(serde_json::Number::from(now + sec)),
677
+ );
678
+ }
679
+ }
680
+ }
681
+
682
+ let token = encode(
683
+ &Header::default(),
684
+ &Value::Object(payload),
685
+ &EncodingKey::from_secret(secret.as_bytes()),
686
+ );
687
+
688
+ match token {
689
+ Ok(tok) => retval.set(v8_str(scope, &tok).into()),
690
+ Err(e) => throw(scope, &e.to_string()),
691
+ }
692
+ }
693
+
694
+ fn native_jwt_verify(
695
+ scope: &mut v8::HandleScope,
696
+ args: v8::FunctionCallbackArguments,
697
+ mut retval: v8::ReturnValue,
698
+ ) {
699
+ let token = v8_to_string(scope, args.get(0));
700
+ let secret = v8_to_string(scope, args.get(1));
701
+
702
+ let mut validation = Validation::default();
703
+ validation.validate_exp = true;
704
+
705
+ let data = decode::<Value>(
706
+ &token,
707
+ &DecodingKey::from_secret(secret.as_bytes()),
708
+ &validation,
709
+ );
710
+
711
+ match data {
712
+ Ok(d) => {
713
+ // Convert claim back to V8 object via JSON
714
+ let json_str = serde_json::to_string(&d.claims).unwrap();
715
+ let v8_json_str = v8_str(scope, &json_str);
716
+ if let Some(val) = v8::json::parse(scope, v8_json_str) {
717
+ retval.set(val);
718
+ }
719
+ }
720
+ Err(e) => throw(scope, &format!("Invalid or expired JWT: {}", e)),
721
+ }
722
+ }
723
+
724
+ fn native_password_hash(
725
+ scope: &mut v8::HandleScope,
726
+ args: v8::FunctionCallbackArguments,
727
+ mut retval: v8::ReturnValue,
728
+ ) {
729
+ let pw = v8_to_string(scope, args.get(0));
730
+ match hash(pw, DEFAULT_COST) {
731
+ Ok(h) => retval.set(v8_str(scope, &h).into()),
732
+ Err(e) => throw(scope, &e.to_string()),
733
+ }
734
+ }
735
+
736
+ fn native_password_verify(
737
+ scope: &mut v8::HandleScope,
738
+ args: v8::FunctionCallbackArguments,
739
+ mut retval: v8::ReturnValue,
740
+ ) {
741
+ let pw = v8_to_string(scope, args.get(0));
742
+ let hash_str = v8_to_string(scope, args.get(1));
743
+
744
+ let ok = verify(pw, &hash_str).unwrap_or(false);
745
+ retval.set(v8::Boolean::new(scope, ok).into());
746
+ }
747
+
748
+ fn native_define_action(
749
+ _scope: &mut v8::HandleScope,
750
+ args: v8::FunctionCallbackArguments,
751
+ mut retval: v8::ReturnValue,
752
+ ) {
753
+ retval.set(args.get(0));
754
+ }
755
+
756
+ // ----------------------------------------------------------------------------
757
+ // NATIVE CALLBACKS (EXTENSIONS)
758
+ // ----------------------------------------------------------------------------
759
+
760
+ // generic wrappers could go here if needed
761
+
762
+ fn native_invoke_extension(
763
+ scope: &mut v8::HandleScope,
764
+ args: v8::FunctionCallbackArguments,
765
+ mut retval: v8::ReturnValue,
766
+ ) {
767
+ let fn_idx = args.get(0).to_integer(scope).unwrap().value() as usize;
768
+
769
+ // Get pointer from registry
770
+ let mut ptr = 0;
771
+ let mut sig = Signature::Unknown;
772
+
773
+ if let Ok(guard) = REGISTRY.lock() {
774
+ if let Some(registry) = &*guard {
775
+ if let Some(entry) = registry.natives.get(fn_idx) {
776
+ ptr = entry.ptr;
777
+ sig = entry.sig;
778
+ }
779
+ }
780
+ }
781
+
782
+ if ptr == 0 {
783
+ throw(scope, "Native function not found");
784
+ return;
785
+ }
786
+
787
+ match sig {
788
+ Signature::F64TwoArgsRetF64 => {
789
+ let a = args
790
+ .get(1)
791
+ .to_number(scope)
792
+ .unwrap_or(v8::Number::new(scope, 0.0))
793
+ .value();
794
+ let b = args
795
+ .get(2)
796
+ .to_number(scope)
797
+ .unwrap_or(v8::Number::new(scope, 0.0))
798
+ .value();
799
+
800
+ unsafe {
801
+ let func: extern "C" fn(f64, f64) -> f64 = std::mem::transmute(ptr);
802
+ let res = func(a, b);
803
+ retval.set(v8::Number::new(scope, res).into());
804
+ }
805
+ }
806
+ _ => throw(scope, "Unsupported signature"),
807
+ }
808
+ }
809
+
810
+ // ----------------------------------------------------------------------------
811
+ // INJECTOR
812
+ // ----------------------------------------------------------------------------
813
+
814
+ pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Object>) {
815
+ // Ensure globalThis reference
816
+ let gt_key = v8_str(scope, "globalThis");
817
+ global.set(scope, gt_key.into(), global.into());
818
+
819
+ let t_obj = v8::Object::new(scope);
820
+ let t_key = v8_str(scope, "t");
821
+ // Use create_data_property to guarantee definition
822
+ global
823
+ .create_data_property(scope, t_key.into(), t_obj.into())
824
+ .unwrap();
825
+
826
+ // defineAction (identity function for clean typing)
827
+ let def_fn = v8::Function::new(scope, native_define_action).unwrap();
828
+ let def_key = v8_str(scope, "defineAction");
829
+ global.set(scope, def_key.into(), def_fn.into());
830
+
831
+ // t.read
832
+ let read_fn = v8::Function::new(scope, native_read).unwrap();
833
+ let read_key = v8_str(scope, "read");
834
+ t_obj.set(scope, read_key.into(), read_fn.into());
835
+
836
+ // t.log
837
+ let log_fn = v8::Function::new(scope, native_log).unwrap();
838
+ let log_key = v8_str(scope, "log");
839
+ t_obj.set(scope, log_key.into(), log_fn.into());
840
+
841
+ // t.fetch
842
+ let fetch_fn = v8::Function::new(scope, native_fetch).unwrap();
843
+ let fetch_key = v8_str(scope, "fetch");
844
+ t_obj.set(scope, fetch_key.into(), fetch_fn.into());
845
+
846
+ // t.jwt
847
+ let jwt_obj = v8::Object::new(scope);
848
+ let sign_fn = v8::Function::new(scope, native_jwt_sign).unwrap();
849
+ let verify_fn = v8::Function::new(scope, native_jwt_verify).unwrap();
850
+
851
+ let sign_key = v8_str(scope, "sign");
852
+ jwt_obj.set(scope, sign_key.into(), sign_fn.into());
853
+ let verify_key = v8_str(scope, "verify");
854
+ jwt_obj.set(scope, verify_key.into(), verify_fn.into());
855
+
856
+ let jwt_key = v8_str(scope, "jwt");
857
+ t_obj.set(scope, jwt_key.into(), jwt_obj.into());
858
+
859
+ // t.password
860
+ let pw_obj = v8::Object::new(scope);
861
+ let hash_fn = v8::Function::new(scope, native_password_hash).unwrap();
862
+ let pw_verify_fn = v8::Function::new(scope, native_password_verify).unwrap();
863
+
864
+ let hash_key = v8_str(scope, "hash");
865
+ pw_obj.set(scope, hash_key.into(), hash_fn.into());
866
+ let pw_verify_key = v8_str(scope, "verify");
867
+ pw_obj.set(scope, pw_verify_key.into(), pw_verify_fn.into());
868
+
869
+ let pw_key = v8_str(scope, "password");
870
+ t_obj.set(scope, pw_key.into(), pw_obj.into());
871
+
872
+ // Inject __titan_invoke_native
873
+ let invoke_fn = v8::Function::new(scope, native_invoke_extension).unwrap();
874
+ let invoke_key = v8_str(scope, "__titan_invoke_native");
875
+ global.set(scope, invoke_key.into(), invoke_fn.into());
876
+
877
+ // Inject Loaded Extensions
878
+ let modules = if let Ok(guard) = REGISTRY.lock() {
879
+ if let Some(registry) = &*guard {
880
+ registry.modules.clone()
881
+ } else {
882
+ Vec::new()
883
+ }
884
+ } else {
885
+ Vec::new()
886
+ };
887
+
888
+ for module in modules {
889
+ // 1. Prepare Native Wrappers
890
+ let natives_obj = v8::Object::new(scope);
891
+ for (fn_name, &idx) in &module.native_indices {
892
+ let code = format!(
893
+ "(function(a, b) {{ return __titan_invoke_native({}, a, b); }})",
894
+ idx
895
+ );
896
+ let source = v8_str(scope, &code);
897
+ // Compile wrappers
898
+ if let Some(script) = v8::Script::compile(scope, source, None) {
899
+ if let Some(val) = script.run(scope) {
900
+ let key = v8_str(scope, fn_name);
901
+ natives_obj.set(scope, key.into(), val);
902
+ }
903
+ }
904
+ }
905
+
906
+ // 2. Prepare JS Wrapper (CommonJS shim)
907
+ // We pass 't' and 'native' (the object we just made) to the module.
908
+ let wrapper_src = format!(
909
+ r#"(function(t, native) {{
910
+ var module = {{ exports: {{}} }};
911
+ var exports = module.exports;
912
+ {}
913
+ return module.exports;
914
+ }})"#,
915
+ module.js
916
+ );
917
+
918
+ let source = v8_str(scope, &wrapper_src);
919
+ let tc = &mut v8::TryCatch::new(scope);
920
+
921
+ // 3. Compile and Run
922
+ if let Some(script) = v8::Script::compile(tc, source, None) {
923
+ if let Some(factory_val) = script.run(tc) {
924
+ if let Ok(factory) = v8::Local::<v8::Function>::try_from(factory_val) {
925
+ let recv = v8::undefined(&mut *tc).into();
926
+ // Pass t_obj and natives_obj
927
+ let args = [t_obj.into(), natives_obj.into()];
928
+
929
+ if let Some(exports_val) = factory.call(&mut *tc, recv, &args) {
930
+ // 4. Assign exports to t.<extName>
931
+ let mod_key = v8_str(&mut *tc, &module.name);
932
+ t_obj.set(&mut *tc, mod_key.into(), exports_val);
933
+
934
+ // println!(
935
+ // "{} {} {}",
936
+ // crate::utils::blue("[Titan]"),
937
+ // crate::utils::green("Injected extension:"),
938
+ // module.name
939
+ // );
940
+ } else {
941
+ // Execution error
942
+ if let Some(msg) = tc.message() {
943
+ let text = msg.get(&mut *tc).to_rust_string_lossy(&mut *tc);
944
+ println!(
945
+ "{} {} {} -> {}",
946
+ crate::utils::blue("[Titan]"),
947
+ crate::utils::red("Error running extension"),
948
+ module.name,
949
+ text
950
+ );
951
+ }
952
+ }
953
+ }
954
+ } else {
955
+ // Runtime error during script run
956
+ if let Some(msg) = tc.message() {
957
+ let text = msg.get(&mut *tc).to_rust_string_lossy(&mut *tc);
958
+ println!(
959
+ "{} {} {} -> {}",
960
+ crate::utils::blue("[Titan]"),
961
+ crate::utils::red("Error evaluating extension wrapper"),
962
+ module.name,
963
+ text
964
+ );
965
+ }
966
+ }
967
+ } else {
968
+ // Compile error
969
+ if let Some(msg) = tc.message() {
970
+ let text = msg.get(&mut *tc).to_rust_string_lossy(&mut *tc);
971
+ println!(
972
+ "{} {} {} -> {}",
973
+ crate::utils::blue("[Titan]"),
974
+ crate::utils::red("Syntax Error in extension"),
975
+ module.name,
976
+ text
977
+ );
978
+ }
979
+ }
980
+ }
981
+
982
+ // t.db (Stub for now)
983
+ let db_obj = v8::Object::new(scope);
984
+ let db_key = v8_str(scope, "db");
985
+ t_obj.set(scope, db_key.into(), db_obj.into());
986
+
987
+ let t_key = v8_str(scope, "t");
988
+ global.set(scope, t_key.into(), t_obj.into());
989
+ }