1use serde_json::Value;
31use std::collections::HashMap;
32
33#[cfg(target_arch = "wasm32")]
36pub(crate) fn read_batch_response() -> Option<Vec<Value>> {
37 let len = unsafe { get_host_response_len() };
38 if len <= 0 {
39 return None;
40 }
41 let mut buf = vec![0u8; len as usize];
42 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
43 if read <= 0 {
44 return None;
45 }
46 buf.truncate(read as usize);
47 let s = String::from_utf8(buf).ok()?;
48 let v: Value = serde_json::from_str(&s).ok()?;
49 v.as_array().cloned()
50}
51
52#[cfg(target_arch = "wasm32")]
57extern "C" {
58 #[link_name = "cufflink_log"]
59 fn cufflink_log(level: i32, msg_ptr: i32, msg_len: i32);
60 fn db_query(sql_ptr: i32, sql_len: i32) -> i32;
61 fn db_execute(sql_ptr: i32, sql_len: i32) -> i32;
62 fn get_host_response_len() -> i32;
63 fn get_host_response(buf_ptr: i32, buf_len: i32) -> i32;
64 fn nats_publish(subj_ptr: i32, subj_len: i32, payload_ptr: i32, payload_len: i32) -> i32;
65 fn nats_request(
66 subj_ptr: i32,
67 subj_len: i32,
68 payload_ptr: i32,
69 payload_len: i32,
70 timeout_ms: i32,
71 ) -> i32;
72 fn http_fetch(
73 method_ptr: i32,
74 method_len: i32,
75 url_ptr: i32,
76 url_len: i32,
77 headers_ptr: i32,
78 headers_len: i32,
79 body_ptr: i32,
80 body_len: i32,
81 ) -> i32;
82 fn get_config(key_ptr: i32, key_len: i32) -> i32;
83 fn s3_download(bucket_ptr: i32, bucket_len: i32, key_ptr: i32, key_len: i32) -> i32;
84 fn s3_download_many(items_ptr: i32, items_len: i32) -> i32;
85 fn http_fetch_many(items_ptr: i32, items_len: i32) -> i32;
86 fn image_transform_jpeg(
87 bucket_ptr: i32,
88 bucket_len: i32,
89 in_key_ptr: i32,
90 in_key_len: i32,
91 out_key_ptr: i32,
92 out_key_len: i32,
93 max_dim: i32,
94 quality: i32,
95 ) -> i32;
96 fn s3_presign_upload(
97 bucket_ptr: i32,
98 bucket_len: i32,
99 key_ptr: i32,
100 key_len: i32,
101 content_type_ptr: i32,
102 content_type_len: i32,
103 expires_secs: i32,
104 ) -> i32;
105 fn redis_get(key_ptr: i32, key_len: i32) -> i32;
106 fn redis_set(key_ptr: i32, key_len: i32, val_ptr: i32, val_len: i32, ttl_secs: i32) -> i32;
107 fn redis_del(key_ptr: i32, key_len: i32) -> i32;
108 fn generate_uuid() -> i32;
109 fn current_time() -> i32;
110}
111
112#[derive(Debug, Clone)]
133pub struct Auth {
134 pub sub: String,
136 pub preferred_username: Option<String>,
138 pub name: Option<String>,
140 pub email: Option<String>,
142 pub realm_roles: Vec<String>,
144 pub claims: HashMap<String, Value>,
146 pub permissions: Vec<String>,
148 pub role_names: Vec<String>,
150 pub is_service_account: bool,
153}
154
155impl Auth {
156 pub fn has_role(&self, role: &str) -> bool {
158 self.realm_roles.iter().any(|r| r == role)
159 }
160
161 pub fn can(&self, area: &str, operation: &str) -> bool {
172 let required = format!("{}:{}", area, operation);
173 let wildcard = format!("{}:*", area);
174 self.permissions
175 .iter()
176 .any(|p| p == &required || p == &wildcard || p == "*")
177 }
178
179 pub fn has_cufflink_role(&self, role: &str) -> bool {
181 self.role_names.iter().any(|r| r == role)
182 }
183
184 pub fn claim(&self, key: &str) -> Option<&Value> {
186 self.claims.get(key)
187 }
188}
189
190#[derive(Debug, Clone)]
197pub struct Request {
198 method: String,
199 handler: String,
200 headers: HashMap<String, String>,
201 body: Value,
202 raw_body: Vec<u8>,
203 tenant: String,
204 service: String,
205 auth: Option<Auth>,
206}
207
208impl Request {
209 pub fn from_json(json: &str) -> Option<Self> {
211 let v: Value = serde_json::from_str(json).ok()?;
212 let headers = v["headers"]
213 .as_object()
214 .map(|m| {
215 m.iter()
216 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
217 .collect()
218 })
219 .unwrap_or_default();
220
221 let auth = v["auth"].as_object().map(|auth_obj| {
222 let a = Value::Object(auth_obj.clone());
223 Auth {
224 sub: a["sub"].as_str().unwrap_or("").to_string(),
225 preferred_username: a["preferred_username"].as_str().map(|s| s.to_string()),
226 name: a["name"].as_str().map(|s| s.to_string()),
227 email: a["email"].as_str().map(|s| s.to_string()),
228 realm_roles: a["realm_roles"]
229 .as_array()
230 .map(|arr| {
231 arr.iter()
232 .filter_map(|v| v.as_str().map(|s| s.to_string()))
233 .collect()
234 })
235 .unwrap_or_default(),
236 claims: a["claims"]
237 .as_object()
238 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
239 .unwrap_or_default(),
240 permissions: a["permissions"]
241 .as_array()
242 .map(|arr| {
243 arr.iter()
244 .filter_map(|v| v.as_str().map(|s| s.to_string()))
245 .collect()
246 })
247 .unwrap_or_default(),
248 role_names: a["role_names"]
249 .as_array()
250 .map(|arr| {
251 arr.iter()
252 .filter_map(|v| v.as_str().map(|s| s.to_string()))
253 .collect()
254 })
255 .unwrap_or_default(),
256 is_service_account: a["is_service_account"].as_bool().unwrap_or(false),
257 }
258 });
259
260 let raw_body = v["body_raw_b64"]
261 .as_str()
262 .filter(|s| !s.is_empty())
263 .and_then(|s| {
264 use base64::{engine::general_purpose, Engine};
265 general_purpose::STANDARD.decode(s).ok()
266 })
267 .unwrap_or_default();
268
269 Some(Self {
270 method: v["method"].as_str().unwrap_or("GET").to_string(),
271 handler: v["handler"].as_str().unwrap_or("").to_string(),
272 headers,
273 body: v["body"].clone(),
274 raw_body,
275 tenant: v["tenant"].as_str().unwrap_or("").to_string(),
276 service: v["service"].as_str().unwrap_or("").to_string(),
277 auth,
278 })
279 }
280
281 pub fn method(&self) -> &str {
283 &self.method
284 }
285
286 pub fn handler(&self) -> &str {
288 &self.handler
289 }
290
291 pub fn headers(&self) -> &HashMap<String, String> {
293 &self.headers
294 }
295
296 pub fn header(&self, name: &str) -> Option<&str> {
298 self.headers.get(name).map(|s| s.as_str())
299 }
300
301 pub fn body(&self) -> &Value {
303 &self.body
304 }
305
306 pub fn raw_body(&self) -> &[u8] {
314 &self.raw_body
315 }
316
317 pub fn tenant(&self) -> &str {
319 &self.tenant
320 }
321
322 pub fn service(&self) -> &str {
324 &self.service
325 }
326
327 pub fn auth(&self) -> Option<&Auth> {
331 self.auth.as_ref()
332 }
333
334 pub fn require_auth(&self) -> Result<&Auth, Response> {
346 self.auth.as_ref().ok_or_else(|| {
347 Response::json(&serde_json::json!({
348 "error": "Authentication required",
349 "status": 401
350 }))
351 })
352 }
353}
354
355#[derive(Debug, Clone)]
359pub struct Response {
360 data: String,
361 status: u16,
362}
363
364impl Response {
365 pub fn json(value: &Value) -> Self {
367 Self {
368 data: serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
369 status: 200,
370 }
371 }
372
373 pub fn text(s: &str) -> Self {
375 Self::json(&Value::String(s.to_string()))
376 }
377
378 pub fn error(message: &str) -> Self {
380 Self {
381 data: serde_json::json!({"error": message}).to_string(),
382 status: 400,
383 }
384 }
385
386 pub fn not_found(message: &str) -> Self {
388 Self {
389 data: serde_json::json!({"error": message}).to_string(),
390 status: 404,
391 }
392 }
393
394 pub fn forbidden(message: &str) -> Self {
396 Self {
397 data: serde_json::json!({"error": message}).to_string(),
398 status: 403,
399 }
400 }
401
402 pub fn empty() -> Self {
404 Self::json(&serde_json::json!({"ok": true}))
405 }
406
407 pub fn with_status(mut self, status: u16) -> Self {
409 self.status = status;
410 self
411 }
412
413 pub fn into_data(self) -> String {
416 if self.status == 200 {
417 self.data
419 } else {
420 serde_json::json!({
422 "__status": self.status,
423 "__body": serde_json::from_str::<Value>(&self.data).unwrap_or(Value::String(self.data)),
424 })
425 .to_string()
426 }
427 }
428}
429
430pub mod db {
437 use super::*;
438
439 pub fn query(sql: &str) -> Vec<Value> {
450 #[cfg(target_arch = "wasm32")]
451 {
452 let bytes = sql.as_bytes();
453 let result = unsafe { db_query(bytes.as_ptr() as i32, bytes.len() as i32) };
454 if result < 0 {
455 return vec![];
456 }
457 read_host_response()
458 }
459 #[cfg(not(target_arch = "wasm32"))]
460 {
461 let _ = sql;
462 vec![]
463 }
464 }
465
466 pub fn query_one(sql: &str) -> Option<Value> {
474 query(sql).into_iter().next()
475 }
476
477 pub fn execute(sql: &str) -> i32 {
486 #[cfg(target_arch = "wasm32")]
487 {
488 let bytes = sql.as_bytes();
489 unsafe { db_execute(bytes.as_ptr() as i32, bytes.len() as i32) }
490 }
491 #[cfg(not(target_arch = "wasm32"))]
492 {
493 let _ = sql;
494 0
495 }
496 }
497
498 #[cfg(target_arch = "wasm32")]
500 fn read_host_response() -> Vec<Value> {
501 let len = unsafe { get_host_response_len() };
502 if len <= 0 {
503 return vec![];
504 }
505 let mut buf = vec![0u8; len as usize];
506 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
507 if read <= 0 {
508 return vec![];
509 }
510 buf.truncate(read as usize);
511 let json_str = String::from_utf8_lossy(&buf);
512 serde_json::from_str(&json_str).unwrap_or_default()
513 }
514}
515
516pub mod nats {
523 #[allow(unused_imports)]
524 use super::*;
525
526 pub fn publish(subject: &str, payload: &str) -> bool {
537 #[cfg(target_arch = "wasm32")]
538 {
539 let subj_bytes = subject.as_bytes();
540 let payload_bytes = payload.as_bytes();
541 let result = unsafe {
542 nats_publish(
543 subj_bytes.as_ptr() as i32,
544 subj_bytes.len() as i32,
545 payload_bytes.as_ptr() as i32,
546 payload_bytes.len() as i32,
547 )
548 };
549 result == 0
550 }
551 #[cfg(not(target_arch = "wasm32"))]
552 {
553 let _ = (subject, payload);
554 true
555 }
556 }
557
558 pub fn request(subject: &str, payload: &str, timeout_ms: i32) -> Option<String> {
570 #[cfg(target_arch = "wasm32")]
571 {
572 let subj_bytes = subject.as_bytes();
573 let payload_bytes = payload.as_bytes();
574 let result = unsafe {
575 nats_request(
576 subj_bytes.as_ptr() as i32,
577 subj_bytes.len() as i32,
578 payload_bytes.as_ptr() as i32,
579 payload_bytes.len() as i32,
580 timeout_ms,
581 )
582 };
583 if result != 0 {
584 return None;
585 }
586 let len = unsafe { get_host_response_len() };
587 if len <= 0 {
588 return None;
589 }
590 let mut buf = vec![0u8; len as usize];
591 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
592 if read <= 0 {
593 return None;
594 }
595 String::from_utf8(buf[..read as usize].to_vec()).ok()
596 }
597 #[cfg(not(target_arch = "wasm32"))]
598 {
599 let _ = (subject, payload, timeout_ms);
600 None
601 }
602 }
603}
604
605pub mod log {
611 #[allow(unused_imports)]
612 use super::*;
613
614 pub fn error(msg: &str) {
616 write(0, msg);
617 }
618
619 pub fn warn(msg: &str) {
621 write(1, msg);
622 }
623
624 pub fn info(msg: &str) {
626 write(2, msg);
627 }
628
629 pub fn debug(msg: &str) {
631 write(3, msg);
632 }
633
634 fn write(level: i32, msg: &str) {
635 #[cfg(target_arch = "wasm32")]
636 {
637 let bytes = msg.as_bytes();
638 unsafe {
639 super::cufflink_log(level, bytes.as_ptr() as i32, bytes.len() as i32);
640 }
641 }
642 #[cfg(not(target_arch = "wasm32"))]
643 {
644 let _ = (level, msg);
645 }
646 }
647}
648
649pub mod http {
656 #[allow(unused_imports)]
657 use super::*;
658
659 #[derive(Debug, Clone)]
661 pub struct FetchResponse {
662 pub status: i32,
664 pub body: String,
666 pub body_encoding: String,
668 pub headers: HashMap<String, String>,
670 }
671
672 impl FetchResponse {
673 pub fn json(&self) -> Option<Value> {
675 serde_json::from_str(&self.body).ok()
676 }
677
678 pub fn is_success(&self) -> bool {
680 (200..300).contains(&self.status)
681 }
682
683 pub fn is_base64(&self) -> bool {
685 self.body_encoding == "base64"
686 }
687 }
688
689 pub fn fetch(
700 method: &str,
701 url: &str,
702 headers: &[(&str, &str)],
703 body: Option<&str>,
704 ) -> Option<FetchResponse> {
705 #[cfg(target_arch = "wasm32")]
706 {
707 let method_bytes = method.as_bytes();
708 let url_bytes = url.as_bytes();
709 let headers_map: HashMap<&str, &str> = headers.iter().copied().collect();
710 let headers_json = serde_json::to_string(&headers_map).unwrap_or_default();
711 let headers_bytes = headers_json.as_bytes();
712 let body_bytes = body.unwrap_or("").as_bytes();
713 let body_len = body.map(|b| b.len()).unwrap_or(0);
714
715 let result = unsafe {
716 http_fetch(
717 method_bytes.as_ptr() as i32,
718 method_bytes.len() as i32,
719 url_bytes.as_ptr() as i32,
720 url_bytes.len() as i32,
721 headers_bytes.as_ptr() as i32,
722 headers_bytes.len() as i32,
723 body_bytes.as_ptr() as i32,
724 body_len as i32,
725 )
726 };
727
728 if result < 0 {
729 return None;
730 }
731
732 read_fetch_response()
733 }
734 #[cfg(not(target_arch = "wasm32"))]
735 {
736 let _ = (method, url, headers, body);
737 None
738 }
739 }
740
741 pub fn get(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
743 fetch("GET", url, headers, None)
744 }
745
746 pub fn post(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
748 fetch("POST", url, headers, Some(body))
749 }
750
751 pub fn put(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
753 fetch("PUT", url, headers, Some(body))
754 }
755
756 pub fn delete(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
758 fetch("DELETE", url, headers, None)
759 }
760
761 pub fn patch(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
763 fetch("PATCH", url, headers, Some(body))
764 }
765
766 #[derive(Debug, Clone)]
768 pub struct FetchRequest<'a> {
769 pub method: &'a str,
770 pub url: &'a str,
771 pub headers: &'a [(&'a str, &'a str)],
772 pub body: Option<&'a str>,
773 }
774
775 pub fn fetch_many(requests: &[FetchRequest<'_>]) -> Vec<Result<FetchResponse, String>> {
790 #[cfg(target_arch = "wasm32")]
791 {
792 let items: Vec<Value> = requests
793 .iter()
794 .map(|r| {
795 let h: HashMap<&str, &str> = r.headers.iter().copied().collect();
796 serde_json::json!({
797 "method": r.method,
798 "url": r.url,
799 "headers": h,
800 "body": r.body,
801 })
802 })
803 .collect();
804 let payload = serde_json::to_string(&items).unwrap_or_else(|_| "[]".to_string());
805 let bytes = payload.as_bytes();
806 let rc = unsafe { super::http_fetch_many(bytes.as_ptr() as i32, bytes.len() as i32) };
807 if rc < 0 {
808 return requests
809 .iter()
810 .map(|_| Err("http_fetch_many host call failed".to_string()))
811 .collect();
812 }
813 super::read_batch_response()
814 .map(|v| v.into_iter().map(parse_fetch_slot).collect())
815 .unwrap_or_else(|| {
816 requests
817 .iter()
818 .map(|_| Err("malformed host response".to_string()))
819 .collect()
820 })
821 }
822 #[cfg(not(target_arch = "wasm32"))]
823 {
824 let _ = requests;
825 vec![]
826 }
827 }
828
829 #[cfg(target_arch = "wasm32")]
830 fn fetch_response_from_json(v: &Value) -> FetchResponse {
831 FetchResponse {
832 status: v["status"].as_i64().unwrap_or(0) as i32,
833 body: v["body"].as_str().unwrap_or("").to_string(),
834 body_encoding: v["body_encoding"].as_str().unwrap_or("utf8").to_string(),
835 headers: v["headers"]
836 .as_object()
837 .map(|m| {
838 m.iter()
839 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
840 .collect()
841 })
842 .unwrap_or_default(),
843 }
844 }
845
846 #[cfg(target_arch = "wasm32")]
847 fn parse_fetch_slot(slot: Value) -> Result<FetchResponse, String> {
848 if !slot["ok"].as_bool().unwrap_or(false) {
849 return Err(slot["error"]
850 .as_str()
851 .unwrap_or("unknown error")
852 .to_string());
853 }
854 Ok(fetch_response_from_json(&slot))
855 }
856
857 #[cfg(target_arch = "wasm32")]
859 fn read_fetch_response() -> Option<FetchResponse> {
860 let len = unsafe { get_host_response_len() };
861 if len <= 0 {
862 return None;
863 }
864 let mut buf = vec![0u8; len as usize];
865 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
866 if read <= 0 {
867 return None;
868 }
869 buf.truncate(read as usize);
870 let json_str = String::from_utf8_lossy(&buf);
871 let v: Value = serde_json::from_str(&json_str).ok()?;
872 Some(fetch_response_from_json(&v))
873 }
874}
875
876pub mod config {
884 #[allow(unused_imports)]
885 use super::*;
886
887 pub fn get(key: &str) -> Option<String> {
896 #[cfg(target_arch = "wasm32")]
897 {
898 let bytes = key.as_bytes();
899 let result = unsafe { get_config(bytes.as_ptr() as i32, bytes.len() as i32) };
900 if result < 0 {
901 return None;
902 }
903 read_config_response()
904 }
905 #[cfg(not(target_arch = "wasm32"))]
906 {
907 let _ = key;
908 None
909 }
910 }
911
912 #[cfg(target_arch = "wasm32")]
914 fn read_config_response() -> Option<String> {
915 let len = unsafe { get_host_response_len() };
916 if len <= 0 {
917 return None;
918 }
919 let mut buf = vec![0u8; len as usize];
920 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
921 if read <= 0 {
922 return None;
923 }
924 buf.truncate(read as usize);
925 String::from_utf8(buf).ok()
926 }
927}
928
929pub mod storage {
937 #[allow(unused_imports)]
938 use super::*;
939
940 pub fn download(bucket: &str, key: &str) -> Option<String> {
951 #[cfg(target_arch = "wasm32")]
952 {
953 let bucket_bytes = bucket.as_bytes();
954 let key_bytes = key.as_bytes();
955 let result = unsafe {
956 s3_download(
957 bucket_bytes.as_ptr() as i32,
958 bucket_bytes.len() as i32,
959 key_bytes.as_ptr() as i32,
960 key_bytes.len() as i32,
961 )
962 };
963 if result < 0 {
964 return None;
965 }
966 read_storage_response()
967 }
968 #[cfg(not(target_arch = "wasm32"))]
969 {
970 let _ = (bucket, key);
971 None
972 }
973 }
974
975 pub fn presign_upload(
988 bucket: &str,
989 key: &str,
990 content_type: &str,
991 expires_secs: u64,
992 ) -> Option<String> {
993 #[cfg(target_arch = "wasm32")]
994 {
995 let bucket_bytes = bucket.as_bytes();
996 let key_bytes = key.as_bytes();
997 let ct_bytes = content_type.as_bytes();
998 let result = unsafe {
999 s3_presign_upload(
1000 bucket_bytes.as_ptr() as i32,
1001 bucket_bytes.len() as i32,
1002 key_bytes.as_ptr() as i32,
1003 key_bytes.len() as i32,
1004 ct_bytes.as_ptr() as i32,
1005 ct_bytes.len() as i32,
1006 expires_secs as i32,
1007 )
1008 };
1009 if result < 0 {
1010 return None;
1011 }
1012 read_storage_response()
1013 }
1014 #[cfg(not(target_arch = "wasm32"))]
1015 {
1016 let _ = (bucket, key, content_type, expires_secs);
1017 None
1018 }
1019 }
1020
1021 #[cfg(target_arch = "wasm32")]
1023 fn read_storage_response() -> Option<String> {
1024 let len = unsafe { get_host_response_len() };
1025 if len <= 0 {
1026 return None;
1027 }
1028 let mut buf = vec![0u8; len as usize];
1029 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1030 if read <= 0 {
1031 return None;
1032 }
1033 buf.truncate(read as usize);
1034 String::from_utf8(buf).ok()
1035 }
1036
1037 pub fn download_many(items: &[(&str, &str)]) -> Vec<Result<Vec<u8>, String>> {
1042 #[cfg(target_arch = "wasm32")]
1043 {
1044 let payload_items: Vec<Value> = items
1045 .iter()
1046 .map(|(b, k)| serde_json::json!({ "bucket": b, "key": k }))
1047 .collect();
1048 let payload = serde_json::to_string(&payload_items).unwrap_or_else(|_| "[]".into());
1049 let bytes = payload.as_bytes();
1050 let rc = unsafe { super::s3_download_many(bytes.as_ptr() as i32, bytes.len() as i32) };
1051 if rc < 0 {
1052 return items
1053 .iter()
1054 .map(|_| Err("s3_download_many host call failed".to_string()))
1055 .collect();
1056 }
1057 super::read_batch_response()
1058 .map(|v| v.into_iter().map(parse_download_slot).collect())
1059 .unwrap_or_else(|| {
1060 items
1061 .iter()
1062 .map(|_| Err("malformed host response".to_string()))
1063 .collect()
1064 })
1065 }
1066 #[cfg(not(target_arch = "wasm32"))]
1067 {
1068 let _ = items;
1069 vec![]
1070 }
1071 }
1072
1073 #[cfg(target_arch = "wasm32")]
1074 fn parse_download_slot(slot: Value) -> Result<Vec<u8>, String> {
1075 if !slot["ok"].as_bool().unwrap_or(false) {
1076 return Err(slot["error"]
1077 .as_str()
1078 .unwrap_or("unknown error")
1079 .to_string());
1080 }
1081 let b64 = slot["data_b64"]
1082 .as_str()
1083 .ok_or_else(|| "missing data_b64".to_string())?;
1084 use base64::{engine::general_purpose, Engine};
1085 general_purpose::STANDARD
1086 .decode(b64)
1087 .map_err(|e| format!("base64 decode failed: {}", e))
1088 }
1089}
1090
1091pub mod image {
1101 #[allow(unused_imports)]
1102 use super::*;
1103
1104 pub fn transform_jpeg(
1115 bucket: &str,
1116 in_key: &str,
1117 out_key: &str,
1118 max_dim: u32,
1119 quality: u8,
1120 ) -> Option<u32> {
1121 #[cfg(target_arch = "wasm32")]
1122 {
1123 let bucket_bytes = bucket.as_bytes();
1124 let in_key_bytes = in_key.as_bytes();
1125 let out_key_bytes = out_key.as_bytes();
1126 let result = unsafe {
1127 super::image_transform_jpeg(
1128 bucket_bytes.as_ptr() as i32,
1129 bucket_bytes.len() as i32,
1130 in_key_bytes.as_ptr() as i32,
1131 in_key_bytes.len() as i32,
1132 out_key_bytes.as_ptr() as i32,
1133 out_key_bytes.len() as i32,
1134 max_dim as i32,
1135 quality as i32,
1136 )
1137 };
1138 if result < 0 {
1139 return None;
1140 }
1141 Some(result as u32)
1142 }
1143 #[cfg(not(target_arch = "wasm32"))]
1144 {
1145 let _ = (bucket, in_key, out_key, max_dim, quality);
1146 None
1147 }
1148 }
1149}
1150
1151pub mod redis {
1158 #[allow(unused_imports)]
1159 use super::*;
1160
1161 pub fn get(key: &str) -> Option<String> {
1170 #[cfg(target_arch = "wasm32")]
1171 {
1172 let bytes = key.as_bytes();
1173 let result = unsafe { redis_get(bytes.as_ptr() as i32, bytes.len() as i32) };
1174 if result < 0 {
1175 return None;
1176 }
1177 read_redis_response()
1178 }
1179 #[cfg(not(target_arch = "wasm32"))]
1180 {
1181 let _ = key;
1182 None
1183 }
1184 }
1185
1186 pub fn set(key: &str, value: &str, ttl_secs: i32) -> bool {
1194 #[cfg(target_arch = "wasm32")]
1195 {
1196 let key_bytes = key.as_bytes();
1197 let val_bytes = value.as_bytes();
1198 let result = unsafe {
1199 redis_set(
1200 key_bytes.as_ptr() as i32,
1201 key_bytes.len() as i32,
1202 val_bytes.as_ptr() as i32,
1203 val_bytes.len() as i32,
1204 ttl_secs,
1205 )
1206 };
1207 result == 0
1208 }
1209 #[cfg(not(target_arch = "wasm32"))]
1210 {
1211 let _ = (key, value, ttl_secs);
1212 true
1213 }
1214 }
1215
1216 pub fn del(key: &str) -> bool {
1224 #[cfg(target_arch = "wasm32")]
1225 {
1226 let bytes = key.as_bytes();
1227 let result = unsafe { redis_del(bytes.as_ptr() as i32, bytes.len() as i32) };
1228 result == 0
1229 }
1230 #[cfg(not(target_arch = "wasm32"))]
1231 {
1232 let _ = key;
1233 true
1234 }
1235 }
1236
1237 #[cfg(target_arch = "wasm32")]
1239 fn read_redis_response() -> Option<String> {
1240 let len = unsafe { get_host_response_len() };
1241 if len <= 0 {
1242 return None;
1243 }
1244 let mut buf = vec![0u8; len as usize];
1245 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1246 if read <= 0 {
1247 return None;
1248 }
1249 buf.truncate(read as usize);
1250 String::from_utf8(buf).ok()
1251 }
1252}
1253
1254pub mod util {
1258 #[allow(unused_imports)]
1259 use super::*;
1260
1261 pub fn current_time() -> String {
1276 #[cfg(target_arch = "wasm32")]
1277 {
1278 let result = unsafe { super::current_time() };
1279 if result < 0 {
1280 return String::new();
1281 }
1282 let len = unsafe { get_host_response_len() };
1283 if len <= 0 {
1284 return String::new();
1285 }
1286 let mut buf = vec![0u8; len as usize];
1287 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1288 if read <= 0 {
1289 return String::new();
1290 }
1291 buf.truncate(read as usize);
1292 String::from_utf8(buf).unwrap_or_default()
1293 }
1294
1295 #[cfg(not(target_arch = "wasm32"))]
1296 {
1297 let secs = std::time::SystemTime::now()
1298 .duration_since(std::time::UNIX_EPOCH)
1299 .map(|d| d.as_secs())
1300 .unwrap_or(0);
1301 format!("1970-01-01T00:00:00Z+{}", secs)
1302 }
1303 }
1304
1305 pub fn generate_uuid() -> String {
1306 #[cfg(target_arch = "wasm32")]
1307 {
1308 let result = unsafe { super::generate_uuid() };
1309 if result < 0 {
1310 return String::new();
1311 }
1312 let len = unsafe { get_host_response_len() };
1313 if len <= 0 {
1314 return String::new();
1315 }
1316 let mut buf = vec![0u8; len as usize];
1317 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1318 if read <= 0 {
1319 return String::new();
1320 }
1321 buf.truncate(read as usize);
1322 String::from_utf8(buf).unwrap_or_default()
1323 }
1324
1325 #[cfg(not(target_arch = "wasm32"))]
1326 {
1327 format!(
1328 "{:08x}-{:04x}-4{:03x}-{:04x}-{:012x}",
1329 std::time::SystemTime::now()
1330 .duration_since(std::time::UNIX_EPOCH)
1331 .map(|d| d.as_nanos() as u32)
1332 .unwrap_or(0),
1333 std::process::id() as u16,
1334 0u16,
1335 0x8000u16,
1336 0u64,
1337 )
1338 }
1339 }
1340}
1341
1342#[doc(hidden)]
1346pub fn __run_handler<F>(ptr: i32, len: i32, f: F) -> i32
1347where
1348 F: FnOnce(Request) -> Response,
1349{
1350 let request_json = unsafe {
1352 let slice = std::slice::from_raw_parts(ptr as *const u8, len as usize);
1353 String::from_utf8_lossy(slice).into_owned()
1354 };
1355
1356 let request = Request::from_json(&request_json).unwrap_or_else(|| Request {
1358 method: "GET".to_string(),
1359 handler: String::new(),
1360 headers: HashMap::new(),
1361 body: Value::Null,
1362 raw_body: Vec::new(),
1363 tenant: String::new(),
1364 service: String::new(),
1365 auth: None,
1366 });
1367
1368 let response = f(request);
1370 let response_bytes = response.into_data().into_bytes();
1371
1372 let total = 4 + response_bytes.len();
1374 let layout = std::alloc::Layout::from_size_align(total, 1).expect("invalid layout");
1375 let out_ptr = unsafe { std::alloc::alloc(layout) };
1376
1377 unsafe {
1378 let len_bytes = (response_bytes.len() as u32).to_le_bytes();
1379 std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), out_ptr, 4);
1380 std::ptr::copy_nonoverlapping(
1381 response_bytes.as_ptr(),
1382 out_ptr.add(4),
1383 response_bytes.len(),
1384 );
1385 }
1386
1387 out_ptr as i32
1388}
1389
1390#[macro_export]
1403macro_rules! init {
1404 () => {
1405 #[no_mangle]
1406 pub extern "C" fn alloc(size: i32) -> i32 {
1407 let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
1408 unsafe { std::alloc::alloc(layout) as i32 }
1409 }
1410 };
1411}
1412
1413#[macro_export]
1440macro_rules! handler {
1441 ($name:ident, |$req:ident : Request| $body:expr) => {
1442 #[no_mangle]
1443 pub extern "C" fn $name(ptr: i32, len: i32) -> i32 {
1444 $crate::__run_handler(ptr, len, |$req: $crate::Request| $body)
1445 }
1446 };
1447}
1448
1449pub mod migrate {
1488 use super::{Request, Response};
1489 pub use cufflink_types::SchemaDiff;
1490
1491 pub fn run<F>(req: Request, handler: F) -> Response
1498 where
1499 F: FnOnce(SchemaDiff) -> Result<(), String>,
1500 {
1501 match serde_json::from_value::<SchemaDiff>(req.body().clone()) {
1502 Ok(diff) => match handler(diff) {
1503 Ok(()) => Response::json(&serde_json::json!({"ok": true})),
1504 Err(e) => Response::error(&e),
1505 },
1506 Err(e) => Response::error(&format!(
1507 "on_migrate: failed to parse SchemaDiff payload: {}",
1508 e
1509 )),
1510 }
1511 }
1512}
1513
1514pub mod prelude {
1522 pub use crate::config;
1523 pub use crate::db;
1524 pub use crate::http;
1525 pub use crate::image;
1526 pub use crate::log;
1527 pub use crate::migrate;
1528 pub use crate::nats;
1529 pub use crate::redis;
1530 pub use crate::storage;
1531 pub use crate::util;
1532 pub use crate::Auth;
1533 pub use crate::Request;
1534 pub use crate::Response;
1535 pub use serde_json::{json, Value};
1536}
1537
1538#[cfg(test)]
1541mod tests {
1542 use super::*;
1543 use serde_json::json;
1544
1545 #[test]
1546 fn test_request_parsing() {
1547 let json = serde_json::to_string(&json!({
1548 "method": "POST",
1549 "handler": "checkout",
1550 "headers": {"content-type": "application/json"},
1551 "body": {"item": "widget", "qty": 3},
1552 "tenant": "acme",
1553 "service": "shop"
1554 }))
1555 .unwrap();
1556
1557 let req = Request::from_json(&json).unwrap();
1558 assert_eq!(req.method(), "POST");
1559 assert_eq!(req.handler(), "checkout");
1560 assert_eq!(req.tenant(), "acme");
1561 assert_eq!(req.service(), "shop");
1562 assert_eq!(req.body()["item"], "widget");
1563 assert_eq!(req.body()["qty"], 3);
1564 assert_eq!(req.header("content-type"), Some("application/json"));
1565 }
1566
1567 #[test]
1568 fn test_request_missing_fields() {
1569 let json = r#"{"method": "GET"}"#;
1570 let req = Request::from_json(json).unwrap();
1571 assert_eq!(req.method(), "GET");
1572 assert_eq!(req.handler(), "");
1573 assert_eq!(req.tenant(), "");
1574 assert_eq!(req.body(), &Value::Null);
1575 assert!(req.raw_body().is_empty());
1576 }
1577
1578 #[test]
1579 fn test_request_raw_body_round_trip() {
1580 use base64::{engine::general_purpose, Engine};
1581 let raw = br#"{"type":"message.delivered","data":{"object":{"id":"AC1"}}}"#;
1582 let json = serde_json::to_string(&json!({
1583 "method": "POST",
1584 "handler": "webhook",
1585 "headers": {"content-type": "application/json"},
1586 "body": serde_json::from_slice::<Value>(raw).unwrap(),
1587 "body_raw_b64": general_purpose::STANDARD.encode(raw),
1588 "tenant": "acme",
1589 "service": "shop",
1590 }))
1591 .unwrap();
1592 let req = Request::from_json(&json).unwrap();
1593 assert_eq!(req.raw_body(), raw);
1594 }
1595
1596 #[test]
1597 fn test_request_raw_body_invalid_base64_yields_empty() {
1598 let json = serde_json::to_string(&json!({
1599 "method": "POST",
1600 "handler": "webhook",
1601 "body": Value::Null,
1602 "body_raw_b64": "not%base64!",
1603 "tenant": "acme",
1604 "service": "shop",
1605 }))
1606 .unwrap();
1607 let req = Request::from_json(&json).unwrap();
1608 assert!(req.raw_body().is_empty());
1609 }
1610
1611 #[test]
1612 fn test_response_json() {
1613 let resp = Response::json(&json!({"status": "ok", "count": 42}));
1614 let data = resp.into_data();
1615 let parsed: Value = serde_json::from_str(&data).unwrap();
1616 assert_eq!(parsed["status"], "ok");
1617 assert_eq!(parsed["count"], 42);
1618 }
1619
1620 #[test]
1621 fn test_response_error() {
1622 let resp = Response::error("something went wrong");
1623 let data = resp.into_data();
1624 let parsed: Value = serde_json::from_str(&data).unwrap();
1625 assert_eq!(parsed["__status"], 400);
1627 assert_eq!(parsed["__body"]["error"], "something went wrong");
1628 }
1629
1630 #[test]
1631 fn test_response_not_found() {
1632 let resp = Response::not_found("item not found");
1633 let data = resp.into_data();
1634 let parsed: Value = serde_json::from_str(&data).unwrap();
1635 assert_eq!(parsed["__status"], 404);
1636 assert_eq!(parsed["__body"]["error"], "item not found");
1637 }
1638
1639 #[test]
1640 fn test_response_with_status() {
1641 let resp = Response::json(&serde_json::json!({"ok": true})).with_status(201);
1642 let data = resp.into_data();
1643 let parsed: Value = serde_json::from_str(&data).unwrap();
1644 assert_eq!(parsed["__status"], 201);
1645 assert_eq!(parsed["__body"]["ok"], true);
1646 }
1647
1648 fn migrate_request(diff: serde_json::Value) -> Request {
1649 let payload = serde_json::to_string(&json!({
1650 "method": "POST",
1651 "handler": "handle_on_migrate",
1652 "headers": {},
1653 "body": diff,
1654 "tenant": "default",
1655 "service": "logistics-service",
1656 }))
1657 .unwrap();
1658 Request::from_json(&payload).unwrap()
1659 }
1660
1661 #[test]
1662 fn test_migrate_run_success() {
1663 let req = migrate_request(json!({
1664 "added_columns": [["pickups", "min"]],
1665 "dropped_columns": [["pickups", "midpoint"]],
1666 }));
1667 let resp = migrate::run(req, |diff| {
1668 assert!(diff.added_column("pickups", "min"));
1669 assert!(diff.dropped_column("pickups", "midpoint"));
1670 Ok(())
1671 });
1672 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1673 assert_eq!(parsed["ok"], true);
1674 }
1675
1676 #[test]
1677 fn test_migrate_run_handler_error() {
1678 let req = migrate_request(json!({}));
1679 let resp = migrate::run(req, |_| Err("backfill failed".into()));
1680 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1681 assert_eq!(parsed["__status"], 400);
1682 assert_eq!(parsed["__body"]["error"], "backfill failed");
1683 }
1684
1685 #[test]
1686 fn test_migrate_run_invalid_payload() {
1687 let req = migrate_request(json!("not a diff"));
1689 let resp = migrate::run(req, |_| {
1690 panic!("closure should not be called for invalid payload")
1691 });
1692 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1693 assert_eq!(parsed["__status"], 400);
1694 assert!(parsed["__body"]["error"]
1695 .as_str()
1696 .unwrap()
1697 .contains("on_migrate: failed to parse SchemaDiff payload"));
1698 }
1699
1700 #[test]
1701 fn test_migrate_run_empty_diff() {
1702 let req = migrate_request(json!({}));
1703 let mut called = false;
1704 let resp = migrate::run(req, |diff| {
1705 assert!(diff.is_empty());
1706 called = true;
1707 Ok(())
1708 });
1709 assert!(called);
1710 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1711 assert_eq!(parsed["ok"], true);
1712 }
1713
1714 #[test]
1715 fn test_response_200_no_wrapper() {
1716 let resp = Response::json(&serde_json::json!({"data": "test"}));
1717 let data = resp.into_data();
1718 let parsed: Value = serde_json::from_str(&data).unwrap();
1719 assert_eq!(parsed["data"], "test");
1721 assert!(parsed.get("__status").is_none());
1722 }
1723
1724 #[test]
1725 fn test_response_empty() {
1726 let resp = Response::empty();
1727 let data = resp.into_data();
1728 let parsed: Value = serde_json::from_str(&data).unwrap();
1729 assert_eq!(parsed["ok"], true);
1730 }
1731
1732 #[test]
1733 fn test_response_text() {
1734 let resp = Response::text("hello world");
1735 let data = resp.into_data();
1736 let parsed: Value = serde_json::from_str(&data).unwrap();
1737 assert_eq!(parsed, "hello world");
1738 }
1739
1740 #[test]
1741 fn test_db_query_noop_on_native() {
1742 let rows = db::query("SELECT 1");
1744 assert!(rows.is_empty());
1745 }
1746
1747 #[test]
1748 fn test_db_query_one_noop_on_native() {
1749 let row = db::query_one("SELECT 1");
1750 assert!(row.is_none());
1751 }
1752
1753 #[test]
1754 fn test_db_execute_noop_on_native() {
1755 let affected = db::execute("INSERT INTO x VALUES (1)");
1756 assert_eq!(affected, 0);
1757 }
1758
1759 #[test]
1760 fn test_nats_publish_noop_on_native() {
1761 let ok = nats::publish("test.subject", "payload");
1762 assert!(ok);
1763 }
1764
1765 #[test]
1766 fn test_request_with_auth() {
1767 let json = serde_json::to_string(&json!({
1768 "method": "POST",
1769 "handler": "checkout",
1770 "headers": {},
1771 "body": {},
1772 "tenant": "acme",
1773 "service": "shop",
1774 "auth": {
1775 "sub": "user-123",
1776 "preferred_username": "john",
1777 "name": "John Doe",
1778 "email": "john@example.com",
1779 "realm_roles": ["admin", "manager"],
1780 "claims": {"department": "engineering"}
1781 }
1782 }))
1783 .unwrap();
1784
1785 let req = Request::from_json(&json).unwrap();
1786 let auth = req.auth().unwrap();
1787 assert_eq!(auth.sub, "user-123");
1788 assert_eq!(auth.preferred_username.as_deref(), Some("john"));
1789 assert_eq!(auth.name.as_deref(), Some("John Doe"));
1790 assert_eq!(auth.email.as_deref(), Some("john@example.com"));
1791 assert!(auth.has_role("admin"));
1792 assert!(auth.has_role("manager"));
1793 assert!(!auth.has_role("viewer"));
1794 assert_eq!(
1795 auth.claim("department").and_then(|v| v.as_str()),
1796 Some("engineering")
1797 );
1798 }
1799
1800 #[test]
1801 fn test_request_without_auth() {
1802 let json = r#"{"method": "GET"}"#;
1803 let req = Request::from_json(json).unwrap();
1804 assert!(req.auth().is_none());
1805 }
1806
1807 #[test]
1808 fn test_request_null_auth() {
1809 let json = serde_json::to_string(&json!({
1810 "method": "GET",
1811 "auth": null
1812 }))
1813 .unwrap();
1814 let req = Request::from_json(&json).unwrap();
1815 assert!(req.auth().is_none());
1816 }
1817
1818 #[test]
1819 fn test_require_auth_success() {
1820 let json = serde_json::to_string(&json!({
1821 "method": "GET",
1822 "auth": {"sub": "user-1", "realm_roles": [], "claims": {}}
1823 }))
1824 .unwrap();
1825 let req = Request::from_json(&json).unwrap();
1826 assert!(req.require_auth().is_ok());
1827 assert_eq!(req.require_auth().unwrap().sub, "user-1");
1828 }
1829
1830 #[test]
1831 fn test_require_auth_fails_when_unauthenticated() {
1832 let json = r#"{"method": "GET"}"#;
1833 let req = Request::from_json(json).unwrap();
1834 assert!(req.require_auth().is_err());
1835 }
1836
1837 #[test]
1838 fn test_http_fetch_noop_on_native() {
1839 let resp = http::fetch("GET", "https://example.com", &[], None);
1840 assert!(resp.is_none());
1841 }
1842
1843 #[test]
1844 fn test_http_get_noop_on_native() {
1845 let resp = http::get("https://example.com", &[]);
1846 assert!(resp.is_none());
1847 }
1848
1849 #[test]
1850 fn test_http_post_noop_on_native() {
1851 let resp = http::post("https://example.com", &[], "{}");
1852 assert!(resp.is_none());
1853 }
1854
1855 #[test]
1856 fn test_storage_download_noop_on_native() {
1857 let data = storage::download("my-bucket", "images/photo.jpg");
1858 assert!(data.is_none());
1859 }
1860
1861 #[test]
1862 fn test_image_transform_jpeg_noop_on_native() {
1863 let bytes = image::transform_jpeg("my-bucket", "in.jpg", "out.jpg", 1024, 80);
1864 assert!(bytes.is_none());
1865 }
1866
1867 #[test]
1868 fn test_auth_permissions() {
1869 let json = serde_json::to_string(&json!({
1870 "method": "POST",
1871 "handler": "test",
1872 "headers": {},
1873 "body": {},
1874 "tenant": "acme",
1875 "service": "shop",
1876 "auth": {
1877 "sub": "user-1",
1878 "realm_roles": ["admin"],
1879 "claims": {},
1880 "permissions": ["staff:create", "staff:view", "items:*"],
1881 "role_names": ["admin", "manager"]
1882 }
1883 }))
1884 .unwrap();
1885
1886 let req = Request::from_json(&json).unwrap();
1887 let auth = req.auth().unwrap();
1888
1889 assert!(auth.can("staff", "create"));
1891 assert!(auth.can("staff", "view"));
1892 assert!(!auth.can("staff", "delete"));
1893
1894 assert!(auth.can("items", "create"));
1896 assert!(auth.can("items", "view"));
1897 assert!(auth.can("items", "delete"));
1898
1899 assert!(!auth.can("batches", "view"));
1901
1902 assert!(auth.has_cufflink_role("admin"));
1904 assert!(auth.has_cufflink_role("manager"));
1905 assert!(!auth.has_cufflink_role("viewer"));
1906 }
1907
1908 #[test]
1909 fn test_auth_super_wildcard() {
1910 let auth = Auth {
1911 sub: "user-1".to_string(),
1912 preferred_username: None,
1913 name: None,
1914 email: None,
1915 realm_roles: vec![],
1916 claims: HashMap::new(),
1917 permissions: vec!["*".to_string()],
1918 role_names: vec!["superadmin".to_string()],
1919 is_service_account: false,
1920 };
1921
1922 assert!(auth.can("anything", "everything"));
1923 assert!(auth.can("staff", "create"));
1924 }
1925
1926 #[test]
1927 fn test_auth_empty_permissions() {
1928 let auth = Auth {
1929 sub: "user-1".to_string(),
1930 preferred_username: None,
1931 name: None,
1932 email: None,
1933 realm_roles: vec![],
1934 claims: HashMap::new(),
1935 permissions: vec![],
1936 role_names: vec![],
1937 is_service_account: false,
1938 };
1939
1940 assert!(!auth.can("staff", "create"));
1941 assert!(!auth.has_cufflink_role("admin"));
1942 }
1943
1944 #[test]
1945 fn test_redis_get_noop_on_native() {
1946 let val = redis::get("some-key");
1947 assert!(val.is_none());
1948 }
1949
1950 #[test]
1951 fn test_redis_set_noop_on_native() {
1952 let ok = redis::set("key", "value", 3600);
1953 assert!(ok);
1954 }
1955
1956 #[test]
1957 fn test_redis_del_noop_on_native() {
1958 let ok = redis::del("key");
1959 assert!(ok);
1960 }
1961
1962 #[test]
1963 fn test_http_fetch_response_helpers() {
1964 let resp = http::FetchResponse {
1965 status: 200,
1966 body: r#"{"key": "value"}"#.to_string(),
1967 body_encoding: "utf8".to_string(),
1968 headers: HashMap::new(),
1969 };
1970 assert!(resp.is_success());
1971 assert!(!resp.is_base64());
1972 let json = resp.json().unwrap();
1973 assert_eq!(json["key"], "value");
1974
1975 let err_resp = http::FetchResponse {
1976 status: 404,
1977 body: "not found".to_string(),
1978 body_encoding: "utf8".to_string(),
1979 headers: HashMap::new(),
1980 };
1981 assert!(!err_resp.is_success());
1982
1983 let binary_resp = http::FetchResponse {
1984 status: 200,
1985 body: "aW1hZ2VkYXRh".to_string(),
1986 body_encoding: "base64".to_string(),
1987 headers: HashMap::new(),
1988 };
1989 assert!(binary_resp.is_base64());
1990 }
1991}