titanpl-sdk 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "titanpl-sdk",
3
- "version": "0.3.0",
3
+ "version": "1.0.0",
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",
@@ -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
+ }