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 redis_get_status(key_ptr: i32, key_len: i32) -> i32;
109 fn redis_mget(keys_ptr: i32, keys_len: i32) -> i32;
110 fn generate_uuid() -> i32;
111 fn current_time() -> i32;
112 fn context_tenant() -> i32;
113}
114
115#[derive(Debug, Clone)]
136pub struct Auth {
137 pub sub: String,
139 pub preferred_username: Option<String>,
141 pub name: Option<String>,
143 pub email: Option<String>,
145 pub realm_roles: Vec<String>,
147 pub claims: HashMap<String, Value>,
149 pub permissions: Vec<String>,
151 pub role_names: Vec<String>,
153 pub is_service_account: bool,
156}
157
158impl Auth {
159 pub fn has_role(&self, role: &str) -> bool {
161 self.realm_roles.iter().any(|r| r == role)
162 }
163
164 pub fn can(&self, area: &str, operation: &str) -> bool {
175 let required = format!("{}:{}", area, operation);
176 let wildcard = format!("{}:*", area);
177 self.permissions
178 .iter()
179 .any(|p| p == &required || p == &wildcard || p == "*")
180 }
181
182 pub fn has_cufflink_role(&self, role: &str) -> bool {
184 self.role_names.iter().any(|r| r == role)
185 }
186
187 pub fn claim(&self, key: &str) -> Option<&Value> {
189 self.claims.get(key)
190 }
191}
192
193#[derive(Debug, Clone)]
202pub struct JobContext {
203 pub id: String,
204 pub attempt: u32,
205 pub max_attempts: u32,
206}
207
208impl JobContext {
209 pub fn is_retry(&self) -> bool {
211 self.attempt > 1
212 }
213}
214
215#[derive(Debug, Clone)]
222pub struct Request {
223 method: String,
224 handler: String,
225 headers: HashMap<String, String>,
226 body: Value,
227 raw_body: Vec<u8>,
228 tenant: String,
229 service: String,
230 auth: Option<Auth>,
231 job: Option<JobContext>,
232}
233
234impl Request {
235 pub fn from_json(json: &str) -> Option<Self> {
237 let v: Value = serde_json::from_str(json).ok()?;
238 let headers = v["headers"]
239 .as_object()
240 .map(|m| {
241 m.iter()
242 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
243 .collect()
244 })
245 .unwrap_or_default();
246
247 let auth = v["auth"].as_object().map(|auth_obj| {
248 let a = Value::Object(auth_obj.clone());
249 Auth {
250 sub: a["sub"].as_str().unwrap_or("").to_string(),
251 preferred_username: a["preferred_username"].as_str().map(|s| s.to_string()),
252 name: a["name"].as_str().map(|s| s.to_string()),
253 email: a["email"].as_str().map(|s| s.to_string()),
254 realm_roles: a["realm_roles"]
255 .as_array()
256 .map(|arr| {
257 arr.iter()
258 .filter_map(|v| v.as_str().map(|s| s.to_string()))
259 .collect()
260 })
261 .unwrap_or_default(),
262 claims: a["claims"]
263 .as_object()
264 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
265 .unwrap_or_default(),
266 permissions: a["permissions"]
267 .as_array()
268 .map(|arr| {
269 arr.iter()
270 .filter_map(|v| v.as_str().map(|s| s.to_string()))
271 .collect()
272 })
273 .unwrap_or_default(),
274 role_names: a["role_names"]
275 .as_array()
276 .map(|arr| {
277 arr.iter()
278 .filter_map(|v| v.as_str().map(|s| s.to_string()))
279 .collect()
280 })
281 .unwrap_or_default(),
282 is_service_account: a["is_service_account"].as_bool().unwrap_or(false),
283 }
284 });
285
286 let raw_body = v["body_raw_b64"]
287 .as_str()
288 .filter(|s| !s.is_empty())
289 .and_then(|s| {
290 use base64::{engine::general_purpose, Engine};
291 general_purpose::STANDARD.decode(s).ok()
292 })
293 .unwrap_or_default();
294
295 let job = v["_job"].as_object().and_then(|j| {
296 let id = j.get("id")?.as_str()?.to_string();
297 let attempt = u32::try_from(j.get("attempt")?.as_u64()?).ok()?;
298 let max_attempts = u32::try_from(j.get("max_attempts")?.as_u64()?).ok()?;
299 Some(JobContext {
300 id,
301 attempt,
302 max_attempts,
303 })
304 });
305
306 Some(Self {
307 method: v["method"].as_str().unwrap_or("GET").to_string(),
308 handler: v["handler"].as_str().unwrap_or("").to_string(),
309 headers,
310 body: v["body"].clone(),
311 raw_body,
312 tenant: v["tenant"].as_str().unwrap_or("").to_string(),
313 service: v["service"].as_str().unwrap_or("").to_string(),
314 auth,
315 job,
316 })
317 }
318
319 pub fn method(&self) -> &str {
321 &self.method
322 }
323
324 pub fn handler(&self) -> &str {
326 &self.handler
327 }
328
329 pub fn headers(&self) -> &HashMap<String, String> {
331 &self.headers
332 }
333
334 pub fn header(&self, name: &str) -> Option<&str> {
336 self.headers.get(name).map(|s| s.as_str())
337 }
338
339 pub fn body(&self) -> &Value {
341 &self.body
342 }
343
344 pub fn raw_body(&self) -> &[u8] {
352 &self.raw_body
353 }
354
355 pub fn tenant(&self) -> &str {
357 &self.tenant
358 }
359
360 pub fn service(&self) -> &str {
362 &self.service
363 }
364
365 pub fn auth(&self) -> Option<&Auth> {
369 self.auth.as_ref()
370 }
371
372 pub fn job(&self) -> Option<&JobContext> {
377 self.job.as_ref()
378 }
379
380 pub fn require_auth(&self) -> Result<&Auth, Response> {
392 self.auth.as_ref().ok_or_else(|| {
393 Response::json(&serde_json::json!({
394 "error": "Authentication required",
395 "status": 401
396 }))
397 })
398 }
399}
400
401#[derive(Debug, Clone)]
405pub struct Response {
406 data: String,
407 status: u16,
408}
409
410impl Response {
411 pub fn json(value: &Value) -> Self {
413 Self {
414 data: serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()),
415 status: 200,
416 }
417 }
418
419 pub fn text(s: &str) -> Self {
421 Self::json(&Value::String(s.to_string()))
422 }
423
424 pub fn error(message: &str) -> Self {
426 Self {
427 data: serde_json::json!({"error": message}).to_string(),
428 status: 400,
429 }
430 }
431
432 pub fn not_found(message: &str) -> Self {
434 Self {
435 data: serde_json::json!({"error": message}).to_string(),
436 status: 404,
437 }
438 }
439
440 pub fn forbidden(message: &str) -> Self {
442 Self {
443 data: serde_json::json!({"error": message}).to_string(),
444 status: 403,
445 }
446 }
447
448 pub fn empty() -> Self {
450 Self::json(&serde_json::json!({"ok": true}))
451 }
452
453 pub fn with_status(mut self, status: u16) -> Self {
455 self.status = status;
456 self
457 }
458
459 pub fn into_data(self) -> String {
462 if self.status == 200 {
463 self.data
465 } else {
466 serde_json::json!({
468 "__status": self.status,
469 "__body": serde_json::from_str::<Value>(&self.data).unwrap_or(Value::String(self.data)),
470 })
471 .to_string()
472 }
473 }
474}
475
476pub mod db {
483 use super::*;
484
485 pub fn query(sql: &str) -> Vec<Value> {
496 #[cfg(target_arch = "wasm32")]
497 {
498 let bytes = sql.as_bytes();
499 let result = unsafe { db_query(bytes.as_ptr() as i32, bytes.len() as i32) };
500 if result < 0 {
501 return vec![];
502 }
503 read_host_response()
504 }
505 #[cfg(not(target_arch = "wasm32"))]
506 {
507 let _ = sql;
508 vec![]
509 }
510 }
511
512 pub fn query_one(sql: &str) -> Option<Value> {
520 query(sql).into_iter().next()
521 }
522
523 pub fn execute(sql: &str) -> i32 {
532 #[cfg(target_arch = "wasm32")]
533 {
534 let bytes = sql.as_bytes();
535 unsafe { db_execute(bytes.as_ptr() as i32, bytes.len() as i32) }
536 }
537 #[cfg(not(target_arch = "wasm32"))]
538 {
539 let _ = sql;
540 0
541 }
542 }
543
544 #[cfg(target_arch = "wasm32")]
546 fn read_host_response() -> Vec<Value> {
547 let len = unsafe { get_host_response_len() };
548 if len <= 0 {
549 return vec![];
550 }
551 let mut buf = vec![0u8; len as usize];
552 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
553 if read <= 0 {
554 return vec![];
555 }
556 buf.truncate(read as usize);
557 let json_str = String::from_utf8_lossy(&buf);
558 serde_json::from_str(&json_str).unwrap_or_default()
559 }
560}
561
562pub mod nats {
569 #[allow(unused_imports)]
570 use super::*;
571
572 pub fn publish(subject: &str, payload: &str) -> bool {
583 #[cfg(target_arch = "wasm32")]
584 {
585 let subj_bytes = subject.as_bytes();
586 let payload_bytes = payload.as_bytes();
587 let result = unsafe {
588 nats_publish(
589 subj_bytes.as_ptr() as i32,
590 subj_bytes.len() as i32,
591 payload_bytes.as_ptr() as i32,
592 payload_bytes.len() as i32,
593 )
594 };
595 result == 0
596 }
597 #[cfg(not(target_arch = "wasm32"))]
598 {
599 let _ = (subject, payload);
600 true
601 }
602 }
603
604 pub fn request(subject: &str, payload: &str, timeout_ms: i32) -> Option<String> {
616 #[cfg(target_arch = "wasm32")]
617 {
618 let subj_bytes = subject.as_bytes();
619 let payload_bytes = payload.as_bytes();
620 let result = unsafe {
621 nats_request(
622 subj_bytes.as_ptr() as i32,
623 subj_bytes.len() as i32,
624 payload_bytes.as_ptr() as i32,
625 payload_bytes.len() as i32,
626 timeout_ms,
627 )
628 };
629 if result != 0 {
630 return None;
631 }
632 let len = unsafe { get_host_response_len() };
633 if len <= 0 {
634 return None;
635 }
636 let mut buf = vec![0u8; len as usize];
637 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
638 if read <= 0 {
639 return None;
640 }
641 String::from_utf8(buf[..read as usize].to_vec()).ok()
642 }
643 #[cfg(not(target_arch = "wasm32"))]
644 {
645 let _ = (subject, payload, timeout_ms);
646 None
647 }
648 }
649}
650
651pub mod log {
657 #[allow(unused_imports)]
658 use super::*;
659
660 pub fn error(msg: &str) {
662 write(0, msg);
663 }
664
665 pub fn warn(msg: &str) {
667 write(1, msg);
668 }
669
670 pub fn info(msg: &str) {
672 write(2, msg);
673 }
674
675 pub fn debug(msg: &str) {
677 write(3, msg);
678 }
679
680 fn write(level: i32, msg: &str) {
681 #[cfg(target_arch = "wasm32")]
682 {
683 let bytes = msg.as_bytes();
684 unsafe {
685 super::cufflink_log(level, bytes.as_ptr() as i32, bytes.len() as i32);
686 }
687 }
688 #[cfg(not(target_arch = "wasm32"))]
689 {
690 let _ = (level, msg);
691 }
692 }
693}
694
695pub mod http {
702 #[allow(unused_imports)]
703 use super::*;
704
705 #[derive(Debug, Clone)]
707 pub struct FetchResponse {
708 pub status: i32,
710 pub body: String,
712 pub body_encoding: String,
714 pub headers: HashMap<String, String>,
716 }
717
718 impl FetchResponse {
719 pub fn json(&self) -> Option<Value> {
721 serde_json::from_str(&self.body).ok()
722 }
723
724 pub fn is_success(&self) -> bool {
726 (200..300).contains(&self.status)
727 }
728
729 pub fn is_base64(&self) -> bool {
731 self.body_encoding == "base64"
732 }
733 }
734
735 pub fn fetch(
746 method: &str,
747 url: &str,
748 headers: &[(&str, &str)],
749 body: Option<&str>,
750 ) -> Option<FetchResponse> {
751 #[cfg(target_arch = "wasm32")]
752 {
753 let method_bytes = method.as_bytes();
754 let url_bytes = url.as_bytes();
755 let headers_map: HashMap<&str, &str> = headers.iter().copied().collect();
756 let headers_json = serde_json::to_string(&headers_map).unwrap_or_default();
757 let headers_bytes = headers_json.as_bytes();
758 let body_bytes = body.unwrap_or("").as_bytes();
759 let body_len = body.map(|b| b.len()).unwrap_or(0);
760
761 let result = unsafe {
762 http_fetch(
763 method_bytes.as_ptr() as i32,
764 method_bytes.len() as i32,
765 url_bytes.as_ptr() as i32,
766 url_bytes.len() as i32,
767 headers_bytes.as_ptr() as i32,
768 headers_bytes.len() as i32,
769 body_bytes.as_ptr() as i32,
770 body_len as i32,
771 )
772 };
773
774 if result < 0 {
775 return None;
776 }
777
778 read_fetch_response()
779 }
780 #[cfg(not(target_arch = "wasm32"))]
781 {
782 let _ = (method, url, headers, body);
783 None
784 }
785 }
786
787 pub fn get(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
789 fetch("GET", url, headers, None)
790 }
791
792 pub fn post(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
794 fetch("POST", url, headers, Some(body))
795 }
796
797 pub fn put(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
799 fetch("PUT", url, headers, Some(body))
800 }
801
802 pub fn delete(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
804 fetch("DELETE", url, headers, None)
805 }
806
807 pub fn patch(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
809 fetch("PATCH", url, headers, Some(body))
810 }
811
812 #[derive(Debug, Clone)]
814 pub struct FetchRequest<'a> {
815 pub method: &'a str,
816 pub url: &'a str,
817 pub headers: &'a [(&'a str, &'a str)],
818 pub body: Option<&'a str>,
819 }
820
821 pub fn fetch_many(requests: &[FetchRequest<'_>]) -> Vec<Result<FetchResponse, String>> {
836 #[cfg(target_arch = "wasm32")]
837 {
838 let items: Vec<Value> = requests
839 .iter()
840 .map(|r| {
841 let h: HashMap<&str, &str> = r.headers.iter().copied().collect();
842 serde_json::json!({
843 "method": r.method,
844 "url": r.url,
845 "headers": h,
846 "body": r.body,
847 })
848 })
849 .collect();
850 let payload = serde_json::to_string(&items).unwrap_or_else(|_| "[]".to_string());
851 let bytes = payload.as_bytes();
852 let rc = unsafe { super::http_fetch_many(bytes.as_ptr() as i32, bytes.len() as i32) };
853 if rc < 0 {
854 return requests
855 .iter()
856 .map(|_| Err("http_fetch_many host call failed".to_string()))
857 .collect();
858 }
859 super::read_batch_response()
860 .map(|v| v.into_iter().map(parse_fetch_slot).collect())
861 .unwrap_or_else(|| {
862 requests
863 .iter()
864 .map(|_| Err("malformed host response".to_string()))
865 .collect()
866 })
867 }
868 #[cfg(not(target_arch = "wasm32"))]
869 {
870 let _ = requests;
871 vec![]
872 }
873 }
874
875 #[cfg(target_arch = "wasm32")]
876 fn fetch_response_from_json(v: &Value) -> FetchResponse {
877 FetchResponse {
878 status: v["status"].as_i64().unwrap_or(0) as i32,
879 body: v["body"].as_str().unwrap_or("").to_string(),
880 body_encoding: v["body_encoding"].as_str().unwrap_or("utf8").to_string(),
881 headers: v["headers"]
882 .as_object()
883 .map(|m| {
884 m.iter()
885 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
886 .collect()
887 })
888 .unwrap_or_default(),
889 }
890 }
891
892 #[cfg(target_arch = "wasm32")]
893 fn parse_fetch_slot(slot: Value) -> Result<FetchResponse, String> {
894 if !slot["ok"].as_bool().unwrap_or(false) {
895 return Err(slot["error"]
896 .as_str()
897 .unwrap_or("unknown error")
898 .to_string());
899 }
900 Ok(fetch_response_from_json(&slot))
901 }
902
903 #[cfg(target_arch = "wasm32")]
905 fn read_fetch_response() -> Option<FetchResponse> {
906 let len = unsafe { get_host_response_len() };
907 if len <= 0 {
908 return None;
909 }
910 let mut buf = vec![0u8; len as usize];
911 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
912 if read <= 0 {
913 return None;
914 }
915 buf.truncate(read as usize);
916 let json_str = String::from_utf8_lossy(&buf);
917 let v: Value = serde_json::from_str(&json_str).ok()?;
918 Some(fetch_response_from_json(&v))
919 }
920}
921
922pub mod config {
930 #[allow(unused_imports)]
931 use super::*;
932
933 pub fn get(key: &str) -> Option<String> {
942 #[cfg(target_arch = "wasm32")]
943 {
944 let bytes = key.as_bytes();
945 let result = unsafe { get_config(bytes.as_ptr() as i32, bytes.len() as i32) };
946 if result < 0 {
947 return None;
948 }
949 read_config_response()
950 }
951 #[cfg(not(target_arch = "wasm32"))]
952 {
953 let _ = key;
954 None
955 }
956 }
957
958 #[cfg(target_arch = "wasm32")]
960 fn read_config_response() -> Option<String> {
961 let len = unsafe { get_host_response_len() };
962 if len <= 0 {
963 return None;
964 }
965 let mut buf = vec![0u8; len as usize];
966 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
967 if read <= 0 {
968 return None;
969 }
970 buf.truncate(read as usize);
971 String::from_utf8(buf).ok()
972 }
973}
974
975pub mod storage {
983 #[allow(unused_imports)]
984 use super::*;
985
986 pub fn download(bucket: &str, key: &str) -> Option<String> {
997 #[cfg(target_arch = "wasm32")]
998 {
999 let bucket_bytes = bucket.as_bytes();
1000 let key_bytes = key.as_bytes();
1001 let result = unsafe {
1002 s3_download(
1003 bucket_bytes.as_ptr() as i32,
1004 bucket_bytes.len() as i32,
1005 key_bytes.as_ptr() as i32,
1006 key_bytes.len() 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);
1017 None
1018 }
1019 }
1020
1021 pub fn presign_upload(
1034 bucket: &str,
1035 key: &str,
1036 content_type: &str,
1037 expires_secs: u64,
1038 ) -> Option<String> {
1039 #[cfg(target_arch = "wasm32")]
1040 {
1041 let bucket_bytes = bucket.as_bytes();
1042 let key_bytes = key.as_bytes();
1043 let ct_bytes = content_type.as_bytes();
1044 let result = unsafe {
1045 s3_presign_upload(
1046 bucket_bytes.as_ptr() as i32,
1047 bucket_bytes.len() as i32,
1048 key_bytes.as_ptr() as i32,
1049 key_bytes.len() as i32,
1050 ct_bytes.as_ptr() as i32,
1051 ct_bytes.len() as i32,
1052 expires_secs as i32,
1053 )
1054 };
1055 if result < 0 {
1056 return None;
1057 }
1058 read_storage_response()
1059 }
1060 #[cfg(not(target_arch = "wasm32"))]
1061 {
1062 let _ = (bucket, key, content_type, expires_secs);
1063 None
1064 }
1065 }
1066
1067 #[cfg(target_arch = "wasm32")]
1069 fn read_storage_response() -> Option<String> {
1070 let len = unsafe { get_host_response_len() };
1071 if len <= 0 {
1072 return None;
1073 }
1074 let mut buf = vec![0u8; len as usize];
1075 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1076 if read <= 0 {
1077 return None;
1078 }
1079 buf.truncate(read as usize);
1080 String::from_utf8(buf).ok()
1081 }
1082
1083 pub fn download_many(items: &[(&str, &str)]) -> Vec<Result<Vec<u8>, String>> {
1088 #[cfg(target_arch = "wasm32")]
1089 {
1090 let payload_items: Vec<Value> = items
1091 .iter()
1092 .map(|(b, k)| serde_json::json!({ "bucket": b, "key": k }))
1093 .collect();
1094 let payload = serde_json::to_string(&payload_items).unwrap_or_else(|_| "[]".into());
1095 let bytes = payload.as_bytes();
1096 let rc = unsafe { super::s3_download_many(bytes.as_ptr() as i32, bytes.len() as i32) };
1097 if rc < 0 {
1098 return items
1099 .iter()
1100 .map(|_| Err("s3_download_many host call failed".to_string()))
1101 .collect();
1102 }
1103 super::read_batch_response()
1104 .map(|v| v.into_iter().map(parse_download_slot).collect())
1105 .unwrap_or_else(|| {
1106 items
1107 .iter()
1108 .map(|_| Err("malformed host response".to_string()))
1109 .collect()
1110 })
1111 }
1112 #[cfg(not(target_arch = "wasm32"))]
1113 {
1114 let _ = items;
1115 vec![]
1116 }
1117 }
1118
1119 #[cfg(target_arch = "wasm32")]
1120 fn parse_download_slot(slot: Value) -> Result<Vec<u8>, String> {
1121 if !slot["ok"].as_bool().unwrap_or(false) {
1122 return Err(slot["error"]
1123 .as_str()
1124 .unwrap_or("unknown error")
1125 .to_string());
1126 }
1127 let b64 = slot["data_b64"]
1128 .as_str()
1129 .ok_or_else(|| "missing data_b64".to_string())?;
1130 use base64::{engine::general_purpose, Engine};
1131 general_purpose::STANDARD
1132 .decode(b64)
1133 .map_err(|e| format!("base64 decode failed: {}", e))
1134 }
1135}
1136
1137pub mod image {
1147 #[allow(unused_imports)]
1148 use super::*;
1149
1150 pub fn transform_jpeg(
1161 bucket: &str,
1162 in_key: &str,
1163 out_key: &str,
1164 max_dim: u32,
1165 quality: u8,
1166 ) -> Option<u32> {
1167 #[cfg(target_arch = "wasm32")]
1168 {
1169 let bucket_bytes = bucket.as_bytes();
1170 let in_key_bytes = in_key.as_bytes();
1171 let out_key_bytes = out_key.as_bytes();
1172 let result = unsafe {
1173 super::image_transform_jpeg(
1174 bucket_bytes.as_ptr() as i32,
1175 bucket_bytes.len() as i32,
1176 in_key_bytes.as_ptr() as i32,
1177 in_key_bytes.len() as i32,
1178 out_key_bytes.as_ptr() as i32,
1179 out_key_bytes.len() as i32,
1180 max_dim as i32,
1181 quality as i32,
1182 )
1183 };
1184 if result < 0 {
1185 return None;
1186 }
1187 Some(result as u32)
1188 }
1189 #[cfg(not(target_arch = "wasm32"))]
1190 {
1191 let _ = (bucket, in_key, out_key, max_dim, quality);
1192 None
1193 }
1194 }
1195}
1196
1197pub mod redis {
1204 #[allow(unused_imports)]
1205 use super::*;
1206
1207 pub fn get(key: &str) -> Option<String> {
1216 #[cfg(target_arch = "wasm32")]
1217 {
1218 let bytes = key.as_bytes();
1219 let result = unsafe { redis_get(bytes.as_ptr() as i32, bytes.len() as i32) };
1220 if result < 0 {
1221 return None;
1222 }
1223 read_redis_response()
1224 }
1225 #[cfg(not(target_arch = "wasm32"))]
1226 {
1227 let _ = key;
1228 None
1229 }
1230 }
1231
1232 pub fn set(key: &str, value: &str, ttl_secs: i32) -> bool {
1240 #[cfg(target_arch = "wasm32")]
1241 {
1242 let key_bytes = key.as_bytes();
1243 let val_bytes = value.as_bytes();
1244 let result = unsafe {
1245 redis_set(
1246 key_bytes.as_ptr() as i32,
1247 key_bytes.len() as i32,
1248 val_bytes.as_ptr() as i32,
1249 val_bytes.len() as i32,
1250 ttl_secs,
1251 )
1252 };
1253 result == 0
1254 }
1255 #[cfg(not(target_arch = "wasm32"))]
1256 {
1257 let _ = (key, value, ttl_secs);
1258 true
1259 }
1260 }
1261
1262 pub fn del(key: &str) -> bool {
1270 #[cfg(target_arch = "wasm32")]
1271 {
1272 let bytes = key.as_bytes();
1273 let result = unsafe { redis_del(bytes.as_ptr() as i32, bytes.len() as i32) };
1274 result == 0
1275 }
1276 #[cfg(not(target_arch = "wasm32"))]
1277 {
1278 let _ = key;
1279 true
1280 }
1281 }
1282
1283 #[cfg(target_arch = "wasm32")]
1285 fn read_redis_response() -> Option<String> {
1286 let len = unsafe { get_host_response_len() };
1287 if len <= 0 {
1288 return None;
1289 }
1290 let mut buf = vec![0u8; len as usize];
1291 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1292 if read <= 0 {
1293 return None;
1294 }
1295 buf.truncate(read as usize);
1296 String::from_utf8(buf).ok()
1297 }
1298
1299 pub fn get_with_status(key: &str) -> Result<Option<String>, String> {
1306 #[cfg(target_arch = "wasm32")]
1307 {
1308 let bytes = key.as_bytes();
1309 let code =
1310 unsafe { super::redis_get_status(bytes.as_ptr() as i32, bytes.len() as i32) };
1311 if code == 1 {
1312 return Ok(None);
1313 }
1314 let body = super::read_host_response_string();
1315 if code < 0 {
1316 Err(err_from_response(body))
1317 } else {
1318 Ok(Some(body))
1319 }
1320 }
1321 #[cfg(not(target_arch = "wasm32"))]
1322 {
1323 let _ = key;
1324 Ok(None)
1325 }
1326 }
1327
1328 pub fn mget(keys: &[&str]) -> Result<Vec<Option<String>>, String> {
1335 if keys.is_empty() {
1336 return Ok(Vec::new());
1337 }
1338 #[cfg(target_arch = "wasm32")]
1339 {
1340 let payload = serde_json::to_string(keys).map_err(|e| e.to_string())?;
1341 let bytes = payload.as_bytes();
1342 let code = unsafe { super::redis_mget(bytes.as_ptr() as i32, bytes.len() as i32) };
1343 let response = super::read_host_response_string();
1344 if code < 0 {
1345 return Err(err_from_response(response));
1346 }
1347 serde_json::from_str(&response).map_err(|e| e.to_string())
1348 }
1349 #[cfg(not(target_arch = "wasm32"))]
1350 {
1351 Ok(vec![None; keys.len()])
1352 }
1353 }
1354
1355 #[cfg(target_arch = "wasm32")]
1356 fn err_from_response(body: String) -> String {
1357 if body.is_empty() {
1358 "redis error".to_string()
1359 } else {
1360 body
1361 }
1362 }
1363}
1364
1365#[cfg(target_arch = "wasm32")]
1370fn read_host_response_string() -> String {
1371 let len = unsafe { get_host_response_len() };
1372 if len <= 0 {
1373 return String::new();
1374 }
1375 let mut buf = vec![0u8; len as usize];
1376 let read = unsafe { get_host_response(buf.as_mut_ptr() as i32, len) };
1377 if read <= 0 {
1378 return String::new();
1379 }
1380 buf.truncate(read as usize);
1381 String::from_utf8(buf).unwrap_or_default()
1382}
1383
1384pub mod util {
1388 #[allow(unused_imports)]
1389 use super::*;
1390
1391 pub fn current_time() -> String {
1406 #[cfg(target_arch = "wasm32")]
1407 {
1408 if unsafe { super::current_time() } < 0 {
1409 return String::new();
1410 }
1411 super::read_host_response_string()
1412 }
1413
1414 #[cfg(not(target_arch = "wasm32"))]
1415 {
1416 let secs = std::time::SystemTime::now()
1417 .duration_since(std::time::UNIX_EPOCH)
1418 .map(|d| d.as_secs())
1419 .unwrap_or(0);
1420 format!("1970-01-01T00:00:00Z+{}", secs)
1421 }
1422 }
1423
1424 pub fn generate_uuid() -> String {
1425 #[cfg(target_arch = "wasm32")]
1426 {
1427 if unsafe { super::generate_uuid() } < 0 {
1428 return String::new();
1429 }
1430 super::read_host_response_string()
1431 }
1432
1433 #[cfg(not(target_arch = "wasm32"))]
1434 {
1435 format!(
1436 "{:08x}-{:04x}-4{:03x}-{:04x}-{:012x}",
1437 std::time::SystemTime::now()
1438 .duration_since(std::time::UNIX_EPOCH)
1439 .map(|d| d.as_nanos() as u32)
1440 .unwrap_or(0),
1441 std::process::id() as u16,
1442 0u16,
1443 0x8000u16,
1444 0u64,
1445 )
1446 }
1447 }
1448}
1449
1450pub mod context {
1456 #[allow(unused_imports)]
1457 use super::*;
1458
1459 pub fn tenant() -> String {
1463 #[cfg(target_arch = "wasm32")]
1464 {
1465 if unsafe { super::context_tenant() } != 0 {
1466 return String::new();
1467 }
1468 super::read_host_response_string()
1469 }
1470 #[cfg(not(target_arch = "wasm32"))]
1471 {
1472 String::new()
1473 }
1474 }
1475}
1476
1477#[doc(hidden)]
1481pub fn __run_handler<F>(ptr: i32, len: i32, f: F) -> i32
1482where
1483 F: FnOnce(Request) -> Response,
1484{
1485 let request_json = unsafe {
1487 let slice = std::slice::from_raw_parts(ptr as *const u8, len as usize);
1488 String::from_utf8_lossy(slice).into_owned()
1489 };
1490
1491 let request = Request::from_json(&request_json).unwrap_or_else(|| Request {
1493 method: "GET".to_string(),
1494 handler: String::new(),
1495 headers: HashMap::new(),
1496 body: Value::Null,
1497 raw_body: Vec::new(),
1498 tenant: String::new(),
1499 service: String::new(),
1500 auth: None,
1501 job: None,
1502 });
1503
1504 let response = f(request);
1506 let response_bytes = response.into_data().into_bytes();
1507
1508 let total = 4 + response_bytes.len();
1510 let layout = std::alloc::Layout::from_size_align(total, 1).expect("invalid layout");
1511 let out_ptr = unsafe { std::alloc::alloc(layout) };
1512
1513 unsafe {
1514 let len_bytes = (response_bytes.len() as u32).to_le_bytes();
1515 std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), out_ptr, 4);
1516 std::ptr::copy_nonoverlapping(
1517 response_bytes.as_ptr(),
1518 out_ptr.add(4),
1519 response_bytes.len(),
1520 );
1521 }
1522
1523 out_ptr as i32
1524}
1525
1526#[macro_export]
1539macro_rules! init {
1540 () => {
1541 #[no_mangle]
1542 pub extern "C" fn alloc(size: i32) -> i32 {
1543 let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
1544 unsafe { std::alloc::alloc(layout) as i32 }
1545 }
1546 };
1547}
1548
1549#[macro_export]
1576macro_rules! handler {
1577 ($name:ident, |$req:ident : Request| $body:expr) => {
1578 #[no_mangle]
1579 pub extern "C" fn $name(ptr: i32, len: i32) -> i32 {
1580 $crate::__run_handler(ptr, len, |$req: $crate::Request| $body)
1581 }
1582 };
1583}
1584
1585pub mod migrate {
1624 use super::{Request, Response};
1625 pub use cufflink_types::SchemaDiff;
1626
1627 pub fn run<F>(req: Request, handler: F) -> Response
1634 where
1635 F: FnOnce(SchemaDiff) -> Result<(), String>,
1636 {
1637 match serde_json::from_value::<SchemaDiff>(req.body().clone()) {
1638 Ok(diff) => match handler(diff) {
1639 Ok(()) => Response::json(&serde_json::json!({"ok": true})),
1640 Err(e) => Response::error(&e),
1641 },
1642 Err(e) => Response::error(&format!(
1643 "on_migrate: failed to parse SchemaDiff payload: {}",
1644 e
1645 )),
1646 }
1647 }
1648}
1649
1650pub mod prelude {
1658 pub use crate::config;
1659 pub use crate::context;
1660 pub use crate::db;
1661 pub use crate::http;
1662 pub use crate::image;
1663 pub use crate::log;
1664 pub use crate::migrate;
1665 pub use crate::nats;
1666 pub use crate::redis;
1667 pub use crate::storage;
1668 pub use crate::util;
1669 pub use crate::Auth;
1670 pub use crate::JobContext;
1671 pub use crate::Request;
1672 pub use crate::Response;
1673 pub use serde_json::{json, Value};
1674}
1675
1676#[cfg(test)]
1679mod tests {
1680 use super::*;
1681 use serde_json::json;
1682
1683 #[test]
1684 fn test_request_parsing() {
1685 let json = serde_json::to_string(&json!({
1686 "method": "POST",
1687 "handler": "checkout",
1688 "headers": {"content-type": "application/json"},
1689 "body": {"item": "widget", "qty": 3},
1690 "tenant": "acme",
1691 "service": "shop"
1692 }))
1693 .unwrap();
1694
1695 let req = Request::from_json(&json).unwrap();
1696 assert_eq!(req.method(), "POST");
1697 assert_eq!(req.handler(), "checkout");
1698 assert_eq!(req.tenant(), "acme");
1699 assert_eq!(req.service(), "shop");
1700 assert_eq!(req.body()["item"], "widget");
1701 assert_eq!(req.body()["qty"], 3);
1702 assert_eq!(req.header("content-type"), Some("application/json"));
1703 }
1704
1705 #[test]
1706 fn test_request_missing_fields() {
1707 let json = r#"{"method": "GET"}"#;
1708 let req = Request::from_json(json).unwrap();
1709 assert_eq!(req.method(), "GET");
1710 assert_eq!(req.handler(), "");
1711 assert_eq!(req.tenant(), "");
1712 assert_eq!(req.body(), &Value::Null);
1713 assert!(req.raw_body().is_empty());
1714 }
1715
1716 #[test]
1717 fn test_request_raw_body_round_trip() {
1718 use base64::{engine::general_purpose, Engine};
1719 let raw = br#"{"type":"message.delivered","data":{"object":{"id":"AC1"}}}"#;
1720 let json = serde_json::to_string(&json!({
1721 "method": "POST",
1722 "handler": "webhook",
1723 "headers": {"content-type": "application/json"},
1724 "body": serde_json::from_slice::<Value>(raw).unwrap(),
1725 "body_raw_b64": general_purpose::STANDARD.encode(raw),
1726 "tenant": "acme",
1727 "service": "shop",
1728 }))
1729 .unwrap();
1730 let req = Request::from_json(&json).unwrap();
1731 assert_eq!(req.raw_body(), raw);
1732 }
1733
1734 #[test]
1735 fn test_request_raw_body_invalid_base64_yields_empty() {
1736 let json = serde_json::to_string(&json!({
1737 "method": "POST",
1738 "handler": "webhook",
1739 "body": Value::Null,
1740 "body_raw_b64": "not%base64!",
1741 "tenant": "acme",
1742 "service": "shop",
1743 }))
1744 .unwrap();
1745 let req = Request::from_json(&json).unwrap();
1746 assert!(req.raw_body().is_empty());
1747 }
1748
1749 #[test]
1750 fn test_request_job_context_parsed_when_present() {
1751 let json = serde_json::to_string(&json!({
1752 "method": "POST",
1753 "handler": "handle_run_ai",
1754 "body": {"id": "item-1"},
1755 "tenant": "acme",
1756 "service": "asset",
1757 "_job": {
1758 "id": "11111111-1111-1111-1111-111111111111",
1759 "attempt": 2,
1760 "max_attempts": 3,
1761 },
1762 }))
1763 .unwrap();
1764 let req = Request::from_json(&json).unwrap();
1765 let job = req.job().expect("job context should be parsed");
1766 assert_eq!(job.id, "11111111-1111-1111-1111-111111111111");
1767 assert_eq!(job.attempt, 2);
1768 assert_eq!(job.max_attempts, 3);
1769 assert!(job.is_retry());
1770 }
1771
1772 #[test]
1773 fn test_request_job_context_absent_on_http_direct() {
1774 let json = serde_json::to_string(&json!({
1775 "method": "GET",
1776 "handler": "list",
1777 "body": Value::Null,
1778 "tenant": "acme",
1779 "service": "asset",
1780 }))
1781 .unwrap();
1782 let req = Request::from_json(&json).unwrap();
1783 assert!(req.job().is_none());
1784 }
1785
1786 #[test]
1787 fn test_request_job_context_first_attempt_is_not_retry() {
1788 let json = serde_json::to_string(&json!({
1789 "method": "POST",
1790 "handler": "handle_run_ai",
1791 "body": Value::Null,
1792 "tenant": "acme",
1793 "service": "asset",
1794 "_job": {
1795 "id": "22222222-2222-2222-2222-222222222222",
1796 "attempt": 1,
1797 "max_attempts": 2,
1798 },
1799 }))
1800 .unwrap();
1801 let req = Request::from_json(&json).unwrap();
1802 let job = req.job().expect("job context should be parsed");
1803 assert_eq!(job.attempt, 1);
1804 assert!(!job.is_retry());
1805 }
1806
1807 #[test]
1808 fn test_request_job_context_rejects_malformed_envelope() {
1809 let json = serde_json::to_string(&json!({
1810 "method": "POST",
1811 "handler": "handle_run_ai",
1812 "body": Value::Null,
1813 "tenant": "acme",
1814 "service": "asset",
1815 "_job": {
1816 "id": "33333333-3333-3333-3333-333333333333",
1817 "max_attempts": 2,
1818 },
1819 }))
1820 .unwrap();
1821 let req = Request::from_json(&json).unwrap();
1822 assert!(
1823 req.job().is_none(),
1824 "missing attempt should yield None, not a partial JobContext"
1825 );
1826 }
1827
1828 #[test]
1829 fn test_response_json() {
1830 let resp = Response::json(&json!({"status": "ok", "count": 42}));
1831 let data = resp.into_data();
1832 let parsed: Value = serde_json::from_str(&data).unwrap();
1833 assert_eq!(parsed["status"], "ok");
1834 assert_eq!(parsed["count"], 42);
1835 }
1836
1837 #[test]
1838 fn test_response_error() {
1839 let resp = Response::error("something went wrong");
1840 let data = resp.into_data();
1841 let parsed: Value = serde_json::from_str(&data).unwrap();
1842 assert_eq!(parsed["__status"], 400);
1844 assert_eq!(parsed["__body"]["error"], "something went wrong");
1845 }
1846
1847 #[test]
1848 fn test_response_not_found() {
1849 let resp = Response::not_found("item not found");
1850 let data = resp.into_data();
1851 let parsed: Value = serde_json::from_str(&data).unwrap();
1852 assert_eq!(parsed["__status"], 404);
1853 assert_eq!(parsed["__body"]["error"], "item not found");
1854 }
1855
1856 #[test]
1857 fn test_response_with_status() {
1858 let resp = Response::json(&serde_json::json!({"ok": true})).with_status(201);
1859 let data = resp.into_data();
1860 let parsed: Value = serde_json::from_str(&data).unwrap();
1861 assert_eq!(parsed["__status"], 201);
1862 assert_eq!(parsed["__body"]["ok"], true);
1863 }
1864
1865 fn migrate_request(diff: serde_json::Value) -> Request {
1866 let payload = serde_json::to_string(&json!({
1867 "method": "POST",
1868 "handler": "handle_on_migrate",
1869 "headers": {},
1870 "body": diff,
1871 "tenant": "default",
1872 "service": "logistics-service",
1873 }))
1874 .unwrap();
1875 Request::from_json(&payload).unwrap()
1876 }
1877
1878 #[test]
1879 fn test_migrate_run_success() {
1880 let req = migrate_request(json!({
1881 "added_columns": [["pickups", "min"]],
1882 "dropped_columns": [["pickups", "midpoint"]],
1883 }));
1884 let resp = migrate::run(req, |diff| {
1885 assert!(diff.added_column("pickups", "min"));
1886 assert!(diff.dropped_column("pickups", "midpoint"));
1887 Ok(())
1888 });
1889 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1890 assert_eq!(parsed["ok"], true);
1891 }
1892
1893 #[test]
1894 fn test_migrate_run_handler_error() {
1895 let req = migrate_request(json!({}));
1896 let resp = migrate::run(req, |_| Err("backfill failed".into()));
1897 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1898 assert_eq!(parsed["__status"], 400);
1899 assert_eq!(parsed["__body"]["error"], "backfill failed");
1900 }
1901
1902 #[test]
1903 fn test_migrate_run_invalid_payload() {
1904 let req = migrate_request(json!("not a diff"));
1906 let resp = migrate::run(req, |_| {
1907 panic!("closure should not be called for invalid payload")
1908 });
1909 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1910 assert_eq!(parsed["__status"], 400);
1911 assert!(parsed["__body"]["error"]
1912 .as_str()
1913 .unwrap()
1914 .contains("on_migrate: failed to parse SchemaDiff payload"));
1915 }
1916
1917 #[test]
1918 fn test_migrate_run_empty_diff() {
1919 let req = migrate_request(json!({}));
1920 let mut called = false;
1921 let resp = migrate::run(req, |diff| {
1922 assert!(diff.is_empty());
1923 called = true;
1924 Ok(())
1925 });
1926 assert!(called);
1927 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1928 assert_eq!(parsed["ok"], true);
1929 }
1930
1931 #[test]
1932 fn test_response_200_no_wrapper() {
1933 let resp = Response::json(&serde_json::json!({"data": "test"}));
1934 let data = resp.into_data();
1935 let parsed: Value = serde_json::from_str(&data).unwrap();
1936 assert_eq!(parsed["data"], "test");
1938 assert!(parsed.get("__status").is_none());
1939 }
1940
1941 #[test]
1942 fn test_response_empty() {
1943 let resp = Response::empty();
1944 let data = resp.into_data();
1945 let parsed: Value = serde_json::from_str(&data).unwrap();
1946 assert_eq!(parsed["ok"], true);
1947 }
1948
1949 #[test]
1950 fn test_response_text() {
1951 let resp = Response::text("hello world");
1952 let data = resp.into_data();
1953 let parsed: Value = serde_json::from_str(&data).unwrap();
1954 assert_eq!(parsed, "hello world");
1955 }
1956
1957 #[test]
1958 fn test_db_query_noop_on_native() {
1959 let rows = db::query("SELECT 1");
1961 assert!(rows.is_empty());
1962 }
1963
1964 #[test]
1965 fn test_db_query_one_noop_on_native() {
1966 let row = db::query_one("SELECT 1");
1967 assert!(row.is_none());
1968 }
1969
1970 #[test]
1971 fn test_db_execute_noop_on_native() {
1972 let affected = db::execute("INSERT INTO x VALUES (1)");
1973 assert_eq!(affected, 0);
1974 }
1975
1976 #[test]
1977 fn test_nats_publish_noop_on_native() {
1978 let ok = nats::publish("test.subject", "payload");
1979 assert!(ok);
1980 }
1981
1982 #[test]
1983 fn test_request_with_auth() {
1984 let json = serde_json::to_string(&json!({
1985 "method": "POST",
1986 "handler": "checkout",
1987 "headers": {},
1988 "body": {},
1989 "tenant": "acme",
1990 "service": "shop",
1991 "auth": {
1992 "sub": "user-123",
1993 "preferred_username": "john",
1994 "name": "John Doe",
1995 "email": "john@example.com",
1996 "realm_roles": ["admin", "manager"],
1997 "claims": {"department": "engineering"}
1998 }
1999 }))
2000 .unwrap();
2001
2002 let req = Request::from_json(&json).unwrap();
2003 let auth = req.auth().unwrap();
2004 assert_eq!(auth.sub, "user-123");
2005 assert_eq!(auth.preferred_username.as_deref(), Some("john"));
2006 assert_eq!(auth.name.as_deref(), Some("John Doe"));
2007 assert_eq!(auth.email.as_deref(), Some("john@example.com"));
2008 assert!(auth.has_role("admin"));
2009 assert!(auth.has_role("manager"));
2010 assert!(!auth.has_role("viewer"));
2011 assert_eq!(
2012 auth.claim("department").and_then(|v| v.as_str()),
2013 Some("engineering")
2014 );
2015 }
2016
2017 #[test]
2018 fn test_request_without_auth() {
2019 let json = r#"{"method": "GET"}"#;
2020 let req = Request::from_json(json).unwrap();
2021 assert!(req.auth().is_none());
2022 }
2023
2024 #[test]
2025 fn test_request_null_auth() {
2026 let json = serde_json::to_string(&json!({
2027 "method": "GET",
2028 "auth": null
2029 }))
2030 .unwrap();
2031 let req = Request::from_json(&json).unwrap();
2032 assert!(req.auth().is_none());
2033 }
2034
2035 #[test]
2036 fn test_require_auth_success() {
2037 let json = serde_json::to_string(&json!({
2038 "method": "GET",
2039 "auth": {"sub": "user-1", "realm_roles": [], "claims": {}}
2040 }))
2041 .unwrap();
2042 let req = Request::from_json(&json).unwrap();
2043 assert!(req.require_auth().is_ok());
2044 assert_eq!(req.require_auth().unwrap().sub, "user-1");
2045 }
2046
2047 #[test]
2048 fn test_require_auth_fails_when_unauthenticated() {
2049 let json = r#"{"method": "GET"}"#;
2050 let req = Request::from_json(json).unwrap();
2051 assert!(req.require_auth().is_err());
2052 }
2053
2054 #[test]
2055 fn test_http_fetch_noop_on_native() {
2056 let resp = http::fetch("GET", "https://example.com", &[], None);
2057 assert!(resp.is_none());
2058 }
2059
2060 #[test]
2061 fn test_http_get_noop_on_native() {
2062 let resp = http::get("https://example.com", &[]);
2063 assert!(resp.is_none());
2064 }
2065
2066 #[test]
2067 fn test_http_post_noop_on_native() {
2068 let resp = http::post("https://example.com", &[], "{}");
2069 assert!(resp.is_none());
2070 }
2071
2072 #[test]
2073 fn test_storage_download_noop_on_native() {
2074 let data = storage::download("my-bucket", "images/photo.jpg");
2075 assert!(data.is_none());
2076 }
2077
2078 #[test]
2079 fn test_image_transform_jpeg_noop_on_native() {
2080 let bytes = image::transform_jpeg("my-bucket", "in.jpg", "out.jpg", 1024, 80);
2081 assert!(bytes.is_none());
2082 }
2083
2084 #[test]
2085 fn test_auth_permissions() {
2086 let json = serde_json::to_string(&json!({
2087 "method": "POST",
2088 "handler": "test",
2089 "headers": {},
2090 "body": {},
2091 "tenant": "acme",
2092 "service": "shop",
2093 "auth": {
2094 "sub": "user-1",
2095 "realm_roles": ["admin"],
2096 "claims": {},
2097 "permissions": ["staff:create", "staff:view", "items:*"],
2098 "role_names": ["admin", "manager"]
2099 }
2100 }))
2101 .unwrap();
2102
2103 let req = Request::from_json(&json).unwrap();
2104 let auth = req.auth().unwrap();
2105
2106 assert!(auth.can("staff", "create"));
2108 assert!(auth.can("staff", "view"));
2109 assert!(!auth.can("staff", "delete"));
2110
2111 assert!(auth.can("items", "create"));
2113 assert!(auth.can("items", "view"));
2114 assert!(auth.can("items", "delete"));
2115
2116 assert!(!auth.can("batches", "view"));
2118
2119 assert!(auth.has_cufflink_role("admin"));
2121 assert!(auth.has_cufflink_role("manager"));
2122 assert!(!auth.has_cufflink_role("viewer"));
2123 }
2124
2125 #[test]
2126 fn test_auth_super_wildcard() {
2127 let auth = Auth {
2128 sub: "user-1".to_string(),
2129 preferred_username: None,
2130 name: None,
2131 email: None,
2132 realm_roles: vec![],
2133 claims: HashMap::new(),
2134 permissions: vec!["*".to_string()],
2135 role_names: vec!["superadmin".to_string()],
2136 is_service_account: false,
2137 };
2138
2139 assert!(auth.can("anything", "everything"));
2140 assert!(auth.can("staff", "create"));
2141 }
2142
2143 #[test]
2144 fn test_auth_empty_permissions() {
2145 let auth = Auth {
2146 sub: "user-1".to_string(),
2147 preferred_username: None,
2148 name: None,
2149 email: None,
2150 realm_roles: vec![],
2151 claims: HashMap::new(),
2152 permissions: vec![],
2153 role_names: vec![],
2154 is_service_account: false,
2155 };
2156
2157 assert!(!auth.can("staff", "create"));
2158 assert!(!auth.has_cufflink_role("admin"));
2159 }
2160
2161 #[test]
2162 fn test_redis_get_noop_on_native() {
2163 let val = redis::get("some-key");
2164 assert!(val.is_none());
2165 }
2166
2167 #[test]
2168 fn test_redis_set_noop_on_native() {
2169 let ok = redis::set("key", "value", 3600);
2170 assert!(ok);
2171 }
2172
2173 #[test]
2174 fn test_redis_del_noop_on_native() {
2175 let ok = redis::del("key");
2176 assert!(ok);
2177 }
2178
2179 #[test]
2180 fn test_http_fetch_response_helpers() {
2181 let resp = http::FetchResponse {
2182 status: 200,
2183 body: r#"{"key": "value"}"#.to_string(),
2184 body_encoding: "utf8".to_string(),
2185 headers: HashMap::new(),
2186 };
2187 assert!(resp.is_success());
2188 assert!(!resp.is_base64());
2189 let json = resp.json().unwrap();
2190 assert_eq!(json["key"], "value");
2191
2192 let err_resp = http::FetchResponse {
2193 status: 404,
2194 body: "not found".to_string(),
2195 body_encoding: "utf8".to_string(),
2196 headers: HashMap::new(),
2197 };
2198 assert!(!err_resp.is_success());
2199
2200 let binary_resp = http::FetchResponse {
2201 status: 200,
2202 body: "aW1hZ2VkYXRh".to_string(),
2203 body_encoding: "base64".to_string(),
2204 headers: HashMap::new(),
2205 };
2206 assert!(binary_resp.is_base64());
2207 }
2208
2209 #[test]
2210 fn context_tenant_returns_empty_outside_wasm() {
2211 assert_eq!(context::tenant(), "");
2212 }
2213
2214 #[test]
2215 fn redis_get_with_status_returns_none_outside_wasm() {
2216 assert_eq!(redis::get_with_status("any"), Ok(None));
2217 }
2218
2219 #[test]
2220 fn redis_mget_returns_empty_for_empty_input() {
2221 assert_eq!(redis::mget(&[]), Ok(Vec::new()));
2222 }
2223
2224 #[test]
2225 fn redis_mget_returns_misses_outside_wasm() {
2226 assert_eq!(redis::mget(&["a", "b", "c"]), Ok(vec![None, None, None]));
2227 }
2228}