@ezetgalaxy/titan 26.11.0 → 26.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -10
- package/package.json +1 -1
- package/templates/js/server/Cargo.toml +2 -0
- package/templates/js/server/src/extensions/builtin.rs +433 -0
- package/templates/js/server/src/extensions/external.rs +309 -0
- package/templates/js/server/src/extensions/mod.rs +242 -0
- package/templates/js/server/src/extensions/titan_core.js +17 -0
- package/templates/js/server/src/main.rs +1 -2
- package/templates/js/server/src/runtime.rs +46 -20
- package/templates/js/titan/dev.js +39 -2
- package/templates/rust-js/titan/dev.js +39 -6
- package/templates/rust-ts/titan/dev.js +39 -6
- package/templates/ts/server/Cargo.toml +2 -0
- package/templates/ts/server/src/errors.rs +9 -50
- package/templates/ts/server/src/extensions/builtin.rs +433 -0
- package/templates/ts/server/src/extensions/external.rs +309 -0
- package/templates/ts/server/src/extensions/mod.rs +242 -0
- package/templates/ts/server/src/extensions/titan_core.js +17 -0
- package/templates/ts/server/src/main.rs +1 -2
- package/templates/ts/server/src/runtime.rs +46 -20
- package/templates/ts/titan/dev.js +40 -6
- package/titanpl-sdk/package.json +1 -1
- package/titanpl-sdk/templates/server/Cargo.toml +2 -0
- package/titanpl-sdk/templates/server/src/extensions/builtin.rs +433 -0
- package/titanpl-sdk/templates/server/src/extensions/external.rs +309 -0
- package/titanpl-sdk/templates/server/src/extensions/mod.rs +242 -0
- package/titanpl-sdk/templates/server/src/extensions/titan_core.js +17 -0
- package/titanpl-sdk/templates/server/src/main.rs +1 -2
- package/titanpl-sdk/templates/server/src/runtime.rs +46 -20
- package/titanpl-sdk/templates/titan/dev.js +36 -0
- package/templates/js/server/Cargo.lock +0 -2869
- package/templates/js/server/src/extensions.rs +0 -1161
- package/templates/ts/server/Cargo.lock +0 -2869
- package/templates/ts/server/src/extensions.rs +0 -1161
- package/titanpl-sdk/templates/server/Cargo.lock +0 -2839
- package/titanpl-sdk/templates/server/src/extensions.rs +0 -1161
package/README.md
CHANGED
|
@@ -222,16 +222,97 @@ Titan compiles your entire app—JS/TS code, Rust code, and server logic—into
|
|
|
222
222
|
|
|
223
223
|
---
|
|
224
224
|
|
|
225
|
-
# 🧱 Architecture
|
|
226
|
-
Titan is **not** a Node.js framework. It is a Rust server that speaks JavaScript/TypeScript.
|
|
227
|
-
* **No Event Loop** for JS (Request/Response model).
|
|
228
|
-
* **No `require`** (Use raw imports or bundled dependencies).
|
|
229
|
-
* **True Isolation** per request.
|
|
225
|
+
# 🧱 Architecture: Strictly Synchronous V8 Runtime
|
|
230
226
|
|
|
231
|
-
|
|
227
|
+
Titan is **not** a Node.js framework. It is a **Rust server with embedded V8 engines** that executes JavaScript/TypeScript **synchronously**.
|
|
228
|
+
|
|
229
|
+
### Key Architectural Principles:
|
|
230
|
+
|
|
231
|
+
* **No Event Loop in Workers**: Unlike Node.js, Titan workers do **not** run an event loop. Code executes synchronously from request entry to response exit.
|
|
232
|
+
* **Request-Driven Execution**: Each worker processes one request at a time, blocks until completion, then awaits the next request.
|
|
233
|
+
* **Blocking I/O**: All I/O operations (HTTP, DB, file system) block the worker thread. Scaling is achieved by increasing worker threads, not through async I/O.
|
|
234
|
+
* **Deterministic Execution**: All code runs linearly, making debugging predictable and straightforward.
|
|
235
|
+
* **True Isolation**: Each worker owns an independent V8 isolate with zero shared state or cross-worker communication.
|
|
236
|
+
* **No `require` or `import.meta`**: Use ES6 imports only. Dependencies are bundled with esbuild.
|
|
237
|
+
* **No Async/Await**: JavaScript actions cannot use Promises, `async/await`, `setTimeout`, or any other asynchronous primitives.
|
|
238
|
+
|
|
239
|
+
### Synchronous Execution Model:
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
243
|
+
│ Incoming HTTP Request │
|
|
244
|
+
└─────────────────────┬───────────────────────────────────────────┘
|
|
245
|
+
│
|
|
246
|
+
▼
|
|
247
|
+
┌────────────────────────┐
|
|
248
|
+
│ Axum HTTP Server │
|
|
249
|
+
│ (Rust, async) │
|
|
250
|
+
└────────┬───────────────┘
|
|
251
|
+
│
|
|
252
|
+
│ Dispatch to Worker
|
|
253
|
+
▼
|
|
254
|
+
┌──────────────────────────────────┐
|
|
255
|
+
│ Worker Thread (Rust) │
|
|
256
|
+
│ ┌────────────────────────────┐ │
|
|
257
|
+
│ │ V8 Isolate │ │
|
|
258
|
+
│ │ ┌──────────────────────┐ │ │
|
|
259
|
+
│ │ │ Execute Action │ │ │ ◄── Synchronous, blocking execution
|
|
260
|
+
│ │ │ (JavaScript/TS) │ │ │
|
|
261
|
+
│ │ │ │ │ │
|
|
262
|
+
│ │ │ t.fetch() ──────────┼──┼──┼──► Blocks until HTTP response
|
|
263
|
+
│ │ │ │ │ │ │
|
|
264
|
+
│ │ │ ▼ │ │ │
|
|
265
|
+
│ │ │ return { ... } │ │ │
|
|
266
|
+
│ │ └──────────────────────┘ │ │
|
|
267
|
+
│ └────────────────────────────┘ │
|
|
268
|
+
└──────────┬───────────────────────┘
|
|
269
|
+
│
|
|
270
|
+
│ Return response
|
|
271
|
+
▼
|
|
272
|
+
┌─────────────────────────┐
|
|
273
|
+
│ HTTP Response Sent │
|
|
274
|
+
└─────────────────────────┘
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Performance Characteristics:
|
|
278
|
+
|
|
279
|
+
* **Cold Start**: ~3-5ms (embedded runtime eliminates disk I/O)
|
|
280
|
+
* **Action Execute**: ~100-500µs
|
|
281
|
+
* **Memory/Worker**: ~40-80MB (configurable via V8 flags)
|
|
282
|
+
* **Throughput**: ~10k - 19k req/sec @ 200 concurrent connections
|
|
283
|
+
* **Latency**: ~14-17ms (p50), ~30ms (p97.5)
|
|
284
|
+
|
|
285
|
+
### When to Use TitanPL:
|
|
232
286
|
|
|
233
|
-
|
|
234
|
-
|
|
287
|
+
✅ **Perfect for:**
|
|
288
|
+
* CPU-bound or compute-heavy services
|
|
289
|
+
* Deterministic execution requirements
|
|
290
|
+
* Linear debugging workflows
|
|
291
|
+
* Predictable memory usage per worker
|
|
292
|
+
* Crash isolation (one worker crash doesn't affect others)
|
|
293
|
+
|
|
294
|
+
❌ **Not ideal for:**
|
|
295
|
+
* I/O-heavy services with high concurrency (use Node.js, Deno, or Bun)
|
|
296
|
+
* Applications requiring `setTimeout`, Promises, or async/await
|
|
297
|
+
* Real-time event-driven architectures
|
|
298
|
+
|
|
299
|
+
### Migration from Async Patterns:
|
|
300
|
+
|
|
301
|
+
If you're coming from Node.js, **do not** try to use async patterns:
|
|
302
|
+
|
|
303
|
+
```javascript
|
|
304
|
+
// ❌ This will NOT work
|
|
305
|
+
export const fetchUser = defineAction(async (req) => {
|
|
306
|
+
const response = await t.fetch("https://api.example.com/user");
|
|
307
|
+
return response;
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ✅ Use synchronous blocking calls instead
|
|
311
|
+
export const fetchUser = defineAction((req) => {
|
|
312
|
+
const response = t.fetch("https://api.example.com/user"); // Blocks until complete
|
|
313
|
+
return response;
|
|
314
|
+
});
|
|
315
|
+
```
|
|
235
316
|
|
|
236
317
|
---
|
|
237
318
|
|
|
@@ -257,7 +338,7 @@ TitanPL spins up a **worker pool**, where each worker owns:
|
|
|
257
338
|
* Its own V8 isolate
|
|
258
339
|
* Its own context
|
|
259
340
|
* Its own compiled actions
|
|
260
|
-
*
|
|
341
|
+
* **No event loop** (synchronous execution only)
|
|
261
342
|
|
|
262
343
|
Workers never share a lock, never block each other, and never wait for global state.
|
|
263
344
|
|
|
@@ -267,7 +348,7 @@ This gives TitanPL a performance profile similar to:
|
|
|
267
348
|
* Chrome’s process-per-tab architecture
|
|
268
349
|
* High-performance Rust servers like Actix or Hyper
|
|
269
350
|
|
|
270
|
-
But executed **directly for JavaScript**.
|
|
351
|
+
But executed **directly for JavaScript with synchronous semantics**.
|
|
271
352
|
|
|
272
353
|
---
|
|
273
354
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ezetgalaxy/titan",
|
|
3
|
-
"version": "26.
|
|
3
|
+
"version": "26.12.1",
|
|
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",
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
use v8;
|
|
2
|
+
use reqwest::{
|
|
3
|
+
blocking::Client,
|
|
4
|
+
header::{HeaderMap, HeaderName, HeaderValue},
|
|
5
|
+
};
|
|
6
|
+
use std::path::PathBuf;
|
|
7
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
8
|
+
use serde_json::Value;
|
|
9
|
+
use jsonwebtoken::{encode, decode, Header, EncodingKey, DecodingKey, Validation};
|
|
10
|
+
use bcrypt::{hash, verify, DEFAULT_COST};
|
|
11
|
+
|
|
12
|
+
use crate::utils::{blue, gray, parse_expires_in};
|
|
13
|
+
use super::{TitanRuntime, v8_str, v8_to_string, throw, ShareContextStore};
|
|
14
|
+
|
|
15
|
+
const TITAN_CORE_JS: &str = include_str!("titan_core.js");
|
|
16
|
+
|
|
17
|
+
pub fn inject_builtin_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Object>, t_obj: v8::Local<v8::Object>) {
|
|
18
|
+
// 1. Native API Bindings
|
|
19
|
+
|
|
20
|
+
// defineAction (Native side)
|
|
21
|
+
let def_fn = v8::Function::new(scope, native_define_action).unwrap();
|
|
22
|
+
let def_key = v8_str(scope, "defineAction");
|
|
23
|
+
global.set(scope, def_key.into(), def_fn.into());
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
// t.read
|
|
27
|
+
let read_fn = v8::Function::new(scope, native_read).unwrap();
|
|
28
|
+
let read_key = v8_str(scope, "read");
|
|
29
|
+
t_obj.set(scope, read_key.into(), read_fn.into());
|
|
30
|
+
|
|
31
|
+
// t.decodeUtf8
|
|
32
|
+
let dec_fn = v8::Function::new(scope, native_decode_utf8).unwrap();
|
|
33
|
+
let dec_key = v8_str(scope, "decodeUtf8");
|
|
34
|
+
t_obj.set(scope, dec_key.into(), dec_fn.into());
|
|
35
|
+
|
|
36
|
+
// t.log
|
|
37
|
+
let log_fn = v8::Function::new(scope, native_log).unwrap();
|
|
38
|
+
let log_key = v8_str(scope, "log");
|
|
39
|
+
t_obj.set(scope, log_key.into(), log_fn.into());
|
|
40
|
+
|
|
41
|
+
// t.fetch
|
|
42
|
+
let fetch_fn = v8::Function::new(scope, native_fetch).unwrap();
|
|
43
|
+
let fetch_key = v8_str(scope, "fetch");
|
|
44
|
+
t_obj.set(scope, fetch_key.into(), fetch_fn.into());
|
|
45
|
+
|
|
46
|
+
// auth, jwt, password ... (keep native)
|
|
47
|
+
setup_native_utils(scope, t_obj);
|
|
48
|
+
|
|
49
|
+
// 2. JS Side Injection (Embedded)
|
|
50
|
+
let source = v8_str(scope, TITAN_CORE_JS);
|
|
51
|
+
if let Some(script) = v8::Script::compile(scope, source, None) {
|
|
52
|
+
script.run(scope);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn setup_native_utils(scope: &mut v8::HandleScope, t_obj: v8::Local<v8::Object>) {
|
|
57
|
+
// t.jwt
|
|
58
|
+
let jwt_obj = v8::Object::new(scope);
|
|
59
|
+
let sign_fn = v8::Function::new(scope, native_jwt_sign).unwrap();
|
|
60
|
+
let verify_fn = v8::Function::new(scope, native_jwt_verify).unwrap();
|
|
61
|
+
|
|
62
|
+
let sign_key = v8_str(scope, "sign");
|
|
63
|
+
jwt_obj.set(scope, sign_key.into(), sign_fn.into());
|
|
64
|
+
let verify_key = v8_str(scope, "verify");
|
|
65
|
+
jwt_obj.set(scope, verify_key.into(), verify_fn.into());
|
|
66
|
+
|
|
67
|
+
let jwt_key = v8_str(scope, "jwt");
|
|
68
|
+
t_obj.set(scope, jwt_key.into(), jwt_obj.into());
|
|
69
|
+
|
|
70
|
+
// t.password
|
|
71
|
+
let pw_obj = v8::Object::new(scope);
|
|
72
|
+
let hash_fn = v8::Function::new(scope, native_password_hash).unwrap();
|
|
73
|
+
let pw_verify_fn = v8::Function::new(scope, native_password_verify).unwrap();
|
|
74
|
+
|
|
75
|
+
let hash_key = v8_str(scope, "hash");
|
|
76
|
+
pw_obj.set(scope, hash_key.into(), hash_fn.into());
|
|
77
|
+
let pw_v_key = v8_str(scope, "verify");
|
|
78
|
+
pw_obj.set(scope, pw_v_key.into(), pw_verify_fn.into());
|
|
79
|
+
|
|
80
|
+
let pw_key = v8_str(scope, "password");
|
|
81
|
+
t_obj.set(scope, pw_key.into(), pw_obj.into());
|
|
82
|
+
|
|
83
|
+
// t.shareContext (Native primitives)
|
|
84
|
+
let sc_obj = v8::Object::new(scope);
|
|
85
|
+
let n_get = v8::Function::new(scope, share_context_get).unwrap();
|
|
86
|
+
let n_set = v8::Function::new(scope, share_context_set).unwrap();
|
|
87
|
+
let n_del = v8::Function::new(scope, share_context_delete).unwrap();
|
|
88
|
+
let n_keys = v8::Function::new(scope, share_context_keys).unwrap();
|
|
89
|
+
let n_pub = v8::Function::new(scope, share_context_broadcast).unwrap();
|
|
90
|
+
|
|
91
|
+
let get_key = v8_str(scope, "get");
|
|
92
|
+
sc_obj.set(scope, get_key.into(), n_get.into());
|
|
93
|
+
let set_key = v8_str(scope, "set");
|
|
94
|
+
sc_obj.set(scope, set_key.into(), n_set.into());
|
|
95
|
+
let del_key = v8_str(scope, "delete");
|
|
96
|
+
sc_obj.set(scope, del_key.into(), n_del.into());
|
|
97
|
+
let keys_key = v8_str(scope, "keys");
|
|
98
|
+
sc_obj.set(scope, keys_key.into(), n_keys.into());
|
|
99
|
+
let pub_key = v8_str(scope, "broadcast");
|
|
100
|
+
sc_obj.set(scope, pub_key.into(), n_pub.into());
|
|
101
|
+
|
|
102
|
+
let sc_key = v8_str(scope, "shareContext");
|
|
103
|
+
let sc_val = sc_obj.into();
|
|
104
|
+
t_obj.set(scope, sc_key.into(), sc_val);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fn native_read(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
108
|
+
let path_val = args.get(0);
|
|
109
|
+
if !path_val.is_string() {
|
|
110
|
+
throw(scope, "t.read(path): path is required");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
let path_str = v8_to_string(scope, path_val);
|
|
114
|
+
|
|
115
|
+
if std::path::Path::new(&path_str).is_absolute() {
|
|
116
|
+
throw(scope, "t.read expects a relative path like 'db/file.sql'");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let context = scope.get_current_context();
|
|
121
|
+
let global = context.global(scope);
|
|
122
|
+
let root_key = v8_str(scope, "__titan_root");
|
|
123
|
+
let root_val = global.get(scope, root_key.into()).unwrap();
|
|
124
|
+
|
|
125
|
+
let root_str = if root_val.is_string() {
|
|
126
|
+
v8_to_string(scope, root_val)
|
|
127
|
+
} else {
|
|
128
|
+
throw(scope, "Internal Error: __titan_root not set");
|
|
129
|
+
return;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
let root_path = PathBuf::from(root_str);
|
|
133
|
+
let root_path = root_path.canonicalize().unwrap_or(root_path);
|
|
134
|
+
let joined = root_path.join(&path_str);
|
|
135
|
+
|
|
136
|
+
let target = match joined.canonicalize() {
|
|
137
|
+
Ok(t) => t,
|
|
138
|
+
Err(_) => {
|
|
139
|
+
throw(scope, &format!("t.read: file not found: {}", path_str));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if !target.starts_with(&root_path) {
|
|
145
|
+
throw(scope, "t.read: path escapes allowed root");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
match std::fs::read_to_string(&target) {
|
|
150
|
+
Ok(content) => {
|
|
151
|
+
retval.set(v8_str(scope, &content).into());
|
|
152
|
+
},
|
|
153
|
+
Err(e) => {
|
|
154
|
+
throw(scope, &format!("t.read failed: {}", e));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fn native_decode_utf8(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
160
|
+
let val = args.get(0);
|
|
161
|
+
if let Ok(u8arr) = v8::Local::<v8::Uint8Array>::try_from(val) {
|
|
162
|
+
let buf = u8arr.buffer(scope).unwrap();
|
|
163
|
+
let store = v8::ArrayBuffer::get_backing_store(&buf);
|
|
164
|
+
let offset = usize::from(u8arr.byte_offset());
|
|
165
|
+
let length = usize::from(u8arr.byte_length());
|
|
166
|
+
let slice = &store[offset..offset+length];
|
|
167
|
+
|
|
168
|
+
let bytes: Vec<u8> = slice.iter().map(|b| b.get()).collect();
|
|
169
|
+
let s = String::from_utf8_lossy(&bytes);
|
|
170
|
+
retval.set(v8_str(scope, &s).into());
|
|
171
|
+
} else if let Ok(ab) = v8::Local::<v8::ArrayBuffer>::try_from(val) {
|
|
172
|
+
let store = v8::ArrayBuffer::get_backing_store(&ab);
|
|
173
|
+
let bytes: Vec<u8> = store.iter().map(|b| b.get()).collect();
|
|
174
|
+
let s = String::from_utf8_lossy(&bytes);
|
|
175
|
+
retval.set(v8_str(scope, &s).into());
|
|
176
|
+
} else {
|
|
177
|
+
retval.set(v8::null(scope).into());
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fn share_context_get(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
182
|
+
let key = v8_to_string(scope, args.get(0));
|
|
183
|
+
let store = ShareContextStore::get();
|
|
184
|
+
if let Some(val) = store.kv.get(&key) {
|
|
185
|
+
let json_str = val.to_string();
|
|
186
|
+
let v8_str = v8::String::new(scope, &json_str).unwrap();
|
|
187
|
+
if let Some(v8_val) = v8::json::parse(scope, v8_str) {
|
|
188
|
+
retval.set(v8_val);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
retval.set(v8::null(scope).into());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fn share_context_set(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut _retval: v8::ReturnValue) {
|
|
196
|
+
let key = v8_to_string(scope, args.get(0));
|
|
197
|
+
let val_v8 = args.get(1);
|
|
198
|
+
|
|
199
|
+
if let Some(json_v8) = v8::json::stringify(scope, val_v8) {
|
|
200
|
+
let json_str = json_v8.to_rust_string_lossy(scope);
|
|
201
|
+
if let Ok(val) = serde_json::from_str(&json_str) {
|
|
202
|
+
ShareContextStore::get().kv.insert(key, val);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fn share_context_delete(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut _retval: v8::ReturnValue) {
|
|
208
|
+
let key = v8_to_string(scope, args.get(0));
|
|
209
|
+
ShareContextStore::get().kv.remove(&key);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fn share_context_keys(scope: &mut v8::HandleScope, _args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
213
|
+
let store = ShareContextStore::get();
|
|
214
|
+
let keys: Vec<v8::Local<v8::Value>> = store.kv.iter().map(|kv| v8_str(scope, kv.key()).into()).collect();
|
|
215
|
+
let arr = v8::Array::new_with_elements(scope, &keys);
|
|
216
|
+
retval.set(arr.into());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
fn share_context_broadcast(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut _retval: v8::ReturnValue) {
|
|
220
|
+
let event = v8_to_string(scope, args.get(0));
|
|
221
|
+
let payload_v8 = args.get(1);
|
|
222
|
+
|
|
223
|
+
if let Some(json_v8) = v8::json::stringify(scope, payload_v8) {
|
|
224
|
+
let json_str = json_v8.to_rust_string_lossy(scope);
|
|
225
|
+
if let Ok(payload) = serde_json::from_str(&json_str) {
|
|
226
|
+
let _ = ShareContextStore::get().broadcast_tx.send((event, payload));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
fn native_log(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut _retval: v8::ReturnValue) {
|
|
234
|
+
let context = scope.get_current_context();
|
|
235
|
+
let global = context.global(scope);
|
|
236
|
+
let action_key = v8_str(scope, "__titan_action");
|
|
237
|
+
let action_name = if let Some(action_val) = global.get(scope, action_key.into()) {
|
|
238
|
+
if action_val.is_string() {
|
|
239
|
+
v8_to_string(scope, action_val)
|
|
240
|
+
} else {
|
|
241
|
+
"init".to_string()
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
"init".to_string()
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
let mut parts = Vec::new();
|
|
248
|
+
for i in 0..args.length() {
|
|
249
|
+
let val = args.get(i);
|
|
250
|
+
let mut appended = false;
|
|
251
|
+
|
|
252
|
+
if val.is_object() && !val.is_function() {
|
|
253
|
+
if let Some(json) = v8::json::stringify(scope, val) {
|
|
254
|
+
parts.push(json.to_rust_string_lossy(scope));
|
|
255
|
+
appended = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if !appended {
|
|
260
|
+
parts.push(v8_to_string(scope, val));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let titan_str = blue("[Titan]");
|
|
265
|
+
let log_msg = gray(&format!("\x1b[90mlog({})\x1b[0m\x1b[97m: {}\x1b[0m", action_name, parts.join(" ")));
|
|
266
|
+
println!(
|
|
267
|
+
"{} {}",
|
|
268
|
+
titan_str,
|
|
269
|
+
log_msg
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fn native_fetch(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
274
|
+
let url = v8_to_string(scope, args.get(0));
|
|
275
|
+
let mut method = "GET".to_string();
|
|
276
|
+
let mut body_str = None;
|
|
277
|
+
let mut headers_vec = Vec::new();
|
|
278
|
+
|
|
279
|
+
let opts_val = args.get(1);
|
|
280
|
+
if opts_val.is_object() {
|
|
281
|
+
let opts_obj = opts_val.to_object(scope).unwrap();
|
|
282
|
+
|
|
283
|
+
let m_key = v8_str(scope, "method");
|
|
284
|
+
if let Some(m_val) = opts_obj.get(scope, m_key.into()) {
|
|
285
|
+
if m_val.is_string() {
|
|
286
|
+
method = v8_to_string(scope, m_val);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let b_key = v8_str(scope, "body");
|
|
291
|
+
if let Some(b_val) = opts_obj.get(scope, b_key.into()) {
|
|
292
|
+
if b_val.is_string() {
|
|
293
|
+
body_str = Some(v8_to_string(scope, b_val));
|
|
294
|
+
} else if b_val.is_object() {
|
|
295
|
+
let json_obj = v8::json::stringify(scope, b_val).unwrap();
|
|
296
|
+
body_str = Some(json_obj.to_rust_string_lossy(scope));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let h_key = v8_str(scope, "headers");
|
|
301
|
+
if let Some(h_val) = opts_obj.get(scope, h_key.into()) {
|
|
302
|
+
if h_val.is_object() {
|
|
303
|
+
let h_obj = h_val.to_object(scope).unwrap();
|
|
304
|
+
if let Some(keys) = h_obj.get_own_property_names(scope, Default::default()) {
|
|
305
|
+
for i in 0..keys.length() {
|
|
306
|
+
let key = keys.get_index(scope, i).unwrap();
|
|
307
|
+
let val = h_obj.get(scope, key).unwrap();
|
|
308
|
+
headers_vec.push((v8_to_string(scope, key), v8_to_string(scope, val)));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let client = Client::builder().use_rustls_tls().tcp_nodelay(true).build().unwrap_or(Client::new());
|
|
316
|
+
let mut req = client.request(method.parse().unwrap_or(reqwest::Method::GET), &url);
|
|
317
|
+
|
|
318
|
+
for (k, v) in headers_vec {
|
|
319
|
+
if let (Ok(name), Ok(val)) = (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(&v)) {
|
|
320
|
+
let mut map = HeaderMap::new();
|
|
321
|
+
map.insert(name, val);
|
|
322
|
+
req = req.headers(map);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if let Some(b) = body_str {
|
|
327
|
+
req = req.body(b);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let res = req.send();
|
|
331
|
+
let obj = v8::Object::new(scope);
|
|
332
|
+
match res {
|
|
333
|
+
Ok(r) => {
|
|
334
|
+
let status = r.status().as_u16();
|
|
335
|
+
let text = r.text().unwrap_or_default();
|
|
336
|
+
|
|
337
|
+
let status_key = v8_str(scope, "status");
|
|
338
|
+
let status_val = v8::Number::new(scope, status as f64);
|
|
339
|
+
obj.set(scope, status_key.into(), status_val.into());
|
|
340
|
+
|
|
341
|
+
let body_key = v8_str(scope, "body");
|
|
342
|
+
let body_val = v8_str(scope, &text);
|
|
343
|
+
obj.set(scope, body_key.into(), body_val.into());
|
|
344
|
+
|
|
345
|
+
let ok_key = v8_str(scope, "ok");
|
|
346
|
+
let ok_val = v8::Boolean::new(scope, true);
|
|
347
|
+
obj.set(scope, ok_key.into(), ok_val.into());
|
|
348
|
+
},
|
|
349
|
+
Err(e) => {
|
|
350
|
+
let ok_key = v8_str(scope, "ok");
|
|
351
|
+
let ok_val = v8::Boolean::new(scope, false);
|
|
352
|
+
obj.set(scope, ok_key.into(), ok_val.into());
|
|
353
|
+
|
|
354
|
+
let err_key = v8_str(scope, "error");
|
|
355
|
+
let err_val = v8_str(scope, &e.to_string());
|
|
356
|
+
obj.set(scope, err_key.into(), err_val.into());
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
retval.set(obj.into());
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
fn native_jwt_sign(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
363
|
+
let payload_val = args.get(0);
|
|
364
|
+
let json_str = v8::json::stringify(scope, payload_val).unwrap().to_rust_string_lossy(scope);
|
|
365
|
+
let mut payload: serde_json::Map<String, Value> = serde_json::from_str(&json_str).unwrap_or_default();
|
|
366
|
+
let secret = v8_to_string(scope, args.get(1));
|
|
367
|
+
|
|
368
|
+
let opts_val = args.get(2);
|
|
369
|
+
if opts_val.is_object() {
|
|
370
|
+
let opts_obj = opts_val.to_object(scope).unwrap();
|
|
371
|
+
let exp_key = v8_str(scope, "expiresIn");
|
|
372
|
+
if let Some(val) = opts_obj.get(scope, exp_key.into()) {
|
|
373
|
+
let seconds = if val.is_number() {
|
|
374
|
+
Some(val.to_number(scope).unwrap().value() as u64)
|
|
375
|
+
} else if val.is_string() {
|
|
376
|
+
parse_expires_in(&v8_to_string(scope, val))
|
|
377
|
+
} else { None };
|
|
378
|
+
if let Some(sec) = seconds {
|
|
379
|
+
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
|
380
|
+
payload.insert("exp".to_string(), Value::Number(serde_json::Number::from(now + sec)));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let token = encode(&Header::default(), &Value::Object(payload), &EncodingKey::from_secret(secret.as_bytes()));
|
|
386
|
+
match token {
|
|
387
|
+
Ok(t) => {
|
|
388
|
+
let res = v8_str(scope, &t);
|
|
389
|
+
retval.set(res.into());
|
|
390
|
+
},
|
|
391
|
+
Err(e) => throw(scope, &e.to_string()),
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
fn native_jwt_verify(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
396
|
+
let token = v8_to_string(scope, args.get(0));
|
|
397
|
+
let secret = v8_to_string(scope, args.get(1));
|
|
398
|
+
let mut validation = Validation::default();
|
|
399
|
+
validation.validate_exp = true;
|
|
400
|
+
let data = decode::<Value>(&token, &DecodingKey::from_secret(secret.as_bytes()), &validation);
|
|
401
|
+
match data {
|
|
402
|
+
Ok(d) => {
|
|
403
|
+
let json_str = serde_json::to_string(&d.claims).unwrap();
|
|
404
|
+
let v8_json_str = v8_str(scope, &json_str);
|
|
405
|
+
if let Some(val) = v8::json::parse(scope, v8_json_str) {
|
|
406
|
+
retval.set(val);
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
Err(e) => throw(scope, &format!("Invalid or expired JWT: {}", e)),
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
fn native_password_hash(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
414
|
+
let pw = v8_to_string(scope, args.get(0));
|
|
415
|
+
match hash(pw, DEFAULT_COST) {
|
|
416
|
+
Ok(h) => {
|
|
417
|
+
let res = v8_str(scope, &h);
|
|
418
|
+
retval.set(res.into());
|
|
419
|
+
},
|
|
420
|
+
Err(e) => throw(scope, &e.to_string()),
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
fn native_password_verify(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
425
|
+
let pw = v8_to_string(scope, args.get(0));
|
|
426
|
+
let hash_str = v8_to_string(scope, args.get(1));
|
|
427
|
+
let ok = verify(pw, &hash_str).unwrap_or(false);
|
|
428
|
+
retval.set(v8::Boolean::new(scope, ok).into());
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
fn native_define_action(_scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
432
|
+
retval.set(args.get(0));
|
|
433
|
+
}
|