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::db;
1660 pub use crate::http;
1661 pub use crate::image;
1662 pub use crate::log;
1663 pub use crate::migrate;
1664 pub use crate::nats;
1665 pub use crate::redis;
1666 pub use crate::storage;
1667 pub use crate::util;
1668 pub use crate::Auth;
1669 pub use crate::JobContext;
1670 pub use crate::Request;
1671 pub use crate::Response;
1672 pub use serde_json::{json, Value};
1673}
1674
1675#[cfg(test)]
1678mod tests {
1679 use super::*;
1680 use serde_json::json;
1681
1682 #[test]
1683 fn test_request_parsing() {
1684 let json = serde_json::to_string(&json!({
1685 "method": "POST",
1686 "handler": "checkout",
1687 "headers": {"content-type": "application/json"},
1688 "body": {"item": "widget", "qty": 3},
1689 "tenant": "acme",
1690 "service": "shop"
1691 }))
1692 .unwrap();
1693
1694 let req = Request::from_json(&json).unwrap();
1695 assert_eq!(req.method(), "POST");
1696 assert_eq!(req.handler(), "checkout");
1697 assert_eq!(req.tenant(), "acme");
1698 assert_eq!(req.service(), "shop");
1699 assert_eq!(req.body()["item"], "widget");
1700 assert_eq!(req.body()["qty"], 3);
1701 assert_eq!(req.header("content-type"), Some("application/json"));
1702 }
1703
1704 #[test]
1705 fn test_request_missing_fields() {
1706 let json = r#"{"method": "GET"}"#;
1707 let req = Request::from_json(json).unwrap();
1708 assert_eq!(req.method(), "GET");
1709 assert_eq!(req.handler(), "");
1710 assert_eq!(req.tenant(), "");
1711 assert_eq!(req.body(), &Value::Null);
1712 assert!(req.raw_body().is_empty());
1713 }
1714
1715 #[test]
1716 fn test_request_raw_body_round_trip() {
1717 use base64::{engine::general_purpose, Engine};
1718 let raw = br#"{"type":"message.delivered","data":{"object":{"id":"AC1"}}}"#;
1719 let json = serde_json::to_string(&json!({
1720 "method": "POST",
1721 "handler": "webhook",
1722 "headers": {"content-type": "application/json"},
1723 "body": serde_json::from_slice::<Value>(raw).unwrap(),
1724 "body_raw_b64": general_purpose::STANDARD.encode(raw),
1725 "tenant": "acme",
1726 "service": "shop",
1727 }))
1728 .unwrap();
1729 let req = Request::from_json(&json).unwrap();
1730 assert_eq!(req.raw_body(), raw);
1731 }
1732
1733 #[test]
1734 fn test_request_raw_body_invalid_base64_yields_empty() {
1735 let json = serde_json::to_string(&json!({
1736 "method": "POST",
1737 "handler": "webhook",
1738 "body": Value::Null,
1739 "body_raw_b64": "not%base64!",
1740 "tenant": "acme",
1741 "service": "shop",
1742 }))
1743 .unwrap();
1744 let req = Request::from_json(&json).unwrap();
1745 assert!(req.raw_body().is_empty());
1746 }
1747
1748 #[test]
1749 fn test_request_job_context_parsed_when_present() {
1750 let json = serde_json::to_string(&json!({
1751 "method": "POST",
1752 "handler": "handle_run_ai",
1753 "body": {"id": "item-1"},
1754 "tenant": "acme",
1755 "service": "asset",
1756 "_job": {
1757 "id": "11111111-1111-1111-1111-111111111111",
1758 "attempt": 2,
1759 "max_attempts": 3,
1760 },
1761 }))
1762 .unwrap();
1763 let req = Request::from_json(&json).unwrap();
1764 let job = req.job().expect("job context should be parsed");
1765 assert_eq!(job.id, "11111111-1111-1111-1111-111111111111");
1766 assert_eq!(job.attempt, 2);
1767 assert_eq!(job.max_attempts, 3);
1768 assert!(job.is_retry());
1769 }
1770
1771 #[test]
1772 fn test_request_job_context_absent_on_http_direct() {
1773 let json = serde_json::to_string(&json!({
1774 "method": "GET",
1775 "handler": "list",
1776 "body": Value::Null,
1777 "tenant": "acme",
1778 "service": "asset",
1779 }))
1780 .unwrap();
1781 let req = Request::from_json(&json).unwrap();
1782 assert!(req.job().is_none());
1783 }
1784
1785 #[test]
1786 fn test_request_job_context_first_attempt_is_not_retry() {
1787 let json = serde_json::to_string(&json!({
1788 "method": "POST",
1789 "handler": "handle_run_ai",
1790 "body": Value::Null,
1791 "tenant": "acme",
1792 "service": "asset",
1793 "_job": {
1794 "id": "22222222-2222-2222-2222-222222222222",
1795 "attempt": 1,
1796 "max_attempts": 2,
1797 },
1798 }))
1799 .unwrap();
1800 let req = Request::from_json(&json).unwrap();
1801 let job = req.job().expect("job context should be parsed");
1802 assert_eq!(job.attempt, 1);
1803 assert!(!job.is_retry());
1804 }
1805
1806 #[test]
1807 fn test_request_job_context_rejects_malformed_envelope() {
1808 let json = serde_json::to_string(&json!({
1809 "method": "POST",
1810 "handler": "handle_run_ai",
1811 "body": Value::Null,
1812 "tenant": "acme",
1813 "service": "asset",
1814 "_job": {
1815 "id": "33333333-3333-3333-3333-333333333333",
1816 "max_attempts": 2,
1817 },
1818 }))
1819 .unwrap();
1820 let req = Request::from_json(&json).unwrap();
1821 assert!(
1822 req.job().is_none(),
1823 "missing attempt should yield None, not a partial JobContext"
1824 );
1825 }
1826
1827 #[test]
1828 fn test_response_json() {
1829 let resp = Response::json(&json!({"status": "ok", "count": 42}));
1830 let data = resp.into_data();
1831 let parsed: Value = serde_json::from_str(&data).unwrap();
1832 assert_eq!(parsed["status"], "ok");
1833 assert_eq!(parsed["count"], 42);
1834 }
1835
1836 #[test]
1837 fn test_response_error() {
1838 let resp = Response::error("something went wrong");
1839 let data = resp.into_data();
1840 let parsed: Value = serde_json::from_str(&data).unwrap();
1841 assert_eq!(parsed["__status"], 400);
1843 assert_eq!(parsed["__body"]["error"], "something went wrong");
1844 }
1845
1846 #[test]
1847 fn test_response_not_found() {
1848 let resp = Response::not_found("item not found");
1849 let data = resp.into_data();
1850 let parsed: Value = serde_json::from_str(&data).unwrap();
1851 assert_eq!(parsed["__status"], 404);
1852 assert_eq!(parsed["__body"]["error"], "item not found");
1853 }
1854
1855 #[test]
1856 fn test_response_with_status() {
1857 let resp = Response::json(&serde_json::json!({"ok": true})).with_status(201);
1858 let data = resp.into_data();
1859 let parsed: Value = serde_json::from_str(&data).unwrap();
1860 assert_eq!(parsed["__status"], 201);
1861 assert_eq!(parsed["__body"]["ok"], true);
1862 }
1863
1864 fn migrate_request(diff: serde_json::Value) -> Request {
1865 let payload = serde_json::to_string(&json!({
1866 "method": "POST",
1867 "handler": "handle_on_migrate",
1868 "headers": {},
1869 "body": diff,
1870 "tenant": "default",
1871 "service": "logistics-service",
1872 }))
1873 .unwrap();
1874 Request::from_json(&payload).unwrap()
1875 }
1876
1877 #[test]
1878 fn test_migrate_run_success() {
1879 let req = migrate_request(json!({
1880 "added_columns": [["pickups", "min"]],
1881 "dropped_columns": [["pickups", "midpoint"]],
1882 }));
1883 let resp = migrate::run(req, |diff| {
1884 assert!(diff.added_column("pickups", "min"));
1885 assert!(diff.dropped_column("pickups", "midpoint"));
1886 Ok(())
1887 });
1888 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1889 assert_eq!(parsed["ok"], true);
1890 }
1891
1892 #[test]
1893 fn test_migrate_run_handler_error() {
1894 let req = migrate_request(json!({}));
1895 let resp = migrate::run(req, |_| Err("backfill failed".into()));
1896 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1897 assert_eq!(parsed["__status"], 400);
1898 assert_eq!(parsed["__body"]["error"], "backfill failed");
1899 }
1900
1901 #[test]
1902 fn test_migrate_run_invalid_payload() {
1903 let req = migrate_request(json!("not a diff"));
1905 let resp = migrate::run(req, |_| {
1906 panic!("closure should not be called for invalid payload")
1907 });
1908 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1909 assert_eq!(parsed["__status"], 400);
1910 assert!(parsed["__body"]["error"]
1911 .as_str()
1912 .unwrap()
1913 .contains("on_migrate: failed to parse SchemaDiff payload"));
1914 }
1915
1916 #[test]
1917 fn test_migrate_run_empty_diff() {
1918 let req = migrate_request(json!({}));
1919 let mut called = false;
1920 let resp = migrate::run(req, |diff| {
1921 assert!(diff.is_empty());
1922 called = true;
1923 Ok(())
1924 });
1925 assert!(called);
1926 let parsed: Value = serde_json::from_str(&resp.into_data()).unwrap();
1927 assert_eq!(parsed["ok"], true);
1928 }
1929
1930 #[test]
1931 fn test_response_200_no_wrapper() {
1932 let resp = Response::json(&serde_json::json!({"data": "test"}));
1933 let data = resp.into_data();
1934 let parsed: Value = serde_json::from_str(&data).unwrap();
1935 assert_eq!(parsed["data"], "test");
1937 assert!(parsed.get("__status").is_none());
1938 }
1939
1940 #[test]
1941 fn test_response_empty() {
1942 let resp = Response::empty();
1943 let data = resp.into_data();
1944 let parsed: Value = serde_json::from_str(&data).unwrap();
1945 assert_eq!(parsed["ok"], true);
1946 }
1947
1948 #[test]
1949 fn test_response_text() {
1950 let resp = Response::text("hello world");
1951 let data = resp.into_data();
1952 let parsed: Value = serde_json::from_str(&data).unwrap();
1953 assert_eq!(parsed, "hello world");
1954 }
1955
1956 #[test]
1957 fn test_db_query_noop_on_native() {
1958 let rows = db::query("SELECT 1");
1960 assert!(rows.is_empty());
1961 }
1962
1963 #[test]
1964 fn test_db_query_one_noop_on_native() {
1965 let row = db::query_one("SELECT 1");
1966 assert!(row.is_none());
1967 }
1968
1969 #[test]
1970 fn test_db_execute_noop_on_native() {
1971 let affected = db::execute("INSERT INTO x VALUES (1)");
1972 assert_eq!(affected, 0);
1973 }
1974
1975 #[test]
1976 fn test_nats_publish_noop_on_native() {
1977 let ok = nats::publish("test.subject", "payload");
1978 assert!(ok);
1979 }
1980
1981 #[test]
1982 fn test_request_with_auth() {
1983 let json = serde_json::to_string(&json!({
1984 "method": "POST",
1985 "handler": "checkout",
1986 "headers": {},
1987 "body": {},
1988 "tenant": "acme",
1989 "service": "shop",
1990 "auth": {
1991 "sub": "user-123",
1992 "preferred_username": "john",
1993 "name": "John Doe",
1994 "email": "john@example.com",
1995 "realm_roles": ["admin", "manager"],
1996 "claims": {"department": "engineering"}
1997 }
1998 }))
1999 .unwrap();
2000
2001 let req = Request::from_json(&json).unwrap();
2002 let auth = req.auth().unwrap();
2003 assert_eq!(auth.sub, "user-123");
2004 assert_eq!(auth.preferred_username.as_deref(), Some("john"));
2005 assert_eq!(auth.name.as_deref(), Some("John Doe"));
2006 assert_eq!(auth.email.as_deref(), Some("john@example.com"));
2007 assert!(auth.has_role("admin"));
2008 assert!(auth.has_role("manager"));
2009 assert!(!auth.has_role("viewer"));
2010 assert_eq!(
2011 auth.claim("department").and_then(|v| v.as_str()),
2012 Some("engineering")
2013 );
2014 }
2015
2016 #[test]
2017 fn test_request_without_auth() {
2018 let json = r#"{"method": "GET"}"#;
2019 let req = Request::from_json(json).unwrap();
2020 assert!(req.auth().is_none());
2021 }
2022
2023 #[test]
2024 fn test_request_null_auth() {
2025 let json = serde_json::to_string(&json!({
2026 "method": "GET",
2027 "auth": null
2028 }))
2029 .unwrap();
2030 let req = Request::from_json(&json).unwrap();
2031 assert!(req.auth().is_none());
2032 }
2033
2034 #[test]
2035 fn test_require_auth_success() {
2036 let json = serde_json::to_string(&json!({
2037 "method": "GET",
2038 "auth": {"sub": "user-1", "realm_roles": [], "claims": {}}
2039 }))
2040 .unwrap();
2041 let req = Request::from_json(&json).unwrap();
2042 assert!(req.require_auth().is_ok());
2043 assert_eq!(req.require_auth().unwrap().sub, "user-1");
2044 }
2045
2046 #[test]
2047 fn test_require_auth_fails_when_unauthenticated() {
2048 let json = r#"{"method": "GET"}"#;
2049 let req = Request::from_json(json).unwrap();
2050 assert!(req.require_auth().is_err());
2051 }
2052
2053 #[test]
2054 fn test_http_fetch_noop_on_native() {
2055 let resp = http::fetch("GET", "https://example.com", &[], None);
2056 assert!(resp.is_none());
2057 }
2058
2059 #[test]
2060 fn test_http_get_noop_on_native() {
2061 let resp = http::get("https://example.com", &[]);
2062 assert!(resp.is_none());
2063 }
2064
2065 #[test]
2066 fn test_http_post_noop_on_native() {
2067 let resp = http::post("https://example.com", &[], "{}");
2068 assert!(resp.is_none());
2069 }
2070
2071 #[test]
2072 fn test_storage_download_noop_on_native() {
2073 let data = storage::download("my-bucket", "images/photo.jpg");
2074 assert!(data.is_none());
2075 }
2076
2077 #[test]
2078 fn test_image_transform_jpeg_noop_on_native() {
2079 let bytes = image::transform_jpeg("my-bucket", "in.jpg", "out.jpg", 1024, 80);
2080 assert!(bytes.is_none());
2081 }
2082
2083 #[test]
2084 fn test_auth_permissions() {
2085 let json = serde_json::to_string(&json!({
2086 "method": "POST",
2087 "handler": "test",
2088 "headers": {},
2089 "body": {},
2090 "tenant": "acme",
2091 "service": "shop",
2092 "auth": {
2093 "sub": "user-1",
2094 "realm_roles": ["admin"],
2095 "claims": {},
2096 "permissions": ["staff:create", "staff:view", "items:*"],
2097 "role_names": ["admin", "manager"]
2098 }
2099 }))
2100 .unwrap();
2101
2102 let req = Request::from_json(&json).unwrap();
2103 let auth = req.auth().unwrap();
2104
2105 assert!(auth.can("staff", "create"));
2107 assert!(auth.can("staff", "view"));
2108 assert!(!auth.can("staff", "delete"));
2109
2110 assert!(auth.can("items", "create"));
2112 assert!(auth.can("items", "view"));
2113 assert!(auth.can("items", "delete"));
2114
2115 assert!(!auth.can("batches", "view"));
2117
2118 assert!(auth.has_cufflink_role("admin"));
2120 assert!(auth.has_cufflink_role("manager"));
2121 assert!(!auth.has_cufflink_role("viewer"));
2122 }
2123
2124 #[test]
2125 fn test_auth_super_wildcard() {
2126 let auth = Auth {
2127 sub: "user-1".to_string(),
2128 preferred_username: None,
2129 name: None,
2130 email: None,
2131 realm_roles: vec![],
2132 claims: HashMap::new(),
2133 permissions: vec!["*".to_string()],
2134 role_names: vec!["superadmin".to_string()],
2135 is_service_account: false,
2136 };
2137
2138 assert!(auth.can("anything", "everything"));
2139 assert!(auth.can("staff", "create"));
2140 }
2141
2142 #[test]
2143 fn test_auth_empty_permissions() {
2144 let auth = Auth {
2145 sub: "user-1".to_string(),
2146 preferred_username: None,
2147 name: None,
2148 email: None,
2149 realm_roles: vec![],
2150 claims: HashMap::new(),
2151 permissions: vec![],
2152 role_names: vec![],
2153 is_service_account: false,
2154 };
2155
2156 assert!(!auth.can("staff", "create"));
2157 assert!(!auth.has_cufflink_role("admin"));
2158 }
2159
2160 #[test]
2161 fn test_redis_get_noop_on_native() {
2162 let val = redis::get("some-key");
2163 assert!(val.is_none());
2164 }
2165
2166 #[test]
2167 fn test_redis_set_noop_on_native() {
2168 let ok = redis::set("key", "value", 3600);
2169 assert!(ok);
2170 }
2171
2172 #[test]
2173 fn test_redis_del_noop_on_native() {
2174 let ok = redis::del("key");
2175 assert!(ok);
2176 }
2177
2178 #[test]
2179 fn test_http_fetch_response_helpers() {
2180 let resp = http::FetchResponse {
2181 status: 200,
2182 body: r#"{"key": "value"}"#.to_string(),
2183 body_encoding: "utf8".to_string(),
2184 headers: HashMap::new(),
2185 };
2186 assert!(resp.is_success());
2187 assert!(!resp.is_base64());
2188 let json = resp.json().unwrap();
2189 assert_eq!(json["key"], "value");
2190
2191 let err_resp = http::FetchResponse {
2192 status: 404,
2193 body: "not found".to_string(),
2194 body_encoding: "utf8".to_string(),
2195 headers: HashMap::new(),
2196 };
2197 assert!(!err_resp.is_success());
2198
2199 let binary_resp = http::FetchResponse {
2200 status: 200,
2201 body: "aW1hZ2VkYXRh".to_string(),
2202 body_encoding: "base64".to_string(),
2203 headers: HashMap::new(),
2204 };
2205 assert!(binary_resp.is_base64());
2206 }
2207
2208 #[test]
2209 fn context_tenant_returns_empty_outside_wasm() {
2210 assert_eq!(context::tenant(), "");
2211 }
2212
2213 #[test]
2214 fn redis_get_with_status_returns_none_outside_wasm() {
2215 assert_eq!(redis::get_with_status("any"), Ok(None));
2216 }
2217
2218 #[test]
2219 fn redis_mget_returns_empty_for_empty_input() {
2220 assert_eq!(redis::mget(&[]), Ok(Vec::new()));
2221 }
2222
2223 #[test]
2224 fn redis_mget_returns_misses_outside_wasm() {
2225 assert_eq!(redis::mget(&["a", "b", "c"]), Ok(vec![None, None, None]));
2226 }
2227}