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