@ezetgalaxy/titan 26.10.1 → 26.10.3

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/index.js CHANGED
@@ -427,19 +427,15 @@ export function startProd() {
427
427
  const isWin = process.platform === "win32";
428
428
  const bin = isWin ? "titan-server.exe" : "titan-server";
429
429
  const root = process.cwd();
430
+ const serverDir = path.join(root, "server");
430
431
 
431
- const exe = path.join(root, "server", "target", "release", bin);
432
+ const exe = path.join(serverDir, "target", "release", bin);
432
433
 
433
434
  if (fs.existsSync(exe)) {
434
- execSync(`"${exe}"`, { stdio: "inherit" });
435
+ execSync(`"${exe}"`, { stdio: "inherit", cwd: serverDir });
435
436
  } else {
436
437
  // Fallback to pure node start if no rust binary
437
438
  const appJs = path.join(root, "app", "app.js");
438
- // Actually, typically we run the bundled/compiled app if we don't have rust server?
439
- // But wait, the pure TS template runs `node .titan/app.js` in Docker.
440
- // But locally `titan start` relies on `app/app.js` being compiled?
441
- // In `buildProd` above we compiled to `app/app.js`.
442
- // Let's check for `.titan/app.js` which is dev artifact? No, use the prod build artifact.
443
439
  execSync(`node "${appJs}"`, { stdio: "inherit" });
444
440
  }
445
441
  }
@@ -663,7 +659,7 @@ export function runExtension() {
663
659
  /* -------------------------------------------------------
664
660
  * ROUTER
665
661
  * ----------------------------------------------------- */
666
- const isMainModule = process.argv[1] === fileURLToPath(import.meta.url);
662
+ const isMainModule = fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
667
663
 
668
664
  if (isMainModule) {
669
665
  const args = process.argv.slice(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ezetgalaxy/titan",
3
- "version": "26.10.1",
3
+ "version": "26.10.3",
4
4
  "description": "Titan Planet is a JavaScript-first backend framework that embeds JS actions into a Rust + Axum server and ships as a single native binary. Routes are compiled to static metadata; only actions run in the embedded JS runtime. No Node.js. No event loop in production.",
5
5
  "license": "ISC",
6
6
  "author": "ezetgalaxy",
@@ -42,6 +42,8 @@
42
42
  "super-backend"
43
43
  ],
44
44
  "scripts": {
45
+ "cli-helper": "node ./scripts/cli-helper.mjs",
46
+ "create-ext": "titan create ext",
45
47
  "init": "rm -rf build && node index.js init build",
46
48
  "build": "cd build && node ../index.js build",
47
49
  "dev": "cd build && node ../index.js dev",
@@ -1,87 +1,132 @@
1
- /**
2
- * TITAN TYPE DEFINITIONS
3
- * ----------------------
4
- * These types are globally available in your Titan project.
5
- */
6
1
 
7
- /**
8
- * The Titan Request Object passed to actions.
9
- */
10
- declare interface TitanRequest {
11
- body: any;
12
- method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
13
- path: string;
14
- headers: {
15
- host?: string;
16
- "content-type"?: string;
17
- "user-agent"?: string;
18
- authorization?: string;
19
- [key: string]: string | undefined;
20
- };
21
- params: Record<string, string>;
22
- query: Record<string, string>;
2
+ // -- Module Definitions (for imports from "titan") --
3
+
4
+ export interface RouteHandler {
5
+ reply(value: any): void;
6
+ action(name: string): void;
23
7
  }
24
8
 
25
- interface DbConnection {
26
- /**
27
- * Execute a SQL query.
28
- * @param sql The SQL query string.
29
- * @param params (Optional) Parameters for the query ($1, $2, etc).
30
- */
31
- query(sql: string, params?: any[]): any[];
9
+ export interface TitanBuilder {
10
+ get(route: string): RouteHandler;
11
+ post(route: string): RouteHandler;
12
+ log(module: string, msg: string): void;
13
+ start(port?: number, msg?: string): Promise<void>;
32
14
  }
33
15
 
16
+ // The default export from titan.js is the Builder
17
+ declare const builder: TitanBuilder;
18
+ export const Titan: TitanBuilder;
19
+ export default builder;
20
+
34
21
  /**
35
22
  * Define a Titan Action with type inference.
36
- * @example
37
- * export const hello = defineAction((req) => {
38
- * return req.headers;
39
- * });
40
23
  */
41
- declare function defineAction<T>(actionFn: (req: TitanRequest) => T): (req: TitanRequest) => T;
24
+ export declare function defineAction<T>(actionFn: (req: TitanRequest) => T): (req: TitanRequest) => T;
25
+
26
+
27
+ // -- Global Definitions (Runtime Environment) --
28
+
29
+ declare global {
30
+ /**
31
+ * The Titan Request Object passed to actions.
32
+ */
33
+ interface TitanRequest {
34
+ body: any;
35
+ method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
36
+ path: string;
37
+ headers: {
38
+ host?: string;
39
+ "content-type"?: string;
40
+ "user-agent"?: string;
41
+ authorization?: string;
42
+ [key: string]: string | undefined;
43
+ };
44
+ params: Record<string, string>;
45
+ query: Record<string, string>;
46
+ }
47
+
48
+ interface DbConnection {
49
+ /**
50
+ * Execute a SQL query.
51
+ * @param sql The SQL query string.
52
+ * @param params (Optional) Parameters for the query ($1, $2, etc).
53
+ */
54
+ query(sql: string, params?: any[]): any[];
55
+ }
42
56
 
43
- /**
44
- * Titan Runtime Utilities
45
- */
46
- declare const t: {
47
57
  /**
48
- * Log messages to the server console with Titan formatting.
58
+ * Global defineAction (available without import in runtime)
49
59
  */
50
- log(...args: any[]): void;
60
+ function defineAction<T>(actionFn: (req: TitanRequest) => T): (req: TitanRequest) => T;
51
61
 
52
62
  /**
53
- * Read a file contents as string.
54
- * @param path Relative path to the file from project root.
63
+ * Global Request Object
64
+ * Available automatically in actions.
55
65
  */
56
- read(path: string): string;
57
-
58
- fetch(url: string, options?: {
59
- method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
60
- headers?: Record<string, string>;
61
- body?: string | object;
62
- }): {
63
- ok: boolean;
64
- status?: number;
65
- body?: string;
66
- error?: string;
67
- };
68
-
69
- jwt: {
70
- sign(
71
- payload: object,
72
- secret: string,
73
- options?: { expiresIn?: string | number }
74
- ): string;
75
- verify(token: string, secret: string): any;
76
- };
77
-
78
- password: {
79
- hash(password: string): string;
80
- verify(password: string, hash: string): boolean;
81
- };
82
-
83
- db: {
84
- connect(url: string): DbConnection;
85
- };
86
- };
66
+ var req: TitanRequest;
87
67
 
68
+ /**
69
+ * Titan Runtime Utilities
70
+ * (Available globally in the runtime, e.g. inside actions)
71
+ */
72
+ interface TitanRuntimeUtils {
73
+ /**
74
+ * Log messages to the server console with Titan formatting.
75
+ */
76
+ log(...args: any[]): void;
77
+
78
+ /**
79
+ * Read a file contents as string.
80
+ * @param path Relative path to the file from project root.
81
+ */
82
+ read(path: string): string;
83
+
84
+ fetch(url: string, options?: {
85
+ method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
86
+ headers?: Record<string, string>;
87
+ body?: string | object;
88
+ }): {
89
+ ok: boolean;
90
+ status?: number;
91
+ body?: string;
92
+ error?: string;
93
+ };
94
+
95
+ jwt: {
96
+ sign(
97
+ payload: object,
98
+ secret: string,
99
+ options?: { expiresIn?: string | number }
100
+ ): string;
101
+ verify(token: string, secret: string): any;
102
+ };
103
+
104
+ password: {
105
+ hash(password: string): string;
106
+ verify(password: string, hash: string): boolean;
107
+ };
108
+
109
+ db: {
110
+ connect(url: string): DbConnection;
111
+ };
112
+
113
+ /**
114
+ * Titan Validator (Zod-compatible)
115
+ */
116
+ valid: any;
117
+
118
+ // Allow extensions
119
+ [key: string]: any;
120
+ }
121
+
122
+ /**
123
+ * Titan Runtime Utilities
124
+ * (Available globally in the runtime, e.g. inside actions)
125
+ */
126
+ const t: TitanRuntimeUtils;
127
+
128
+ /**
129
+ * Titan Runtime Utilities (Alias for t)
130
+ */
131
+ const Titan: TitanRuntimeUtils;
132
+ }
@@ -1,104 +1,65 @@
1
- # 🪐 Titan Extension: {{name}}
1
+ # Titan Extension Template
2
2
 
3
- > Elevate Titan Planet with custom JavaScript and high-performance Native Rust logic.
3
+ This template provides a starting point for building native extensions for Titan.
4
4
 
5
- Welcome to your new Titan extension! This template provides everything you need to build, test, and deploy powerful additions to the Titan project.
5
+ ## Directory Structure
6
6
 
7
- ---
7
+ - `index.js`: The JavaScript entry point for your extension. It runs within the Titan runtime.
8
+ - `index.d.ts`: TypeScript definitions for your extension. This ensures users get autocompletion when using your extension.
9
+ - `native/`: (Optional) Rust source code for native high-performance logic.
10
+ - `titan.json`: Configuration file defining your extension's native ABI (if using Rust).
8
11
 
9
- ## 🛠 Project Structure
12
+ ## Type Definitions (`index.d.ts`)
10
13
 
11
- - `index.js`: The JavaScript entry point where you define your extension's API on the global `t` object.
12
- - `titan.json`: Manifest file defining extension metadata and Native module mappings.
13
- - `native/`: Directory for Rust source code.
14
- - `src/lib.rs`: Your Native function implementations.
15
- - `Cargo.toml`: Rust package and dependency configuration.
16
- - `jsconfig.json`: Enables full IntelliSense for the Titan Runtime API.
14
+ The `index.d.ts` file is crucial for Developer Experience (DX). It allows Titan projects to "see" your extension's API on the global `t` object.
17
15
 
18
- ---
16
+ ### How it works
19
17
 
20
- ## 🚀 Quick Start
18
+ Titan uses **Declaration Merging** to extend the global `Titan.Runtime` interface. When a user installs your extension, this file acts as a plugin to their TypeScript environment.
21
19
 
22
- ### 1. Install Dependencies
23
- Get full type support in your IDE:
24
- ```bash
25
- npm install
26
- ```
27
-
28
- ### 2. Build Native Module (Optional)
29
- If your extension uses Rust, compile it to a dynamic library:
30
- ```bash
31
- cd native
32
- cargo build --release
33
- cd ..
34
- ```
35
-
36
- ### 3. Test the Extension
37
- Use the Titan SDK to run a local test harness:
38
- ```bash
39
- titan run ext
40
- ```
41
- *Tip: Visit `http://localhost:3000/test` after starting the runner to see your extension in action!*
20
+ ### Customizing Types
42
21
 
43
- ---
22
+ Edit `index.d.ts` to match the API you expose in `index.js`.
44
23
 
45
- ## 💻 Development Guide
24
+ **Example:**
46
25
 
47
- ### Writing JavaScript
48
- Extensions interact with the global `t` object. It's best practice to namespace your extension:
26
+ If your `index.js` looks like this:
49
27
 
50
28
  ```javascript
51
- t.{{name}} = {
52
- myMethod: (val) => {
53
- t.log("{{name}}", "Doing something...");
54
- return val * 2;
55
- }
29
+ // index.js
30
+ t.ext.my_cool_ext = {
31
+ greet: (name) => `Hello, ${name}!`,
32
+ compute: (x) => x * 2
56
33
  };
57
34
  ```
58
35
 
59
- ### Writing Native Rust Functions
60
- Native functions should be marked with `#[unsafe(no_mangle)]` and use `extern "C"`:
61
-
62
- ```rust
63
- #[unsafe(no_mangle)]
64
- pub extern "C" fn multiply(a: f64, b: f64) -> f64 {
65
- a * b
66
- }
67
- ```
68
-
69
- ### Mapping Native Functions in `titan.json`
70
- Expose your Rust functions to JavaScript by adding them to the `native.functions` section:
71
-
72
- ```json
73
- "functions": {
74
- "add": {
75
- "symbol": "add",
76
- "parameters": ["f64", "f64"],
77
- "result": "f64"
36
+ Your `index.d.ts` should look like this:
37
+
38
+ ```typescript
39
+ // index.d.ts
40
+ declare global {
41
+ namespace Titan {
42
+ interface Runtime {
43
+ "my-cool-ext": {
44
+ /**
45
+ * Sends a greeting.
46
+ */
47
+ greet(name: string): string;
48
+
49
+ /**
50
+ * Computes a value.
51
+ */
52
+ compute(x: number): number;
53
+ }
54
+ }
78
55
  }
79
56
  }
57
+ export { };
80
58
  ```
81
59
 
82
- ---
83
-
84
- ## 🧪 Testing with Titan SDK
85
-
86
- The `titan run ext` command automates the testing workflow:
87
- 1. It builds your native code.
88
- 2. It sets up a temporary Titan project environment.
89
- 3. It links your extension into `node_modules`.
90
- 4. It starts the Titan Runtime at `http://localhost:3000`.
91
-
92
- You can modify the test harness or add custom test cases by exploring the generated `.titan_test_run` directory (it is git-ignored).
93
-
94
- ---
95
-
96
- ## 📦 Deployment
97
- To use your extension in a Titan project:
98
- 1. Publish your extension to npm or link it locally.
99
- 2. In your Titan project: `npm install my-extension`.
100
- 3. The Titan Runtime will automatically detect and load your extension if it contains a `titan.json`.
101
-
102
- ---
60
+ ## Native Bindings (Rust)
103
61
 
104
- Happy coding on Titan Planet! 🚀
62
+ If your extension requires native performance or system access, use the `native/` directory.
63
+ 1. Define functions in `native/src/lib.rs`.
64
+ 2. Map them in `titan.json`.
65
+ 3. Call them from `index.js` using `Titan.native.invoke(...)` (or the helper provided in the template).
@@ -0,0 +1,27 @@
1
+ // Type definitions for {{name}}
2
+ // This file facilitates type inference when this extension is installed in a Titan project.
3
+
4
+ declare global {
5
+ namespace Titan {
6
+ interface Runtime {
7
+ /**
8
+ * {{name}} Extension
9
+ */
10
+ "{{name}}": {
11
+ /**
12
+ * Example hello function
13
+ */
14
+ hello(name: string): string;
15
+
16
+ /**
17
+ * Example calc function (native wrapper)
18
+ */
19
+ calc(a: number, b: number): number;
20
+
21
+ // Add your extension methods here
22
+ }
23
+ }
24
+ }
25
+ }
26
+
27
+ export { };
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  // Define your extension Key
7
+ if (typeof Titan === "undefined") globalThis.Titan = t;
7
8
  const EXT_KEY = "{{name}}";
8
9
 
9
10
  t.log(EXT_KEY, "Extension loading...");
@@ -3,6 +3,7 @@
3
3
  "version": "1.0.0",
4
4
  "description": "A Titan Planet extension",
5
5
  "main": "index.js",
6
+ "types": "index.d.ts",
6
7
  "type": "commonjs",
7
8
  "scripts": {
8
9
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -19,4 +20,4 @@
19
20
  "esbuild": "^0.27.2",
20
21
  "titanpl-sdk": "latest"
21
22
  }
22
- }
23
+ }
@@ -14,6 +14,7 @@
14
14
  }
15
15
  },
16
16
  "include": [
17
- "app/**/*"
17
+ "app/**/*",
18
+ "titan/**/*"
18
19
  ]
19
20
  }
@@ -535,7 +535,13 @@ fn js_from_value<'a>(
535
535
  val: serde_json::Value,
536
536
  ) -> v8::Local<'a, v8::Value> {
537
537
  match ret_type {
538
- ReturnType::String => v8::String::new(scope, val.as_str().unwrap_or("")).unwrap().into(),
538
+ ReturnType::String => {
539
+ let s = match val.as_str() {
540
+ Some(x) => x,
541
+ None => "",
542
+ };
543
+ v8::String::new(scope, s).unwrap().into()
544
+ },
539
545
  ReturnType::F64 => v8::Number::new(scope, val.as_f64().unwrap_or(0.0)).into(),
540
546
  ReturnType::Bool => v8::Boolean::new(scope, val.as_bool().unwrap_or(false)).into(),
541
547
  ReturnType::Json => {
@@ -566,10 +572,32 @@ fn js_from_value<'a>(
566
572
  macro_rules! dispatch_ret {
567
573
  ($ptr:expr, $ret:expr, ($($arg_ty:ty),*), ($($arg:expr),*)) => {
568
574
  match $ret {
569
- ReturnType::String => { let f: extern "C" fn($($arg_ty),*) -> String; f = std::mem::transmute($ptr); serde_json::Value::String(f($($arg),*)) },
575
+ ReturnType::String => {
576
+ let f: extern "C" fn($($arg_ty),*) -> *mut std::os::raw::c_char;
577
+ f = std::mem::transmute($ptr);
578
+ let ptr = f($($arg),*);
579
+ if ptr.is_null() {
580
+ serde_json::Value::String(String::new())
581
+ } else {
582
+ let s = unsafe { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() };
583
+ // We leak the pointer here because we don't have a shared allocator/free function.
584
+ // This prevents the double-free/heap corruption crash.
585
+ serde_json::Value::String(s)
586
+ }
587
+ },
570
588
  ReturnType::F64 => { let f: extern "C" fn($($arg_ty),*) -> f64; f = std::mem::transmute($ptr); serde_json::json!(f($($arg),*)) },
571
589
  ReturnType::Bool => { let f: extern "C" fn($($arg_ty),*) -> bool; f = std::mem::transmute($ptr); serde_json::json!(f($($arg),*)) },
572
- ReturnType::Json => { let f: extern "C" fn($($arg_ty),*) -> serde_json::Value; f = std::mem::transmute($ptr); f($($arg),*) },
590
+ ReturnType::Json => {
591
+ let f: extern "C" fn($($arg_ty),*) -> *mut std::os::raw::c_char;
592
+ f = std::mem::transmute($ptr);
593
+ let ptr = f($($arg),*);
594
+ if ptr.is_null() {
595
+ serde_json::Value::Null
596
+ } else {
597
+ let s = unsafe { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() };
598
+ serde_json::from_str(&s).unwrap_or(serde_json::Value::Null)
599
+ }
600
+ },
573
601
  ReturnType::Buffer => { let f: extern "C" fn($($arg_ty),*) -> Vec<u8>; f = std::mem::transmute($ptr);
574
602
  let v = f($($arg),*);
575
603
  serde_json::Value::Array(v.into_iter().map(serde_json::Value::from).collect())
@@ -619,10 +647,18 @@ fn native_invoke_extension(scope: &mut v8::HandleScope, args: v8::FunctionCallba
619
647
  1 => {
620
648
  let v0 = vals.remove(0);
621
649
  match sig.params[0] {
622
- ParamType::String => { let a0 = v0.as_str().unwrap_or("").to_string(); dispatch_ret!(ptr, sig.ret, (String), (a0)) },
650
+ ParamType::String => {
651
+ let s = v0.as_str().unwrap_or("").to_string();
652
+ let c = std::ffi::CString::new(s).unwrap();
653
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char), (c.as_ptr()))
654
+ },
623
655
  ParamType::F64 => { let a0 = v0.as_f64().unwrap_or(0.0); dispatch_ret!(ptr, sig.ret, (f64), (a0)) },
624
656
  ParamType::Bool => { let a0 = v0.as_bool().unwrap_or(false); dispatch_ret!(ptr, sig.ret, (bool), (a0)) },
625
- ParamType::Json => { let a0 = v0; dispatch_ret!(ptr, sig.ret, (serde_json::Value), (a0)) },
657
+ ParamType::Json => {
658
+ let s = v0.to_string();
659
+ let c = std::ffi::CString::new(s).unwrap();
660
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char), (c.as_ptr()))
661
+ },
626
662
  ParamType::Buffer => {
627
663
  // Extract vec u8
628
664
  let a0: Vec<u8> = if let Some(arr) = v0.as_array() {
@@ -637,14 +673,17 @@ fn native_invoke_extension(scope: &mut v8::HandleScope, args: v8::FunctionCallba
637
673
  let v1 = vals.remove(0);
638
674
  match (sig.params[0].clone(), sig.params[1].clone()) { // Clone to satisfy borrow checker if needed
639
675
  (ParamType::String, ParamType::String) => {
640
- let a0 = v0.as_str().unwrap_or("").to_string();
641
- let a1 = v1.as_str().unwrap_or("").to_string();
642
- dispatch_ret!(ptr, sig.ret, (String, String), (a0, a1))
676
+ let s0 = v0.as_str().unwrap_or("").to_string();
677
+ let c0 = std::ffi::CString::new(s0).unwrap();
678
+ let s1 = v1.as_str().unwrap_or("").to_string();
679
+ let c1 = std::ffi::CString::new(s1).unwrap();
680
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char, *const std::os::raw::c_char), (c0.as_ptr(), c1.as_ptr()))
643
681
  },
644
682
  (ParamType::String, ParamType::F64) => {
645
- let a0 = v0.as_str().unwrap_or("").to_string();
683
+ let s0 = v0.as_str().unwrap_or("").to_string();
684
+ let c0 = std::ffi::CString::new(s0).unwrap();
646
685
  let a1 = v1.as_f64().unwrap_or(0.0);
647
- dispatch_ret!(ptr, sig.ret, (String, f64), (a0, a1))
686
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char, f64), (c0.as_ptr(), a1))
648
687
  },
649
688
  // Add more combinations as needed.
650
689
  _ => { println!("Unsupported 2-arg signature"); serde_json::Value::Null }
@@ -803,4 +842,4 @@ pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Obje
803
842
 
804
843
  let t_key = v8_str(scope, "t");
805
844
  global.set(scope, t_key.into(), t_obj.into());
806
- }
845
+ }
@@ -50,7 +50,7 @@ async function bundleJs() {
50
50
  target: "es2020",
51
51
  logLevel: "silent",
52
52
  banner: {
53
- js: "const defineAction = (fn) => fn;"
53
+ js: "const defineAction = (fn) => fn; const Titan = t;"
54
54
  },
55
55
 
56
56
  footer: {
@@ -119,4 +119,8 @@ const t = {
119
119
  };
120
120
 
121
121
 
122
+ /**
123
+ * Titan App Builder (Alias for t)
124
+ */
125
+ export const Titan = t;
122
126
  export default t;
@@ -14,6 +14,7 @@
14
14
  }
15
15
  },
16
16
  "include": [
17
- "app/**/*"
17
+ "app/**/*",
18
+ "titan/**/*"
18
19
  ]
19
20
  }
@@ -18,6 +18,15 @@ use std::sync::Mutex;
18
18
  use std::collections::HashMap;
19
19
  use std::fs;
20
20
 
21
+ // ----------------------------------------------------------------------------
22
+ // T MODULE (Rust API equivalent to JS t object)
23
+ // ----------------------------------------------------------------------------
24
+ pub mod t {
25
+ pub fn log(module: &str, msg: &str) {
26
+ println!("{} {}", crate::utils::blue("[Titan]"), crate::utils::gray(&format!("\x1b[90mlog({})\x1b[0m\x1b[97m: {}\x1b[0m", module, msg)));
27
+ }
28
+ }
29
+
21
30
  // ----------------------------------------------------------------------------
22
31
  // GLOBAL REGISTRY
23
32
  // ----------------------------------------------------------------------------
@@ -535,7 +544,13 @@ fn js_from_value<'a>(
535
544
  val: serde_json::Value,
536
545
  ) -> v8::Local<'a, v8::Value> {
537
546
  match ret_type {
538
- ReturnType::String => v8::String::new(scope, val.as_str().unwrap_or("")).unwrap().into(),
547
+ ReturnType::String => {
548
+ let s = match val.as_str() {
549
+ Some(x) => x,
550
+ None => "",
551
+ };
552
+ v8::String::new(scope, s).unwrap().into()
553
+ },
539
554
  ReturnType::F64 => v8::Number::new(scope, val.as_f64().unwrap_or(0.0)).into(),
540
555
  ReturnType::Bool => v8::Boolean::new(scope, val.as_bool().unwrap_or(false)).into(),
541
556
  ReturnType::Json => {
@@ -566,10 +581,32 @@ fn js_from_value<'a>(
566
581
  macro_rules! dispatch_ret {
567
582
  ($ptr:expr, $ret:expr, ($($arg_ty:ty),*), ($($arg:expr),*)) => {
568
583
  match $ret {
569
- ReturnType::String => { let f: extern "C" fn($($arg_ty),*) -> String; f = std::mem::transmute($ptr); serde_json::Value::String(f($($arg),*)) },
584
+ ReturnType::String => {
585
+ let f: extern "C" fn($($arg_ty),*) -> *mut std::os::raw::c_char;
586
+ f = std::mem::transmute($ptr);
587
+ let ptr = f($($arg),*);
588
+ if ptr.is_null() {
589
+ serde_json::Value::String(String::new())
590
+ } else {
591
+ let s = unsafe { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() };
592
+ // We leak the pointer here because we don't have a shared allocator/free function.
593
+ // This prevents the double-free/heap corruption crash.
594
+ serde_json::Value::String(s)
595
+ }
596
+ },
570
597
  ReturnType::F64 => { let f: extern "C" fn($($arg_ty),*) -> f64; f = std::mem::transmute($ptr); serde_json::json!(f($($arg),*)) },
571
598
  ReturnType::Bool => { let f: extern "C" fn($($arg_ty),*) -> bool; f = std::mem::transmute($ptr); serde_json::json!(f($($arg),*)) },
572
- ReturnType::Json => { let f: extern "C" fn($($arg_ty),*) -> serde_json::Value; f = std::mem::transmute($ptr); f($($arg),*) },
599
+ ReturnType::Json => {
600
+ let f: extern "C" fn($($arg_ty),*) -> *mut std::os::raw::c_char;
601
+ f = std::mem::transmute($ptr);
602
+ let ptr = f($($arg),*);
603
+ if ptr.is_null() {
604
+ serde_json::Value::Null
605
+ } else {
606
+ let s = unsafe { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() };
607
+ serde_json::from_str(&s).unwrap_or(serde_json::Value::Null)
608
+ }
609
+ },
573
610
  ReturnType::Buffer => { let f: extern "C" fn($($arg_ty),*) -> Vec<u8>; f = std::mem::transmute($ptr);
574
611
  let v = f($($arg),*);
575
612
  serde_json::Value::Array(v.into_iter().map(serde_json::Value::from).collect())
@@ -619,10 +656,18 @@ fn native_invoke_extension(scope: &mut v8::HandleScope, args: v8::FunctionCallba
619
656
  1 => {
620
657
  let v0 = vals.remove(0);
621
658
  match sig.params[0] {
622
- ParamType::String => { let a0 = v0.as_str().unwrap_or("").to_string(); dispatch_ret!(ptr, sig.ret, (String), (a0)) },
659
+ ParamType::String => {
660
+ let s = v0.as_str().unwrap_or("").to_string();
661
+ let c = std::ffi::CString::new(s).unwrap();
662
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char), (c.as_ptr()))
663
+ },
623
664
  ParamType::F64 => { let a0 = v0.as_f64().unwrap_or(0.0); dispatch_ret!(ptr, sig.ret, (f64), (a0)) },
624
665
  ParamType::Bool => { let a0 = v0.as_bool().unwrap_or(false); dispatch_ret!(ptr, sig.ret, (bool), (a0)) },
625
- ParamType::Json => { let a0 = v0; dispatch_ret!(ptr, sig.ret, (serde_json::Value), (a0)) },
666
+ ParamType::Json => {
667
+ let s = v0.to_string();
668
+ let c = std::ffi::CString::new(s).unwrap();
669
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char), (c.as_ptr()))
670
+ },
626
671
  ParamType::Buffer => {
627
672
  // Extract vec u8
628
673
  let a0: Vec<u8> = if let Some(arr) = v0.as_array() {
@@ -637,14 +682,17 @@ fn native_invoke_extension(scope: &mut v8::HandleScope, args: v8::FunctionCallba
637
682
  let v1 = vals.remove(0);
638
683
  match (sig.params[0].clone(), sig.params[1].clone()) { // Clone to satisfy borrow checker if needed
639
684
  (ParamType::String, ParamType::String) => {
640
- let a0 = v0.as_str().unwrap_or("").to_string();
641
- let a1 = v1.as_str().unwrap_or("").to_string();
642
- dispatch_ret!(ptr, sig.ret, (String, String), (a0, a1))
685
+ let s0 = v0.as_str().unwrap_or("").to_string();
686
+ let c0 = std::ffi::CString::new(s0).unwrap();
687
+ let s1 = v1.as_str().unwrap_or("").to_string();
688
+ let c1 = std::ffi::CString::new(s1).unwrap();
689
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char, *const std::os::raw::c_char), (c0.as_ptr(), c1.as_ptr()))
643
690
  },
644
691
  (ParamType::String, ParamType::F64) => {
645
- let a0 = v0.as_str().unwrap_or("").to_string();
692
+ let s0 = v0.as_str().unwrap_or("").to_string();
693
+ let c0 = std::ffi::CString::new(s0).unwrap();
646
694
  let a1 = v1.as_f64().unwrap_or(0.0);
647
- dispatch_ret!(ptr, sig.ret, (String, f64), (a0, a1))
695
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char, f64), (c0.as_ptr(), a1))
648
696
  },
649
697
  // Add more combinations as needed.
650
698
  _ => { println!("Unsupported 2-arg signature"); serde_json::Value::Null }
@@ -803,4 +851,4 @@ pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Obje
803
851
 
804
852
  let t_key = v8_str(scope, "t");
805
853
  global.set(scope, t_key.into(), t_obj.into());
806
- }
854
+ }
@@ -50,7 +50,7 @@ async function bundleJs() {
50
50
  target: "es2020",
51
51
  logLevel: "silent",
52
52
  banner: {
53
- js: "const defineAction = (fn) => fn;"
53
+ js: "const defineAction = (fn) => fn; const Titan = t;"
54
54
  },
55
55
 
56
56
  footer: {
@@ -119,4 +119,8 @@ const t = {
119
119
  };
120
120
 
121
121
 
122
+ /**
123
+ * Titan App Builder (Alias for t)
124
+ */
125
+ export const Titan = t;
122
126
  export default t;
@@ -18,6 +18,15 @@ use std::sync::Mutex;
18
18
  use std::collections::HashMap;
19
19
  use std::fs;
20
20
 
21
+ // ----------------------------------------------------------------------------
22
+ // T MODULE (Rust API equivalent to JS t object)
23
+ // ----------------------------------------------------------------------------
24
+ pub mod t {
25
+ pub fn log(module: &str, msg: &str) {
26
+ println!("{} {}", crate::utils::blue("[Titan]"), crate::utils::gray(&format!("\x1b[90mlog({})\x1b[0m\x1b[97m: {}\x1b[0m", module, msg)));
27
+ }
28
+ }
29
+
21
30
  // ----------------------------------------------------------------------------
22
31
  // GLOBAL REGISTRY
23
32
  // ----------------------------------------------------------------------------
@@ -535,7 +544,13 @@ fn js_from_value<'a>(
535
544
  val: serde_json::Value,
536
545
  ) -> v8::Local<'a, v8::Value> {
537
546
  match ret_type {
538
- ReturnType::String => v8::String::new(scope, val.as_str().unwrap_or("")).unwrap().into(),
547
+ ReturnType::String => {
548
+ let s = match val.as_str() {
549
+ Some(x) => x,
550
+ None => "",
551
+ };
552
+ v8::String::new(scope, s).unwrap().into()
553
+ },
539
554
  ReturnType::F64 => v8::Number::new(scope, val.as_f64().unwrap_or(0.0)).into(),
540
555
  ReturnType::Bool => v8::Boolean::new(scope, val.as_bool().unwrap_or(false)).into(),
541
556
  ReturnType::Json => {
@@ -566,10 +581,32 @@ fn js_from_value<'a>(
566
581
  macro_rules! dispatch_ret {
567
582
  ($ptr:expr, $ret:expr, ($($arg_ty:ty),*), ($($arg:expr),*)) => {
568
583
  match $ret {
569
- ReturnType::String => { let f: extern "C" fn($($arg_ty),*) -> String; f = std::mem::transmute($ptr); serde_json::Value::String(f($($arg),*)) },
584
+ ReturnType::String => {
585
+ let f: extern "C" fn($($arg_ty),*) -> *mut std::os::raw::c_char;
586
+ f = std::mem::transmute($ptr);
587
+ let ptr = f($($arg),*);
588
+ if ptr.is_null() {
589
+ serde_json::Value::String(String::new())
590
+ } else {
591
+ let s = unsafe { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() };
592
+ // We leak the pointer here because we don't have a shared allocator/free function.
593
+ // This prevents the double-free/heap corruption crash.
594
+ serde_json::Value::String(s)
595
+ }
596
+ },
570
597
  ReturnType::F64 => { let f: extern "C" fn($($arg_ty),*) -> f64; f = std::mem::transmute($ptr); serde_json::json!(f($($arg),*)) },
571
598
  ReturnType::Bool => { let f: extern "C" fn($($arg_ty),*) -> bool; f = std::mem::transmute($ptr); serde_json::json!(f($($arg),*)) },
572
- ReturnType::Json => { let f: extern "C" fn($($arg_ty),*) -> serde_json::Value; f = std::mem::transmute($ptr); f($($arg),*) },
599
+ ReturnType::Json => {
600
+ let f: extern "C" fn($($arg_ty),*) -> *mut std::os::raw::c_char;
601
+ f = std::mem::transmute($ptr);
602
+ let ptr = f($($arg),*);
603
+ if ptr.is_null() {
604
+ serde_json::Value::Null
605
+ } else {
606
+ let s = unsafe { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() };
607
+ serde_json::from_str(&s).unwrap_or(serde_json::Value::Null)
608
+ }
609
+ },
573
610
  ReturnType::Buffer => { let f: extern "C" fn($($arg_ty),*) -> Vec<u8>; f = std::mem::transmute($ptr);
574
611
  let v = f($($arg),*);
575
612
  serde_json::Value::Array(v.into_iter().map(serde_json::Value::from).collect())
@@ -619,10 +656,18 @@ fn native_invoke_extension(scope: &mut v8::HandleScope, args: v8::FunctionCallba
619
656
  1 => {
620
657
  let v0 = vals.remove(0);
621
658
  match sig.params[0] {
622
- ParamType::String => { let a0 = v0.as_str().unwrap_or("").to_string(); dispatch_ret!(ptr, sig.ret, (String), (a0)) },
659
+ ParamType::String => {
660
+ let s = v0.as_str().unwrap_or("").to_string();
661
+ let c = std::ffi::CString::new(s).unwrap();
662
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char), (c.as_ptr()))
663
+ },
623
664
  ParamType::F64 => { let a0 = v0.as_f64().unwrap_or(0.0); dispatch_ret!(ptr, sig.ret, (f64), (a0)) },
624
665
  ParamType::Bool => { let a0 = v0.as_bool().unwrap_or(false); dispatch_ret!(ptr, sig.ret, (bool), (a0)) },
625
- ParamType::Json => { let a0 = v0; dispatch_ret!(ptr, sig.ret, (serde_json::Value), (a0)) },
666
+ ParamType::Json => {
667
+ let s = v0.to_string();
668
+ let c = std::ffi::CString::new(s).unwrap();
669
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char), (c.as_ptr()))
670
+ },
626
671
  ParamType::Buffer => {
627
672
  // Extract vec u8
628
673
  let a0: Vec<u8> = if let Some(arr) = v0.as_array() {
@@ -637,14 +682,17 @@ fn native_invoke_extension(scope: &mut v8::HandleScope, args: v8::FunctionCallba
637
682
  let v1 = vals.remove(0);
638
683
  match (sig.params[0].clone(), sig.params[1].clone()) { // Clone to satisfy borrow checker if needed
639
684
  (ParamType::String, ParamType::String) => {
640
- let a0 = v0.as_str().unwrap_or("").to_string();
641
- let a1 = v1.as_str().unwrap_or("").to_string();
642
- dispatch_ret!(ptr, sig.ret, (String, String), (a0, a1))
685
+ let s0 = v0.as_str().unwrap_or("").to_string();
686
+ let c0 = std::ffi::CString::new(s0).unwrap();
687
+ let s1 = v1.as_str().unwrap_or("").to_string();
688
+ let c1 = std::ffi::CString::new(s1).unwrap();
689
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char, *const std::os::raw::c_char), (c0.as_ptr(), c1.as_ptr()))
643
690
  },
644
691
  (ParamType::String, ParamType::F64) => {
645
- let a0 = v0.as_str().unwrap_or("").to_string();
692
+ let s0 = v0.as_str().unwrap_or("").to_string();
693
+ let c0 = std::ffi::CString::new(s0).unwrap();
646
694
  let a1 = v1.as_f64().unwrap_or(0.0);
647
- dispatch_ret!(ptr, sig.ret, (String, f64), (a0, a1))
695
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char, f64), (c0.as_ptr(), a1))
648
696
  },
649
697
  // Add more combinations as needed.
650
698
  _ => { println!("Unsupported 2-arg signature"); serde_json::Value::Null }
@@ -803,4 +851,4 @@ pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Obje
803
851
 
804
852
  let t_key = v8_str(scope, "t");
805
853
  global.set(scope, t_key.into(), t_obj.into());
806
- }
854
+ }
@@ -50,7 +50,7 @@ async function bundleJs(actionsDir, outDir) {
50
50
  target: "es2020",
51
51
  logLevel: "silent",
52
52
  banner: {
53
- js: "const defineAction = (fn) => fn;"
53
+ js: "const defineAction = (fn) => fn; const Titan = t;"
54
54
  },
55
55
 
56
56
  footer: {
@@ -15,6 +15,7 @@ export interface TitanBuilder {
15
15
 
16
16
  // The default export from titan.js is the Builder
17
17
  declare const builder: TitanBuilder;
18
+ export const Titan: TitanBuilder;
18
19
  export default builder;
19
20
 
20
21
  /**
@@ -68,7 +69,7 @@ declare global {
68
69
  * Titan Runtime Utilities
69
70
  * (Available globally in the runtime, e.g. inside actions)
70
71
  */
71
- const t: {
72
+ interface TitanRuntimeUtils {
72
73
  /**
73
74
  * Log messages to the server console with Titan formatting.
74
75
  */
@@ -113,5 +114,16 @@ declare global {
113
114
  * Titan Validator (Zod-compatible)
114
115
  */
115
116
  valid: any;
116
- };
117
+ }
118
+
119
+ /**
120
+ * Titan Runtime Utilities
121
+ * (Available globally in the runtime, e.g. inside actions)
122
+ */
123
+ const t: TitanRuntimeUtils;
124
+
125
+ /**
126
+ * Titan Runtime Utilities (Alias for t)
127
+ */
128
+ const Titan: TitanRuntimeUtils;
117
129
  }
@@ -119,4 +119,8 @@ const t = {
119
119
  }
120
120
  };
121
121
 
122
+ /**
123
+ * Titan App Builder (Alias for t)
124
+ */
125
+ export const Titan = t;
122
126
  export default t;
@@ -535,7 +535,13 @@ fn js_from_value<'a>(
535
535
  val: serde_json::Value,
536
536
  ) -> v8::Local<'a, v8::Value> {
537
537
  match ret_type {
538
- ReturnType::String => v8::String::new(scope, val.as_str().unwrap_or("")).unwrap().into(),
538
+ ReturnType::String => {
539
+ let s = match val.as_str() {
540
+ Some(x) => x,
541
+ None => "",
542
+ };
543
+ v8::String::new(scope, s).unwrap().into()
544
+ },
539
545
  ReturnType::F64 => v8::Number::new(scope, val.as_f64().unwrap_or(0.0)).into(),
540
546
  ReturnType::Bool => v8::Boolean::new(scope, val.as_bool().unwrap_or(false)).into(),
541
547
  ReturnType::Json => {
@@ -566,10 +572,32 @@ fn js_from_value<'a>(
566
572
  macro_rules! dispatch_ret {
567
573
  ($ptr:expr, $ret:expr, ($($arg_ty:ty),*), ($($arg:expr),*)) => {
568
574
  match $ret {
569
- ReturnType::String => { let f: extern "C" fn($($arg_ty),*) -> String; f = std::mem::transmute($ptr); serde_json::Value::String(f($($arg),*)) },
575
+ ReturnType::String => {
576
+ let f: extern "C" fn($($arg_ty),*) -> *mut std::os::raw::c_char;
577
+ f = std::mem::transmute($ptr);
578
+ let ptr = f($($arg),*);
579
+ if ptr.is_null() {
580
+ serde_json::Value::String(String::new())
581
+ } else {
582
+ let s = unsafe { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() };
583
+ // We leak the pointer here because we don't have a shared allocator/free function.
584
+ // This prevents the double-free/heap corruption crash.
585
+ serde_json::Value::String(s)
586
+ }
587
+ },
570
588
  ReturnType::F64 => { let f: extern "C" fn($($arg_ty),*) -> f64; f = std::mem::transmute($ptr); serde_json::json!(f($($arg),*)) },
571
589
  ReturnType::Bool => { let f: extern "C" fn($($arg_ty),*) -> bool; f = std::mem::transmute($ptr); serde_json::json!(f($($arg),*)) },
572
- ReturnType::Json => { let f: extern "C" fn($($arg_ty),*) -> serde_json::Value; f = std::mem::transmute($ptr); f($($arg),*) },
590
+ ReturnType::Json => {
591
+ let f: extern "C" fn($($arg_ty),*) -> *mut std::os::raw::c_char;
592
+ f = std::mem::transmute($ptr);
593
+ let ptr = f($($arg),*);
594
+ if ptr.is_null() {
595
+ serde_json::Value::Null
596
+ } else {
597
+ let s = unsafe { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() };
598
+ serde_json::from_str(&s).unwrap_or(serde_json::Value::Null)
599
+ }
600
+ },
573
601
  ReturnType::Buffer => { let f: extern "C" fn($($arg_ty),*) -> Vec<u8>; f = std::mem::transmute($ptr);
574
602
  let v = f($($arg),*);
575
603
  serde_json::Value::Array(v.into_iter().map(serde_json::Value::from).collect())
@@ -619,10 +647,18 @@ fn native_invoke_extension(scope: &mut v8::HandleScope, args: v8::FunctionCallba
619
647
  1 => {
620
648
  let v0 = vals.remove(0);
621
649
  match sig.params[0] {
622
- ParamType::String => { let a0 = v0.as_str().unwrap_or("").to_string(); dispatch_ret!(ptr, sig.ret, (String), (a0)) },
650
+ ParamType::String => {
651
+ let s = v0.as_str().unwrap_or("").to_string();
652
+ let c = std::ffi::CString::new(s).unwrap();
653
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char), (c.as_ptr()))
654
+ },
623
655
  ParamType::F64 => { let a0 = v0.as_f64().unwrap_or(0.0); dispatch_ret!(ptr, sig.ret, (f64), (a0)) },
624
656
  ParamType::Bool => { let a0 = v0.as_bool().unwrap_or(false); dispatch_ret!(ptr, sig.ret, (bool), (a0)) },
625
- ParamType::Json => { let a0 = v0; dispatch_ret!(ptr, sig.ret, (serde_json::Value), (a0)) },
657
+ ParamType::Json => {
658
+ let s = v0.to_string();
659
+ let c = std::ffi::CString::new(s).unwrap();
660
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char), (c.as_ptr()))
661
+ },
626
662
  ParamType::Buffer => {
627
663
  // Extract vec u8
628
664
  let a0: Vec<u8> = if let Some(arr) = v0.as_array() {
@@ -637,14 +673,17 @@ fn native_invoke_extension(scope: &mut v8::HandleScope, args: v8::FunctionCallba
637
673
  let v1 = vals.remove(0);
638
674
  match (sig.params[0].clone(), sig.params[1].clone()) { // Clone to satisfy borrow checker if needed
639
675
  (ParamType::String, ParamType::String) => {
640
- let a0 = v0.as_str().unwrap_or("").to_string();
641
- let a1 = v1.as_str().unwrap_or("").to_string();
642
- dispatch_ret!(ptr, sig.ret, (String, String), (a0, a1))
676
+ let s0 = v0.as_str().unwrap_or("").to_string();
677
+ let c0 = std::ffi::CString::new(s0).unwrap();
678
+ let s1 = v1.as_str().unwrap_or("").to_string();
679
+ let c1 = std::ffi::CString::new(s1).unwrap();
680
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char, *const std::os::raw::c_char), (c0.as_ptr(), c1.as_ptr()))
643
681
  },
644
682
  (ParamType::String, ParamType::F64) => {
645
- let a0 = v0.as_str().unwrap_or("").to_string();
683
+ let s0 = v0.as_str().unwrap_or("").to_string();
684
+ let c0 = std::ffi::CString::new(s0).unwrap();
646
685
  let a1 = v1.as_f64().unwrap_or(0.0);
647
- dispatch_ret!(ptr, sig.ret, (String, f64), (a0, a1))
686
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char, f64), (c0.as_ptr(), a1))
648
687
  },
649
688
  // Add more combinations as needed.
650
689
  _ => { println!("Unsupported 2-arg signature"); serde_json::Value::Null }
@@ -803,4 +842,4 @@ pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Obje
803
842
 
804
843
  let t_key = v8_str(scope, "t");
805
844
  global.set(scope, t_key.into(), t_obj.into());
806
- }
845
+ }
@@ -48,7 +48,7 @@ async function bundleJs(actionsDir, outDir) {
48
48
  target: "es2020",
49
49
  logLevel: "silent",
50
50
  banner: {
51
- js: "const defineAction = (fn) => fn;"
51
+ js: "const defineAction = (fn) => fn; const Titan = t;"
52
52
  },
53
53
 
54
54
  footer: {
@@ -15,6 +15,7 @@ export interface TitanBuilder {
15
15
 
16
16
  // The default export from titan.js is the Builder
17
17
  declare const builder: TitanBuilder;
18
+ export const Titan: TitanBuilder;
18
19
  export default builder;
19
20
 
20
21
  /**
@@ -68,7 +69,7 @@ declare global {
68
69
  * Titan Runtime Utilities
69
70
  * (Available globally in the runtime, e.g. inside actions)
70
71
  */
71
- const t: {
72
+ interface TitanRuntimeUtils {
72
73
  /**
73
74
  * Log messages to the server console with Titan formatting.
74
75
  */
@@ -113,5 +114,16 @@ declare global {
113
114
  * Titan Validator (Zod-compatible)
114
115
  */
115
116
  valid: any;
116
- };
117
+ }
118
+
119
+ /**
120
+ * Titan Runtime Utilities
121
+ * (Available globally in the runtime, e.g. inside actions)
122
+ */
123
+ const t: TitanRuntimeUtils;
124
+
125
+ /**
126
+ * Titan Runtime Utilities (Alias for t)
127
+ */
128
+ const Titan: TitanRuntimeUtils;
117
129
  }
@@ -119,4 +119,5 @@ const t = {
119
119
  }
120
120
  };
121
121
 
122
+ export const Titan = t;
122
123
  export default t;
@@ -5,6 +5,10 @@ declare global {
5
5
  * Titan Runtime Global Object
6
6
  */
7
7
  const t: Titan.Runtime;
8
+ /**
9
+ * Titan Runtime Global Object
10
+ */
11
+ const Titan: Titan.Runtime;
8
12
  }
9
13
 
10
14
  export namespace Titan {
@@ -18,7 +22,7 @@ export namespace Titan {
18
22
  * Read file content
19
23
  */
20
24
  read(path: string): string;
21
-
25
+
22
26
  /**
23
27
  * Fetch API wrapper
24
28
  */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "titanpl-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",
@@ -535,7 +535,13 @@ fn js_from_value<'a>(
535
535
  val: serde_json::Value,
536
536
  ) -> v8::Local<'a, v8::Value> {
537
537
  match ret_type {
538
- ReturnType::String => v8::String::new(scope, val.as_str().unwrap_or("")).unwrap().into(),
538
+ ReturnType::String => {
539
+ let s = match val.as_str() {
540
+ Some(x) => x,
541
+ None => "",
542
+ };
543
+ v8::String::new(scope, s).unwrap().into()
544
+ },
539
545
  ReturnType::F64 => v8::Number::new(scope, val.as_f64().unwrap_or(0.0)).into(),
540
546
  ReturnType::Bool => v8::Boolean::new(scope, val.as_bool().unwrap_or(false)).into(),
541
547
  ReturnType::Json => {
@@ -566,10 +572,32 @@ fn js_from_value<'a>(
566
572
  macro_rules! dispatch_ret {
567
573
  ($ptr:expr, $ret:expr, ($($arg_ty:ty),*), ($($arg:expr),*)) => {
568
574
  match $ret {
569
- ReturnType::String => { let f: extern "C" fn($($arg_ty),*) -> String; f = std::mem::transmute($ptr); serde_json::Value::String(f($($arg),*)) },
575
+ ReturnType::String => {
576
+ let f: extern "C" fn($($arg_ty),*) -> *mut std::os::raw::c_char;
577
+ f = std::mem::transmute($ptr);
578
+ let ptr = f($($arg),*);
579
+ if ptr.is_null() {
580
+ serde_json::Value::String(String::new())
581
+ } else {
582
+ let s = unsafe { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() };
583
+ // We leak the pointer here because we don't have a shared allocator/free function.
584
+ // This prevents the double-free/heap corruption crash.
585
+ serde_json::Value::String(s)
586
+ }
587
+ },
570
588
  ReturnType::F64 => { let f: extern "C" fn($($arg_ty),*) -> f64; f = std::mem::transmute($ptr); serde_json::json!(f($($arg),*)) },
571
589
  ReturnType::Bool => { let f: extern "C" fn($($arg_ty),*) -> bool; f = std::mem::transmute($ptr); serde_json::json!(f($($arg),*)) },
572
- ReturnType::Json => { let f: extern "C" fn($($arg_ty),*) -> serde_json::Value; f = std::mem::transmute($ptr); f($($arg),*) },
590
+ ReturnType::Json => {
591
+ let f: extern "C" fn($($arg_ty),*) -> *mut std::os::raw::c_char;
592
+ f = std::mem::transmute($ptr);
593
+ let ptr = f($($arg),*);
594
+ if ptr.is_null() {
595
+ serde_json::Value::Null
596
+ } else {
597
+ let s = unsafe { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() };
598
+ serde_json::from_str(&s).unwrap_or(serde_json::Value::Null)
599
+ }
600
+ },
573
601
  ReturnType::Buffer => { let f: extern "C" fn($($arg_ty),*) -> Vec<u8>; f = std::mem::transmute($ptr);
574
602
  let v = f($($arg),*);
575
603
  serde_json::Value::Array(v.into_iter().map(serde_json::Value::from).collect())
@@ -619,10 +647,18 @@ fn native_invoke_extension(scope: &mut v8::HandleScope, args: v8::FunctionCallba
619
647
  1 => {
620
648
  let v0 = vals.remove(0);
621
649
  match sig.params[0] {
622
- ParamType::String => { let a0 = v0.as_str().unwrap_or("").to_string(); dispatch_ret!(ptr, sig.ret, (String), (a0)) },
650
+ ParamType::String => {
651
+ let s = v0.as_str().unwrap_or("").to_string();
652
+ let c = std::ffi::CString::new(s).unwrap();
653
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char), (c.as_ptr()))
654
+ },
623
655
  ParamType::F64 => { let a0 = v0.as_f64().unwrap_or(0.0); dispatch_ret!(ptr, sig.ret, (f64), (a0)) },
624
656
  ParamType::Bool => { let a0 = v0.as_bool().unwrap_or(false); dispatch_ret!(ptr, sig.ret, (bool), (a0)) },
625
- ParamType::Json => { let a0 = v0; dispatch_ret!(ptr, sig.ret, (serde_json::Value), (a0)) },
657
+ ParamType::Json => {
658
+ let s = v0.to_string();
659
+ let c = std::ffi::CString::new(s).unwrap();
660
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char), (c.as_ptr()))
661
+ },
626
662
  ParamType::Buffer => {
627
663
  // Extract vec u8
628
664
  let a0: Vec<u8> = if let Some(arr) = v0.as_array() {
@@ -637,14 +673,17 @@ fn native_invoke_extension(scope: &mut v8::HandleScope, args: v8::FunctionCallba
637
673
  let v1 = vals.remove(0);
638
674
  match (sig.params[0].clone(), sig.params[1].clone()) { // Clone to satisfy borrow checker if needed
639
675
  (ParamType::String, ParamType::String) => {
640
- let a0 = v0.as_str().unwrap_or("").to_string();
641
- let a1 = v1.as_str().unwrap_or("").to_string();
642
- dispatch_ret!(ptr, sig.ret, (String, String), (a0, a1))
676
+ let s0 = v0.as_str().unwrap_or("").to_string();
677
+ let c0 = std::ffi::CString::new(s0).unwrap();
678
+ let s1 = v1.as_str().unwrap_or("").to_string();
679
+ let c1 = std::ffi::CString::new(s1).unwrap();
680
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char, *const std::os::raw::c_char), (c0.as_ptr(), c1.as_ptr()))
643
681
  },
644
682
  (ParamType::String, ParamType::F64) => {
645
- let a0 = v0.as_str().unwrap_or("").to_string();
683
+ let s0 = v0.as_str().unwrap_or("").to_string();
684
+ let c0 = std::ffi::CString::new(s0).unwrap();
646
685
  let a1 = v1.as_f64().unwrap_or(0.0);
647
- dispatch_ret!(ptr, sig.ret, (String, f64), (a0, a1))
686
+ dispatch_ret!(ptr, sig.ret, (*const std::os::raw::c_char, f64), (c0.as_ptr(), a1))
648
687
  },
649
688
  // Add more combinations as needed.
650
689
  _ => { println!("Unsupported 2-arg signature"); serde_json::Value::Null }
@@ -803,4 +842,4 @@ pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Obje
803
842
 
804
843
  let t_key = v8_str(scope, "t");
805
844
  global.set(scope, t_key.into(), t_obj.into());
806
- }
845
+ }
@@ -1,3 +0,0 @@
1
- node_modules
2
- npm-debug.log
3
- .git
@@ -1,53 +0,0 @@
1
- # ================================================================
2
- # STAGE 1 — Build Titan (JS → Rust)
3
- # ================================================================
4
- FROM rust:1.91.1 AS builder
5
-
6
- # Install Node for Titan CLI + bundler
7
- RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
8
- && apt-get install -y nodejs
9
-
10
- # Install Titan CLI (latest)
11
- RUN npm install -g @ezetgalaxy/titan@latest
12
-
13
- WORKDIR /app
14
-
15
- # Copy project files
16
- COPY . .
17
-
18
- # Install JS dependencies (needed for Titan DSL + bundler)
19
- RUN npm install
20
-
21
- # Build Titan metadata + bundle JS actions
22
- RUN titan build
23
-
24
- # Build Rust binary
25
- RUN cd server && cargo build --release
26
-
27
-
28
-
29
- # ================================================================
30
- # STAGE 2 — Runtime Image (Lightweight)
31
- # ================================================================
32
- FROM debian:stable-slim
33
-
34
- WORKDIR /app
35
-
36
- # Copy Rust binary from builder stage
37
- COPY --from=builder /app/server/target/release/server ./titan-server
38
-
39
- # Copy Titan routing metadata
40
- COPY --from=builder /app/server/routes.json ./routes.json
41
- COPY --from=builder /app/server/action_map.json ./action_map.json
42
-
43
- # Copy Titan JS bundles
44
- RUN mkdir -p /app/actions
45
- COPY --from=builder /app/server/actions /app/actions
46
-
47
- COPY --from=builder /app/db /app/assets
48
-
49
- # Expose Titan port
50
- EXPOSE 3000
51
-
52
- # Start Titan
53
- CMD ["./titan-server"]