1use serde_json::Value;
31use std::collections::HashMap;
32
33#[cfg(target_arch = "wasm32")]
38extern "C" {
39 #[link_name = "cufflink_log"]
40 fn cufflink_log(level: i32, msg_ptr: i32, msg_len: i32);
41 fn db_query(sql_ptr: i32, sql_len: i32) -> i32;
42 fn db_execute(sql_ptr: i32, sql_len: i32) -> i32;
43 fn get_host_response_len() -> i32;
44 fn get_host_response(buf_ptr: i32, buf_len: i32) -> i32;
45 fn nats_publish(subj_ptr: i32, subj_len: i32, payload_ptr: i32, payload_len: i32) -> i32;
46 fn nats_request(
47 subj_ptr: i32,
48 subj_len: i32,
49 payload_ptr: i32,
50 payload_len: i32,
51 timeout_ms: i32,
52 ) -> i32;
53 fn http_fetch(
54 method_ptr: i32,
55 method_len: i32,
56 url_ptr: i32,
57 url_len: i32,
58 headers_ptr: i32,
59 headers_len: i32,
60 body_ptr: i32,
61 body_len: i32,
62 ) -> i32;
63 fn get_config(key_ptr: i32, key_len: i32) -> i32;
64 fn s3_download(bucket_ptr: i32, bucket_len: i32, key_ptr: i32, key_len: i32) -> i32;
65 fn image_transform_jpeg(
66 bucket_ptr: i32,
67 bucket_len: i32,
68 in_key_ptr: i32,
69 in_key_len: i32,
70 out_key_ptr: i32,
71 out_key_len: i32,
72 max_dim: i32,
73 quality: i32,
74 ) -> i32;
75 fn s3_presign_upload(
76 bucket_ptr: i32,
77 bucket_len: i32,
78 key_ptr: i32,
79 key_len: i32,
80 content_type_ptr: i32,
81 content_type_len: i32,
82 expires_secs: i32,
83 ) -> i32;
84 fn redis_get(key_ptr: i32, key_len: i32) -> i32;
85 fn redis_set(key_ptr: i32, key_len: i32, val_ptr: i32, val_len: i32, ttl_secs: i32) -> i32;
86 fn redis_del(key_ptr: i32, key_len: i32) -> i32;
87 fn generate_uuid() -> i32;
88 fn current_time() -> i32;
89}
90
91#[derive(Debug, Clone)]
112pub struct Auth {
113 pub sub: String,
115 pub preferred_username: Option<String>,
117 pub name: Option<String>,
119 pub email: Option<String>,
121 pub realm_roles: Vec<String>,
123 pub claims: HashMap<String, Value>,
125 pub permissions: Vec<String>,
127 pub role_names: Vec<String>,
129 pub is_service_account: bool,
132}
133
134impl Auth {
135 pub fn has_role(&self, role: &str) -> bool {
137 self.realm_roles.iter().any(|r| r == role)
138 }
139
140 pub fn can(&self, area: &str, operation: &str) -> bool {
151 let required = format!("{}:{}", area, operation);
152 let wildcard = format!("{}:*", area);
153 self.permissions
154 .iter()
155 .any(|p| p == &required || p == &wildcard || p == "*")
156 }
157
158 pub fn has_cufflink_role(&self, role: &str) -> bool {
160 self.role_names.iter().any(|r| r == role)
161 }
162
163 pub fn claim(&self, key: &str) -> Option<&Value> {
165 self.claims.get(key)
166 }
167}
168
169#[derive(Debug, Clone)]
176pub struct Request {
177 method: String,
178 handler: String,
179 headers: HashMap<String, String>,
180 body: Value,
181 raw_body: Vec<u8>,
182 tenant: String,
183 service: String,
184 auth: Option<Auth>,
185}
186
187impl Request {
188 pub fn from_json(json: &str) -> Option<Self> {
190 let v: Value = serde_json::from_str(json).ok()?;
191 let headers = v["headers"]
192 .as_object()
193 .map(|m| {
194 m.iter()
195 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
196 .collect()
197 })
198 .unwrap_or_default();
199
200 let auth = v["auth"].as_object().map(|auth_obj| {
201 let a = Value::Object(auth_obj.clone());
202 Auth {
203 sub: a["sub"].as_str().unwrap_or("").to_string(),
204 preferred_username: a["preferred_username"].as_str().map(|s| s.to_string()),
205 name: a["name"].as_str().map(|s| s.to_string()),
206 email: a["email"].as_str().map(|s| s.to_string()),
207 realm_roles: a["realm_roles"]
208 .as_array()
209 .map(|arr| {
210 arr.iter()
211 .filter_map(|v| v.as_str().map(|s| s.to_string()))
212 .collect()
213 })
214 .unwrap_or_default(),
215 claims: a["claims"]
216 .as_object()
217 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
218 .unwrap_or_default(),
219 permissions: a["permissions"]
220 .as_array()
221 .map(|arr| {
222 arr.iter()
223 .filter_map(|v| v.as_str().map(|s| s.to_string()))
224 .collect()
225 })
226 .unwrap_or_default(),
227 role_names: a["role_names"]
228 .as_array()
229 .map(|arr| {
230 arr.iter()
231 .filter_map(|v| v.as_str().map(|s| s.to_string()))
232 .collect()
233 })
234 .unwrap_or_default(),
235 is_service_account: a["is_service_account"].as_bool().unwrap_or(false),
236 }
237 });
238
239 let raw_body = v["body_raw_b64"]
240 .as_str()
241 .filter(|s| !s.is_empty())
242 .and_then(|s| {
243 use base64::{engine::general_purpose, Engine};
244 general_purpose::STANDARD.decode(s).ok()
245 })
246 .unwrap_or_default();
247
248 Some(Self {
249 method: v["method"].as_str().unwrap_or("GET").to_string(),
250 handler: v["handler"].as_str().unwrap_or("").to_string(),
251 headers,
252 body: v["body"].clone(),
253 raw_body,
254 tenant: v["tenant"].as_str().unwrap_or("").to_string(),
255 service: v["service"].as_str().unwrap_or("").to_string(),
256 auth,
257 })
258 }
259
260 pub fn method(&self) -> &str {
262 &self.method
263 }
264
265 pub fn handler(&self) -> &str {
267 &self.handler
268 }
269
270 pub fn headers(&self) -> &HashMap<String, String> {
272 &self.headers
273 }
274
275 pub fn header(&self, name: &str) -> Option<&str> {
277 self.headers.get(name).map(|s| s.as_str())
278 }
279
280 pub fn body(&self) -> &Value {
282 &self.body
283 }
284
285 pub fn raw_body(&self) -> &[u8] {
293 &self.raw_body
294 }
295
296 pub fn tenant(&self) -> &str {
298 &self.tenant
299 }
300
301 pub fn service(&self) -> &str {
303 &self.service
304 }
305
306 pub fn auth(&self) -> Option<&Auth> {
310 self.auth.as_ref()
311 }
312
313 pub fn require_auth(&self) -> Result<&Auth, Response> {
325 self.auth.as_ref().ok_or_else(|| {
326 Response::json(&serde_json::json!({
327 "error": "Authentication required",
328 "status": 401
329 }))
330 })
331 }
332}
333
334#[derive(Debug, Clone)]
338pub struct Response {
339 data: String,
340 status: u16,
341}
342
343impl Response {
344 pub fn json(value: &Value) -> Self {
346 Self {
347 data: serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
348 status: 200,
349 }
350 }
351
352 pub fn text(s: &str) -> Self {
354 Self::json(&Value::String(s.to_string()))
355 }
356
357 pub fn error(message: &str) -> Self {
359 Self {
360 data: serde_json::json!({"error": message}).to_string(),
361 status: 400,
362 }
363 }
364
365 pub fn not_found(message: &str) -> Self {
367 Self {
368 data: serde_json::json!({"error": message}).to_string(),
369 status: 404,
370 }
371 }
372
373 pub fn forbidden(message: &str) -> Self {
375 Self {
376 data: serde_json::json!({"error": message}).to_string(),
377 status: 403,
378 }
379 }
380
381 pub fn empty() -> Self {
383 Self::json(&serde_json::json!({"ok": true}))
384 }
385
386 pub fn with_status(mut self, status: u16) -> Self {
388 self.status = status;
389 self
390 }
391
392 pub fn into_data(self) -> String {
395 if self.status == 200 {
396 self.data
398 } else {
399 serde_json::json!({
401 "__status": self.status,
402 "__body": serde_json::from_str::<Value>(&self.data).unwrap_or(Value::String(self.data)),
403 })
404 .to_string()
405 }
406 }
407}
408
409pub mod db {
416 use super::*;
417
418 pub fn query(sql: &str) -> Vec<Value> {
429 #[cfg(target_arch = "wasm32")]
430 {
431 let bytes = sql.as_bytes();
432 let result = unsafe { db_query(bytes.as_ptr() as i32, bytes.len() as i32) };
433 if result < 0 {
434 return vec![];
435 }
436 read_host_response()
437 }
438 #[cfg(not(target_arch = "wasm32"))]
439 {
440 let _ = sql;
441 vec![]
442 }
443 }
444
445 pub fn query_one(sql: &str) -> Option<Value> {
453 query(sql).into_iter().next()
454 }
455
456 pub fn execute(sql: &str) -> i32 {
465 #[cfg(target_arch = "wasm32")]
466 {
467 let bytes = sql.as_bytes();
468 unsafe { db_execute(bytes.as_ptr() as i32, bytes.len() as i32) }
469 }
470 #[cfg(not(target_arch = "wasm32"))]
471 {
472 let _ = sql;
473 0
474 }
475 }
476
477 #[cfg(target_arch = "wasm32")]
479 fn read_host_response() -> Vec<Value> {
480 let len = unsafe { get_host_response_len() };
481 if len <= 0 {
482 return vec![];
483 }
484 let mut buf = vec![0u8; len as usize];
485 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
486 if read <= 0 {
487 return vec![];
488 }
489 buf.truncate(read as usize);
490 let json_str = String::from_utf8_lossy(&buf);
491 serde_json::from_str(&json_str).unwrap_or_default()
492 }
493}
494
495pub mod nats {
502 #[allow(unused_imports)]
503 use super::*;
504
505 pub fn publish(subject: &str, payload: &str) -> bool {
516 #[cfg(target_arch = "wasm32")]
517 {
518 let subj_bytes = subject.as_bytes();
519 let payload_bytes = payload.as_bytes();
520 let result = unsafe {
521 nats_publish(
522 subj_bytes.as_ptr() as i32,
523 subj_bytes.len() as i32,
524 payload_bytes.as_ptr() as i32,
525 payload_bytes.len() as i32,
526 )
527 };
528 result == 0
529 }
530 #[cfg(not(target_arch = "wasm32"))]
531 {
532 let _ = (subject, payload);
533 true
534 }
535 }
536
537 pub fn request(subject: &str, payload: &str, timeout_ms: i32) -> Option<String> {
549 #[cfg(target_arch = "wasm32")]
550 {
551 let subj_bytes = subject.as_bytes();
552 let payload_bytes = payload.as_bytes();
553 let result = unsafe {
554 nats_request(
555 subj_bytes.as_ptr() as i32,
556 subj_bytes.len() as i32,
557 payload_bytes.as_ptr() as i32,
558 payload_bytes.len() as i32,
559 timeout_ms,
560 )
561 };
562 if result != 0 {
563 return None;
564 }
565 let len = unsafe { get_host_response_len() };
566 if len <= 0 {
567 return None;
568 }
569 let mut buf = vec![0u8; len as usize];
570 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
571 if read <= 0 {
572 return None;
573 }
574 String::from_utf8(buf[..read as usize].to_vec()).ok()
575 }
576 #[cfg(not(target_arch = "wasm32"))]
577 {
578 let _ = (subject, payload, timeout_ms);
579 None
580 }
581 }
582}
583
584pub mod log {
590 #[allow(unused_imports)]
591 use super::*;
592
593 pub fn error(msg: &str) {
595 write(0, msg);
596 }
597
598 pub fn warn(msg: &str) {
600 write(1, msg);
601 }
602
603 pub fn info(msg: &str) {
605 write(2, msg);
606 }
607
608 pub fn debug(msg: &str) {
610 write(3, msg);
611 }
612
613 fn write(level: i32, msg: &str) {
614 #[cfg(target_arch = "wasm32")]
615 {
616 let bytes = msg.as_bytes();
617 unsafe {
618 super::cufflink_log(level, bytes.as_ptr() as i32, bytes.len() as i32);
619 }
620 }
621 #[cfg(not(target_arch = "wasm32"))]
622 {
623 let _ = (level, msg);
624 }
625 }
626}
627
628pub mod http {
635 #[allow(unused_imports)]
636 use super::*;
637
638 #[derive(Debug, Clone)]
640 pub struct FetchResponse {
641 pub status: i32,
643 pub body: String,
645 pub body_encoding: String,
647 pub headers: HashMap<String, String>,
649 }
650
651 impl FetchResponse {
652 pub fn json(&self) -> Option<Value> {
654 serde_json::from_str(&self.body).ok()
655 }
656
657 pub fn is_success(&self) -> bool {
659 (200..300).contains(&self.status)
660 }
661
662 pub fn is_base64(&self) -> bool {
664 self.body_encoding == "base64"
665 }
666 }
667
668 pub fn fetch(
679 method: &str,
680 url: &str,
681 headers: &[(&str, &str)],
682 body: Option<&str>,
683 ) -> Option<FetchResponse> {
684 #[cfg(target_arch = "wasm32")]
685 {
686 let method_bytes = method.as_bytes();
687 let url_bytes = url.as_bytes();
688 let headers_map: HashMap<&str, &str> = headers.iter().copied().collect();
689 let headers_json = serde_json::to_string(&headers_map).unwrap_or_default();
690 let headers_bytes = headers_json.as_bytes();
691 let body_bytes = body.unwrap_or("").as_bytes();
692 let body_len = body.map(|b| b.len()).unwrap_or(0);
693
694 let result = unsafe {
695 http_fetch(
696 method_bytes.as_ptr() as i32,
697 method_bytes.len() as i32,
698 url_bytes.as_ptr() as i32,
699 url_bytes.len() as i32,
700 headers_bytes.as_ptr() as i32,
701 headers_bytes.len() as i32,
702 body_bytes.as_ptr() as i32,
703 body_len as i32,
704 )
705 };
706
707 if result < 0 {
708 return None;
709 }
710
711 read_fetch_response()
712 }
713 #[cfg(not(target_arch = "wasm32"))]
714 {
715 let _ = (method, url, headers, body);
716 None
717 }
718 }
719
720 pub fn get(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
722 fetch("GET", url, headers, None)
723 }
724
725 pub fn post(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
727 fetch("POST", url, headers, Some(body))
728 }
729
730 pub fn put(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
732 fetch("PUT", url, headers, Some(body))
733 }
734
735 pub fn delete(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
737 fetch("DELETE", url, headers, None)
738 }
739
740 pub fn patch(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
742 fetch("PATCH", url, headers, Some(body))
743 }
744
745 #[cfg(target_arch = "wasm32")]
747 fn read_fetch_response() -> Option<FetchResponse> {
748 let len = unsafe { get_host_response_len() };
749 if len <= 0 {
750 return None;
751 }
752 let mut buf = vec![0u8; len as usize];
753 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
754 if read <= 0 {
755 return None;
756 }
757 buf.truncate(read as usize);
758 let json_str = String::from_utf8_lossy(&buf);
759 let v: Value = serde_json::from_str(&json_str).ok()?;
760 Some(FetchResponse {
761 status: v["status"].as_i64().unwrap_or(0) as i32,
762 body: v["body"].as_str().unwrap_or("").to_string(),
763 body_encoding: v["body_encoding"].as_str().unwrap_or("utf8").to_string(),
764 headers: v["headers"]
765 .as_object()
766 .map(|m| {
767 m.iter()
768 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
769 .collect()
770 })
771 .unwrap_or_default(),
772 })
773 }
774}
775
776pub mod config {
784 #[allow(unused_imports)]
785 use super::*;
786
787 pub fn get(key: &str) -> Option<String> {
796 #[cfg(target_arch = "wasm32")]
797 {
798 let bytes = key.as_bytes();
799 let result = unsafe { get_config(bytes.as_ptr() as i32, bytes.len() as i32) };
800 if result < 0 {
801 return None;
802 }
803 read_config_response()
804 }
805 #[cfg(not(target_arch = "wasm32"))]
806 {
807 let _ = key;
808 None
809 }
810 }
811
812 #[cfg(target_arch = "wasm32")]
814 fn read_config_response() -> Option<String> {
815 let len = unsafe { get_host_response_len() };
816 if len <= 0 {
817 return None;
818 }
819 let mut buf = vec![0u8; len as usize];
820 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
821 if read <= 0 {
822 return None;
823 }
824 buf.truncate(read as usize);
825 String::from_utf8(buf).ok()
826 }
827}
828
829pub mod storage {
837 #[allow(unused_imports)]
838 use super::*;
839
840 pub fn download(bucket: &str, key: &str) -> Option<String> {
851 #[cfg(target_arch = "wasm32")]
852 {
853 let bucket_bytes = bucket.as_bytes();
854 let key_bytes = key.as_bytes();
855 let result = unsafe {
856 s3_download(
857 bucket_bytes.as_ptr() as i32,
858 bucket_bytes.len() as i32,
859 key_bytes.as_ptr() as i32,
860 key_bytes.len() as i32,
861 )
862 };
863 if result < 0 {
864 return None;
865 }
866 read_storage_response()
867 }
868 #[cfg(not(target_arch = "wasm32"))]
869 {
870 let _ = (bucket, key);
871 None
872 }
873 }
874
875 pub fn presign_upload(
888 bucket: &str,
889 key: &str,
890 content_type: &str,
891 expires_secs: u64,
892 ) -> Option<String> {
893 #[cfg(target_arch = "wasm32")]
894 {
895 let bucket_bytes = bucket.as_bytes();
896 let key_bytes = key.as_bytes();
897 let ct_bytes = content_type.as_bytes();
898 let result = unsafe {
899 s3_presign_upload(
900 bucket_bytes.as_ptr() as i32,
901 bucket_bytes.len() as i32,
902 key_bytes.as_ptr() as i32,
903 key_bytes.len() as i32,
904 ct_bytes.as_ptr() as i32,
905 ct_bytes.len() as i32,
906 expires_secs as i32,
907 )
908 };
909 if result < 0 {
910 return None;
911 }
912 read_storage_response()
913 }
914 #[cfg(not(target_arch = "wasm32"))]
915 {
916 let _ = (bucket, key, content_type, expires_secs);
917 None
918 }
919 }
920
921 #[cfg(target_arch = "wasm32")]
923 fn read_storage_response() -> Option<String> {
924 let len = unsafe { get_host_response_len() };
925 if len <= 0 {
926 return None;
927 }
928 let mut buf = vec![0u8; len as usize];
929 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
930 if read <= 0 {
931 return None;
932 }
933 buf.truncate(read as usize);
934 String::from_utf8(buf).ok()
935 }
936}
937
938pub mod image {
948 #[allow(unused_imports)]
949 use super::*;
950
951 pub fn transform_jpeg(
962 bucket: &str,
963 in_key: &str,
964 out_key: &str,
965 max_dim: u32,
966 quality: u8,
967 ) -> Option<u32> {
968 #[cfg(target_arch = "wasm32")]
969 {
970 let bucket_bytes = bucket.as_bytes();
971 let in_key_bytes = in_key.as_bytes();
972 let out_key_bytes = out_key.as_bytes();
973 let result = unsafe {
974 super::image_transform_jpeg(
975 bucket_bytes.as_ptr() as i32,
976 bucket_bytes.len() as i32,
977 in_key_bytes.as_ptr() as i32,
978 in_key_bytes.len() as i32,
979 out_key_bytes.as_ptr() as i32,
980 out_key_bytes.len() as i32,
981 max_dim as i32,
982 quality as i32,
983 )
984 };
985 if result < 0 {
986 return None;
987 }
988 Some(result as u32)
989 }
990 #[cfg(not(target_arch = "wasm32"))]
991 {
992 let _ = (bucket, in_key, out_key, max_dim, quality);
993 None
994 }
995 }
996}
997
998pub mod redis {
1005 #[allow(unused_imports)]
1006 use super::*;
1007
1008 pub fn get(key: &str) -> Option<String> {
1017 #[cfg(target_arch = "wasm32")]
1018 {
1019 let bytes = key.as_bytes();
1020 let result = unsafe { redis_get(bytes.as_ptr() as i32, bytes.len() as i32) };
1021 if result < 0 {
1022 return None;
1023 }
1024 read_redis_response()
1025 }
1026 #[cfg(not(target_arch = "wasm32"))]
1027 {
1028 let _ = key;
1029 None
1030 }
1031 }
1032
1033 pub fn set(key: &str, value: &str, ttl_secs: i32) -> bool {
1041 #[cfg(target_arch = "wasm32")]
1042 {
1043 let key_bytes = key.as_bytes();
1044 let val_bytes = value.as_bytes();
1045 let result = unsafe {
1046 redis_set(
1047 key_bytes.as_ptr() as i32,
1048 key_bytes.len() as i32,
1049 val_bytes.as_ptr() as i32,
1050 val_bytes.len() as i32,
1051 ttl_secs,
1052 )
1053 };
1054 result == 0
1055 }
1056 #[cfg(not(target_arch = "wasm32"))]
1057 {
1058 let _ = (key, value, ttl_secs);
1059 true
1060 }
1061 }
1062
1063 pub fn del(key: &str) -> bool {
1071 #[cfg(target_arch = "wasm32")]
1072 {
1073 let bytes = key.as_bytes();
1074 let result = unsafe { redis_del(bytes.as_ptr() as i32, bytes.len() as i32) };
1075 result == 0
1076 }
1077 #[cfg(not(target_arch = "wasm32"))]
1078 {
1079 let _ = key;
1080 true
1081 }
1082 }
1083
1084 #[cfg(target_arch = "wasm32")]
1086 fn read_redis_response() -> Option<String> {
1087 let len = unsafe { get_host_response_len() };
1088 if len <= 0 {
1089 return None;
1090 }
1091 let mut buf = vec![0u8; len as usize];
1092 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1093 if read <= 0 {
1094 return None;
1095 }
1096 buf.truncate(read as usize);
1097 String::from_utf8(buf).ok()
1098 }
1099}
1100
1101pub mod util {
1105 #[allow(unused_imports)]
1106 use super::*;
1107
1108 pub fn current_time() -> String {
1123 #[cfg(target_arch = "wasm32")]
1124 {
1125 let result = unsafe { super::current_time() };
1126 if result < 0 {
1127 return String::new();
1128 }
1129 let len = unsafe { get_host_response_len() };
1130 if len <= 0 {
1131 return String::new();
1132 }
1133 let mut buf = vec![0u8; len as usize];
1134 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1135 if read <= 0 {
1136 return String::new();
1137 }
1138 buf.truncate(read as usize);
1139 String::from_utf8(buf).unwrap_or_default()
1140 }
1141
1142 #[cfg(not(target_arch = "wasm32"))]
1143 {
1144 let secs = std::time::SystemTime::now()
1145 .duration_since(std::time::UNIX_EPOCH)
1146 .map(|d| d.as_secs())
1147 .unwrap_or(0);
1148 format!("1970-01-01T00:00:00Z+{}", secs)
1149 }
1150 }
1151
1152 pub fn generate_uuid() -> String {
1153 #[cfg(target_arch = "wasm32")]
1154 {
1155 let result = unsafe { super::generate_uuid() };
1156 if result < 0 {
1157 return String::new();
1158 }
1159 let len = unsafe { get_host_response_len() };
1160 if len <= 0 {
1161 return String::new();
1162 }
1163 let mut buf = vec![0u8; len as usize];
1164 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1165 if read <= 0 {
1166 return String::new();
1167 }
1168 buf.truncate(read as usize);
1169 String::from_utf8(buf).unwrap_or_default()
1170 }
1171
1172 #[cfg(not(target_arch = "wasm32"))]
1173 {
1174 format!(
1175 "{:08x}-{:04x}-4{:03x}-{:04x}-{:012x}",
1176 std::time::SystemTime::now()
1177 .duration_since(std::time::UNIX_EPOCH)
1178 .map(|d| d.as_nanos() as u32)
1179 .unwrap_or(0),
1180 std::process::id() as u16,
1181 0u16,
1182 0x8000u16,
1183 0u64,
1184 )
1185 }
1186 }
1187}
1188
1189#[doc(hidden)]
1193pub fn __run_handler<F>(ptr: i32, len: i32, f: F) -> i32
1194where
1195 F: FnOnce(Request) -> Response,
1196{
1197 let request_json = unsafe {
1199 let slice = std::slice::from_raw_parts(ptr as *const u8, len as usize);
1200 String::from_utf8_lossy(slice).into_owned()
1201 };
1202
1203 let request = Request::from_json(&request_json).unwrap_or_else(|| Request {
1205 method: "GET".to_string(),
1206 handler: String::new(),
1207 headers: HashMap::new(),
1208 body: Value::Null,
1209 raw_body: Vec::new(),
1210 tenant: String::new(),
1211 service: String::new(),
1212 auth: None,
1213 });
1214
1215 let response = f(request);
1217 let response_bytes = response.into_data().into_bytes();
1218
1219 let total = 4 + response_bytes.len();
1221 let layout = std::alloc::Layout::from_size_align(total, 1).expect("invalid layout");
1222 let out_ptr = unsafe { std::alloc::alloc(layout) };
1223
1224 unsafe {
1225 let len_bytes = (response_bytes.len() as u32).to_le_bytes();
1226 std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), out_ptr, 4);
1227 std::ptr::copy_nonoverlapping(
1228 response_bytes.as_ptr(),
1229 out_ptr.add(4),
1230 response_bytes.len(),
1231 );
1232 }
1233
1234 out_ptr as i32
1235}
1236
1237#[macro_export]
1250macro_rules! init {
1251 () => {
1252 #[no_mangle]
1253 pub extern "C" fn alloc(size: i32) -> i32 {
1254 let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
1255 unsafe { std::alloc::alloc(layout) as i32 }
1256 }
1257 };
1258}
1259
1260#[macro_export]
1287macro_rules! handler {
1288 ($name:ident, |$req:ident : Request| $body:expr) => {
1289 #[no_mangle]
1290 pub extern "C" fn $name(ptr: i32, len: i32) -> i32 {
1291 $crate::__run_handler(ptr, len, |$req: $crate::Request| $body)
1292 }
1293 };
1294}
1295
1296pub mod migrate {
1335 use super::{Request, Response};
1336 pub use cufflink_types::SchemaDiff;
1337
1338 pub fn run<F>(req: Request, handler: F) -> Response
1345 where
1346 F: FnOnce(SchemaDiff) -> Result<(), String>,
1347 {
1348 match serde_json::from_value::<SchemaDiff>(req.body().clone()) {
1349 Ok(diff) => match handler(diff) {
1350 Ok(()) => Response::json(&serde_json::json!({"ok": true})),
1351 Err(e) => Response::error(&e),
1352 },
1353 Err(e) => Response::error(&format!(
1354 "on_migrate: failed to parse SchemaDiff payload: {}",
1355 e
1356 )),
1357 }
1358 }
1359}
1360
1361pub mod prelude {
1369 pub use crate::config;
1370 pub use crate::db;
1371 pub use crate::http;
1372 pub use crate::image;
1373 pub use crate::log;
1374 pub use crate::migrate;
1375 pub use crate::nats;
1376 pub use crate::redis;
1377 pub use crate::storage;
1378 pub use crate::util;
1379 pub use crate::Auth;
1380 pub use crate::Request;
1381 pub use crate::Response;
1382 pub use serde_json::{json, Value};
1383}
1384
1385#[cfg(test)]
1388mod tests {
1389 use super::*;
1390 use serde_json::json;
1391
1392 #[test]
1393 fn test_request_parsing() {
1394 let json = serde_json::to_string(&json!({
1395 "method": "POST",
1396 "handler": "checkout",
1397 "headers": {"content-type": "application/json"},
1398 "body": {"item": "widget", "qty": 3},
1399 "tenant": "acme",
1400 "service": "shop"
1401 }))
1402 .unwrap();
1403
1404 let req = Request::from_json(&json).unwrap();
1405 assert_eq!(req.method(), "POST");
1406 assert_eq!(req.handler(), "checkout");
1407 assert_eq!(req.tenant(), "acme");
1408 assert_eq!(req.service(), "shop");
1409 assert_eq!(req.body()["item"], "widget");
1410 assert_eq!(req.body()["qty"], 3);
1411 assert_eq!(req.header("content-type"), Some("application/json"));
1412 }
1413
1414 #[test]
1415 fn test_request_missing_fields() {
1416 let json = r#"{"method": "GET"}"#;
1417 let req = Request::from_json(json).unwrap();
1418 assert_eq!(req.method(), "GET");
1419 assert_eq!(req.handler(), "");
1420 assert_eq!(req.tenant(), "");
1421 assert_eq!(req.body(), &Value::Null);
1422 assert!(req.raw_body().is_empty());
1423 }
1424
1425 #[test]
1426 fn test_request_raw_body_round_trip() {
1427 use base64::{engine::general_purpose, Engine};
1428 let raw = br#"{"type":"message.delivered","data":{"object":{"id":"AC1"}}}"#;
1429 let json = serde_json::to_string(&json!({
1430 "method": "POST",
1431 "handler": "webhook",
1432 "headers": {"content-type": "application/json"},
1433 "body": serde_json::from_slice::<Value>(raw).unwrap(),
1434 "body_raw_b64": general_purpose::STANDARD.encode(raw),
1435 "tenant": "acme",
1436 "service": "shop",
1437 }))
1438 .unwrap();
1439 let req = Request::from_json(&json).unwrap();
1440 assert_eq!(req.raw_body(), raw);
1441 }
1442
1443 #[test]
1444 fn test_request_raw_body_invalid_base64_yields_empty() {
1445 let json = serde_json::to_string(&json!({
1446 "method": "POST",
1447 "handler": "webhook",
1448 "body": Value::Null,
1449 "body_raw_b64": "not%base64!",
1450 "tenant": "acme",
1451 "service": "shop",
1452 }))
1453 .unwrap();
1454 let req = Request::from_json(&json).unwrap();
1455 assert!(req.raw_body().is_empty());
1456 }
1457
1458 #[test]
1459 fn test_response_json() {
1460 let resp = Response::json(&json!({"status": "ok", "count": 42}));
1461 let data = resp.into_data();
1462 let parsed: Value = serde_json::from_str(&data).unwrap();
1463 assert_eq!(parsed["status"], "ok");
1464 assert_eq!(parsed["count"], 42);
1465 }
1466
1467 #[test]
1468 fn test_response_error() {
1469 let resp = Response::error("something went wrong");
1470 let data = resp.into_data();
1471 let parsed: Value = serde_json::from_str(&data).unwrap();
1472 assert_eq!(parsed["__status"], 400);
1474 assert_eq!(parsed["__body"]["error"], "something went wrong");
1475 }
1476
1477 #[test]
1478 fn test_response_not_found() {
1479 let resp = Response::not_found("item not found");
1480 let data = resp.into_data();
1481 let parsed: Value = serde_json::from_str(&data).unwrap();
1482 assert_eq!(parsed["__status"], 404);
1483 assert_eq!(parsed["__body"]["error"], "item not found");
1484 }
1485
1486 #[test]
1487 fn test_response_with_status() {
1488 let resp = Response::json(&serde_json::json!({"ok": true})).with_status(201);
1489 let data = resp.into_data();
1490 let parsed: Value = serde_json::from_str(&data).unwrap();
1491 assert_eq!(parsed["__status"], 201);
1492 assert_eq!(parsed["__body"]["ok"], true);
1493 }
1494
1495 fn migrate_request(diff: serde_json::Value) -> Request {
1496 let payload = serde_json::to_string(&json!({
1497 "method": "POST",
1498 "handler": "handle_on_migrate",
1499 "headers": {},
1500 "body": diff,
1501 "tenant": "default",
1502 "service": "logistics-service",
1503 }))
1504 .unwrap();
1505 Request::from_json(&payload).unwrap()
1506 }
1507
1508 #[test]
1509 fn test_migrate_run_success() {
1510 let req = migrate_request(json!({
1511 "added_columns": [["pickups", "min"]],
1512 "dropped_columns": [["pickups", "midpoint"]],
1513 }));
1514 let resp = migrate::run(req, |diff| {
1515 assert!(diff.added_column("pickups", "min"));
1516 assert!(diff.dropped_column("pickups", "midpoint"));
1517 Ok(())
1518 });
1519 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1520 assert_eq!(parsed["ok"], true);
1521 }
1522
1523 #[test]
1524 fn test_migrate_run_handler_error() {
1525 let req = migrate_request(json!({}));
1526 let resp = migrate::run(req, |_| Err("backfill failed".into()));
1527 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1528 assert_eq!(parsed["__status"], 400);
1529 assert_eq!(parsed["__body"]["error"], "backfill failed");
1530 }
1531
1532 #[test]
1533 fn test_migrate_run_invalid_payload() {
1534 let req = migrate_request(json!("not a diff"));
1536 let resp = migrate::run(req, |_| {
1537 panic!("closure should not be called for invalid payload")
1538 });
1539 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1540 assert_eq!(parsed["__status"], 400);
1541 assert!(parsed["__body"]["error"]
1542 .as_str()
1543 .unwrap()
1544 .contains("on_migrate: failed to parse SchemaDiff payload"));
1545 }
1546
1547 #[test]
1548 fn test_migrate_run_empty_diff() {
1549 let req = migrate_request(json!({}));
1550 let mut called = false;
1551 let resp = migrate::run(req, |diff| {
1552 assert!(diff.is_empty());
1553 called = true;
1554 Ok(())
1555 });
1556 assert!(called);
1557 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1558 assert_eq!(parsed["ok"], true);
1559 }
1560
1561 #[test]
1562 fn test_response_200_no_wrapper() {
1563 let resp = Response::json(&serde_json::json!({"data": "test"}));
1564 let data = resp.into_data();
1565 let parsed: Value = serde_json::from_str(&data).unwrap();
1566 assert_eq!(parsed["data"], "test");
1568 assert!(parsed.get("__status").is_none());
1569 }
1570
1571 #[test]
1572 fn test_response_empty() {
1573 let resp = Response::empty();
1574 let data = resp.into_data();
1575 let parsed: Value = serde_json::from_str(&data).unwrap();
1576 assert_eq!(parsed["ok"], true);
1577 }
1578
1579 #[test]
1580 fn test_response_text() {
1581 let resp = Response::text("hello world");
1582 let data = resp.into_data();
1583 let parsed: Value = serde_json::from_str(&data).unwrap();
1584 assert_eq!(parsed, "hello world");
1585 }
1586
1587 #[test]
1588 fn test_db_query_noop_on_native() {
1589 let rows = db::query("SELECT 1");
1591 assert!(rows.is_empty());
1592 }
1593
1594 #[test]
1595 fn test_db_query_one_noop_on_native() {
1596 let row = db::query_one("SELECT 1");
1597 assert!(row.is_none());
1598 }
1599
1600 #[test]
1601 fn test_db_execute_noop_on_native() {
1602 let affected = db::execute("INSERT INTO x VALUES (1)");
1603 assert_eq!(affected, 0);
1604 }
1605
1606 #[test]
1607 fn test_nats_publish_noop_on_native() {
1608 let ok = nats::publish("test.subject", "payload");
1609 assert!(ok);
1610 }
1611
1612 #[test]
1613 fn test_request_with_auth() {
1614 let json = serde_json::to_string(&json!({
1615 "method": "POST",
1616 "handler": "checkout",
1617 "headers": {},
1618 "body": {},
1619 "tenant": "acme",
1620 "service": "shop",
1621 "auth": {
1622 "sub": "user-123",
1623 "preferred_username": "john",
1624 "name": "John Doe",
1625 "email": "john@example.com",
1626 "realm_roles": ["admin", "manager"],
1627 "claims": {"department": "engineering"}
1628 }
1629 }))
1630 .unwrap();
1631
1632 let req = Request::from_json(&json).unwrap();
1633 let auth = req.auth().unwrap();
1634 assert_eq!(auth.sub, "user-123");
1635 assert_eq!(auth.preferred_username.as_deref(), Some("john"));
1636 assert_eq!(auth.name.as_deref(), Some("John Doe"));
1637 assert_eq!(auth.email.as_deref(), Some("john@example.com"));
1638 assert!(auth.has_role("admin"));
1639 assert!(auth.has_role("manager"));
1640 assert!(!auth.has_role("viewer"));
1641 assert_eq!(
1642 auth.claim("department").and_then(|v| v.as_str()),
1643 Some("engineering")
1644 );
1645 }
1646
1647 #[test]
1648 fn test_request_without_auth() {
1649 let json = r#"{"method": "GET"}"#;
1650 let req = Request::from_json(json).unwrap();
1651 assert!(req.auth().is_none());
1652 }
1653
1654 #[test]
1655 fn test_request_null_auth() {
1656 let json = serde_json::to_string(&json!({
1657 "method": "GET",
1658 "auth": null
1659 }))
1660 .unwrap();
1661 let req = Request::from_json(&json).unwrap();
1662 assert!(req.auth().is_none());
1663 }
1664
1665 #[test]
1666 fn test_require_auth_success() {
1667 let json = serde_json::to_string(&json!({
1668 "method": "GET",
1669 "auth": {"sub": "user-1", "realm_roles": [], "claims": {}}
1670 }))
1671 .unwrap();
1672 let req = Request::from_json(&json).unwrap();
1673 assert!(req.require_auth().is_ok());
1674 assert_eq!(req.require_auth().unwrap().sub, "user-1");
1675 }
1676
1677 #[test]
1678 fn test_require_auth_fails_when_unauthenticated() {
1679 let json = r#"{"method": "GET"}"#;
1680 let req = Request::from_json(json).unwrap();
1681 assert!(req.require_auth().is_err());
1682 }
1683
1684 #[test]
1685 fn test_http_fetch_noop_on_native() {
1686 let resp = http::fetch("GET", "https://example.com", &[], None);
1687 assert!(resp.is_none());
1688 }
1689
1690 #[test]
1691 fn test_http_get_noop_on_native() {
1692 let resp = http::get("https://example.com", &[]);
1693 assert!(resp.is_none());
1694 }
1695
1696 #[test]
1697 fn test_http_post_noop_on_native() {
1698 let resp = http::post("https://example.com", &[], "{}");
1699 assert!(resp.is_none());
1700 }
1701
1702 #[test]
1703 fn test_storage_download_noop_on_native() {
1704 let data = storage::download("my-bucket", "images/photo.jpg");
1705 assert!(data.is_none());
1706 }
1707
1708 #[test]
1709 fn test_image_transform_jpeg_noop_on_native() {
1710 let bytes = image::transform_jpeg("my-bucket", "in.jpg", "out.jpg", 1024, 80);
1711 assert!(bytes.is_none());
1712 }
1713
1714 #[test]
1715 fn test_auth_permissions() {
1716 let json = serde_json::to_string(&json!({
1717 "method": "POST",
1718 "handler": "test",
1719 "headers": {},
1720 "body": {},
1721 "tenant": "acme",
1722 "service": "shop",
1723 "auth": {
1724 "sub": "user-1",
1725 "realm_roles": ["admin"],
1726 "claims": {},
1727 "permissions": ["staff:create", "staff:view", "items:*"],
1728 "role_names": ["admin", "manager"]
1729 }
1730 }))
1731 .unwrap();
1732
1733 let req = Request::from_json(&json).unwrap();
1734 let auth = req.auth().unwrap();
1735
1736 assert!(auth.can("staff", "create"));
1738 assert!(auth.can("staff", "view"));
1739 assert!(!auth.can("staff", "delete"));
1740
1741 assert!(auth.can("items", "create"));
1743 assert!(auth.can("items", "view"));
1744 assert!(auth.can("items", "delete"));
1745
1746 assert!(!auth.can("batches", "view"));
1748
1749 assert!(auth.has_cufflink_role("admin"));
1751 assert!(auth.has_cufflink_role("manager"));
1752 assert!(!auth.has_cufflink_role("viewer"));
1753 }
1754
1755 #[test]
1756 fn test_auth_super_wildcard() {
1757 let auth = Auth {
1758 sub: "user-1".to_string(),
1759 preferred_username: None,
1760 name: None,
1761 email: None,
1762 realm_roles: vec![],
1763 claims: HashMap::new(),
1764 permissions: vec!["*".to_string()],
1765 role_names: vec!["superadmin".to_string()],
1766 is_service_account: false,
1767 };
1768
1769 assert!(auth.can("anything", "everything"));
1770 assert!(auth.can("staff", "create"));
1771 }
1772
1773 #[test]
1774 fn test_auth_empty_permissions() {
1775 let auth = Auth {
1776 sub: "user-1".to_string(),
1777 preferred_username: None,
1778 name: None,
1779 email: None,
1780 realm_roles: vec![],
1781 claims: HashMap::new(),
1782 permissions: vec![],
1783 role_names: vec![],
1784 is_service_account: false,
1785 };
1786
1787 assert!(!auth.can("staff", "create"));
1788 assert!(!auth.has_cufflink_role("admin"));
1789 }
1790
1791 #[test]
1792 fn test_redis_get_noop_on_native() {
1793 let val = redis::get("some-key");
1794 assert!(val.is_none());
1795 }
1796
1797 #[test]
1798 fn test_redis_set_noop_on_native() {
1799 let ok = redis::set("key", "value", 3600);
1800 assert!(ok);
1801 }
1802
1803 #[test]
1804 fn test_redis_del_noop_on_native() {
1805 let ok = redis::del("key");
1806 assert!(ok);
1807 }
1808
1809 #[test]
1810 fn test_http_fetch_response_helpers() {
1811 let resp = http::FetchResponse {
1812 status: 200,
1813 body: r#"{"key": "value"}"#.to_string(),
1814 body_encoding: "utf8".to_string(),
1815 headers: HashMap::new(),
1816 };
1817 assert!(resp.is_success());
1818 assert!(!resp.is_base64());
1819 let json = resp.json().unwrap();
1820 assert_eq!(json["key"], "value");
1821
1822 let err_resp = http::FetchResponse {
1823 status: 404,
1824 body: "not found".to_string(),
1825 body_encoding: "utf8".to_string(),
1826 headers: HashMap::new(),
1827 };
1828 assert!(!err_resp.is_success());
1829
1830 let binary_resp = http::FetchResponse {
1831 status: 200,
1832 body: "aW1hZ2VkYXRh".to_string(),
1833 body_encoding: "base64".to_string(),
1834 headers: HashMap::new(),
1835 };
1836 assert!(binary_resp.is_base64());
1837 }
1838}