@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.
Files changed (36) hide show
  1. package/README.md +91 -10
  2. package/package.json +1 -1
  3. package/templates/js/server/Cargo.toml +2 -0
  4. package/templates/js/server/src/extensions/builtin.rs +433 -0
  5. package/templates/js/server/src/extensions/external.rs +309 -0
  6. package/templates/js/server/src/extensions/mod.rs +242 -0
  7. package/templates/js/server/src/extensions/titan_core.js +17 -0
  8. package/templates/js/server/src/main.rs +1 -2
  9. package/templates/js/server/src/runtime.rs +46 -20
  10. package/templates/js/titan/dev.js +39 -2
  11. package/templates/rust-js/titan/dev.js +39 -6
  12. package/templates/rust-ts/titan/dev.js +39 -6
  13. package/templates/ts/server/Cargo.toml +2 -0
  14. package/templates/ts/server/src/errors.rs +9 -50
  15. package/templates/ts/server/src/extensions/builtin.rs +433 -0
  16. package/templates/ts/server/src/extensions/external.rs +309 -0
  17. package/templates/ts/server/src/extensions/mod.rs +242 -0
  18. package/templates/ts/server/src/extensions/titan_core.js +17 -0
  19. package/templates/ts/server/src/main.rs +1 -2
  20. package/templates/ts/server/src/runtime.rs +46 -20
  21. package/templates/ts/titan/dev.js +40 -6
  22. package/titanpl-sdk/package.json +1 -1
  23. package/titanpl-sdk/templates/server/Cargo.toml +2 -0
  24. package/titanpl-sdk/templates/server/src/extensions/builtin.rs +433 -0
  25. package/titanpl-sdk/templates/server/src/extensions/external.rs +309 -0
  26. package/titanpl-sdk/templates/server/src/extensions/mod.rs +242 -0
  27. package/titanpl-sdk/templates/server/src/extensions/titan_core.js +17 -0
  28. package/titanpl-sdk/templates/server/src/main.rs +1 -2
  29. package/titanpl-sdk/templates/server/src/runtime.rs +46 -20
  30. package/titanpl-sdk/templates/titan/dev.js +36 -0
  31. package/templates/js/server/Cargo.lock +0 -2869
  32. package/templates/js/server/src/extensions.rs +0 -1161
  33. package/templates/ts/server/Cargo.lock +0 -2869
  34. package/templates/ts/server/src/extensions.rs +0 -1161
  35. package/titanpl-sdk/templates/server/Cargo.lock +0 -2839
  36. 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 Note
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
- Here is a **clear, strong, marketing-friendly, developer-focused promotion section** that highlights TitanPL’s **multi-threaded JS runtime** in a powerful way.
234
- You can use this in your website, README, landing page, package description, or docs.
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
- * Its own event loop
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.11.0",
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",
@@ -26,5 +26,7 @@ postgres = { version = "0.19", features = ["with-serde_json-1"] }
26
26
  libloading = "0.8"
27
27
  walkdir = "2"
28
28
  crossbeam = "0.8.4"
29
+ dashmap = "6.1.0"
29
30
  bytes = "1.11.0"
30
31
  smallvec = "1.15.1"
32
+ num_cpus = "1.17.0"
@@ -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
+ }