1use std::sync::Arc;
7
8use serde::Deserialize;
9
10use crate::{
11 executor_contract::{MeshControlEnvelope, MeshObjectPolicy},
12 HostExtension,
13};
14
15use super::mesh_runtime::{InMemoryMeshRuntime, MeshRuntime};
16
17pub struct MeshExtension {
18 runtime: Arc<dyn MeshRuntime>,
19 inject_script: String,
20}
21
22impl Default for MeshExtension {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl MeshExtension {
29 pub fn new() -> Self {
30 Self::with_runtime(InMemoryMeshRuntime::new())
31 }
32
33 pub fn with_runtime<R>(runtime: R) -> Self
34 where
35 R: MeshRuntime,
36 {
37 Self::with_shared_runtime(Arc::new(runtime))
38 }
39
40 pub fn with_shared_runtime(runtime: Arc<dyn MeshRuntime>) -> Self {
41 let inject_script = build_mesh_script(runtime.supports_private());
42 Self {
43 runtime,
44 inject_script,
45 }
46 }
47}
48
49impl HostExtension for MeshExtension {
50 fn namespace(&self) -> &str {
51 "mesh"
52 }
53
54 fn channel(&self) -> &str {
55 "hostBridge"
56 }
57
58 fn inject_script(&self) -> &str {
59 &self.inject_script
60 }
61
62 fn handle_message(&self, method: &str, params: &str) -> Option<String> {
63 match method {
64 "meshPublish" => {
65 let request: TopicPayload = serde_json::from_str(params).ok()?;
66 serde_json::to_string(&self.runtime.publish(&request.topic, &request.data_base64))
67 .ok()
68 }
69 "meshSubscribe" => {
70 let request: TopicOnlyPayload = serde_json::from_str(params).ok()?;
71 serde_json::to_string(&self.runtime.subscribe(&request.topic)).ok()
72 }
73 "meshPutObject" => {
74 let request: PutObjectPayload = serde_json::from_str(params).ok()?;
75 serde_json::to_string(&self.runtime.put_object(
76 &request.path,
77 &request.data_base64,
78 request.policy.unwrap_or_default(),
79 ))
80 .ok()
81 }
82 "meshGetObject" => {
83 let request: PathOnlyPayload = serde_json::from_str(params).ok()?;
84 serde_json::to_string(&self.runtime.get_object(&request.path)).ok()
85 }
86 "meshGetObjectResult" => {
87 let request: PathOnlyPayload = serde_json::from_str(params).ok()?;
88 serde_json::to_string(&self.runtime.get_object_result(&request.path)).ok()
89 }
90 "meshRequest" => {
91 let request: PathPayload = serde_json::from_str(params).ok()?;
92 serde_json::to_string(&self.runtime.request(&request.path, &request.data_base64))
93 .ok()
94 }
95 "meshRespond" => {
96 let request: ResponsePayload = serde_json::from_str(params).ok()?;
97 serde_json::to_string(
98 &self
99 .runtime
100 .respond(&request.request_id, &request.data_base64),
101 )
102 .ok()
103 }
104 "meshStatus" => serde_json::to_string(&self.runtime.status()).ok(),
105 "meshPrivatePutObject" if self.runtime.supports_private() => {
106 let request: PrivatePutObjectPayload = serde_json::from_str(params).ok()?;
107 serde_json::to_string(
108 &self.runtime.put_private_object(
109 &request.data_base64,
110 request.policy.unwrap_or_default(),
111 ),
112 )
113 .ok()
114 }
115 "meshPrivateGetObject" if self.runtime.supports_private() => {
116 let request: PrivateGetObjectPayload = serde_json::from_str(params).ok()?;
117 serde_json::to_string(
118 &self
119 .runtime
120 .get_private_object_result(&request.handle, &request.capability),
121 )
122 .ok()
123 }
124 "meshPrivateRequest" if self.runtime.supports_private() => {
125 let request: PrivateRequestPayload = serde_json::from_str(params).ok()?;
126 serde_json::to_string(&self.runtime.request_private(
127 &request.handle,
128 &request.capability,
129 &request.data_base64,
130 ))
131 .ok()
132 }
133 "meshPrivateControlPublish" if self.runtime.supports_private() => {
134 let request: PrivateControlPublishPayload = serde_json::from_str(params).ok()?;
135 serde_json::to_string(
136 &self
137 .runtime
138 .publish_private_control(&request.capability, request.envelope),
139 )
140 .ok()
141 }
142 "meshPrivateControlSubscribe" if self.runtime.supports_private() => {
143 let request: PrivateCapabilityPayload = serde_json::from_str(params).ok()?;
144 serde_json::to_string(&self.runtime.subscribe_private_control(&request.capability))
145 .ok()
146 }
147 "meshPrivateReceiptPublish" if self.runtime.supports_private() => {
148 let request: PrivateReceiptPublishPayload = serde_json::from_str(params).ok()?;
149 serde_json::to_string(
150 &self
151 .runtime
152 .publish_private_receipt(&request.capability, &request.data_base64),
153 )
154 .ok()
155 }
156 "meshPrivateReceiptSubscribe" if self.runtime.supports_private() => {
157 let request: PrivateCapabilityPayload = serde_json::from_str(params).ok()?;
158 serde_json::to_string(&self.runtime.subscribe_private_receipt(&request.capability))
159 .ok()
160 }
161 _ => None,
162 }
163 }
164
165 fn drain_events(&self) -> Vec<crate::HostPushEvent> {
166 self.runtime.drain_events()
167 }
168}
169
170fn build_mesh_script(supports_private: bool) -> String {
171 let private_block = if supports_private {
172 r#",
173 private: Object.freeze({
174 objects: Object.freeze({
175 put: function(dataBase64, policy) {
176 return window.__hostCall('hostBridge', 'meshPrivatePutObject', {
177 dataBase64: dataBase64,
178 policy: policy
179 });
180 },
181 get: function(handle, capability) {
182 return window.__hostCall('hostBridge', 'meshPrivateGetObject', {
183 handle: handle,
184 capability: capability
185 });
186 }
187 }),
188 query: Object.freeze({
189 request: function(handle, capability, dataBase64) {
190 return window.__hostCall('hostBridge', 'meshPrivateRequest', {
191 handle: handle,
192 capability: capability,
193 dataBase64: dataBase64
194 });
195 }
196 }),
197 control: Object.freeze({
198 publish: function(capability, envelope) {
199 return window.__hostCall('hostBridge', 'meshPrivateControlPublish', {
200 capability: capability,
201 envelope: envelope
202 });
203 },
204 subscribe: function(capability) {
205 return window.__hostCall('hostBridge', 'meshPrivateControlSubscribe', {
206 capability: capability
207 });
208 }
209 }),
210 receipts: Object.freeze({
211 publish: function(capability, dataBase64) {
212 return window.__hostCall('hostBridge', 'meshPrivateReceiptPublish', {
213 capability: capability,
214 dataBase64: dataBase64
215 });
216 },
217 subscribe: function(capability) {
218 return window.__hostCall('hostBridge', 'meshPrivateReceiptSubscribe', {
219 capability: capability
220 });
221 }
222 })
223 })"#
224 } else {
225 ""
226 };
227
228 format!(
229 r#"
230(function(){{
231 if (!window.host || !window.host.ext) return;
232 window.host.ext.mesh = Object.freeze({{
233 transport: Object.freeze({{
234 publish: function(topic, dataBase64) {{
235 return window.__hostCall('hostBridge', 'meshPublish', {{
236 topic: topic,
237 dataBase64: dataBase64
238 }});
239 }},
240 subscribe: function(topic) {{
241 return window.__hostCall('hostBridge', 'meshSubscribe', {{
242 topic: topic
243 }});
244 }}
245 }}),
246 objects: Object.freeze({{
247 put: function(path, dataBase64, policy) {{
248 return window.__hostCall('hostBridge', 'meshPutObject', {{
249 path: path,
250 dataBase64: dataBase64,
251 policy: policy
252 }});
253 }},
254 get: function(path) {{
255 return window.__hostCall('hostBridge', 'meshGetObject', {{
256 path: path
257 }});
258 }},
259 getResult: function(path) {{
260 return window.__hostCall('hostBridge', 'meshGetObjectResult', {{
261 path: path
262 }});
263 }}
264 }}),
265 query: Object.freeze({{
266 request: function(path, dataBase64) {{
267 return window.__hostCall('hostBridge', 'meshRequest', {{
268 path: path,
269 dataBase64: dataBase64
270 }});
271 }},
272 respond: function(requestId, dataBase64) {{
273 return window.__hostCall('hostBridge', 'meshRespond', {{
274 requestId: requestId,
275 dataBase64: dataBase64
276 }});
277 }}
278 }}),
279 status: Object.freeze({{
280 status: function() {{
281 return window.__hostCall('hostBridge', 'meshStatus', {{}});
282 }}
283 }}){private_block}
284 }});
285}})();
286"#
287 )
288}
289
290#[derive(Deserialize)]
291#[serde(rename_all = "camelCase")]
292struct TopicPayload {
293 topic: String,
294 data_base64: String,
295}
296
297#[derive(Deserialize)]
298#[serde(rename_all = "camelCase")]
299struct TopicOnlyPayload {
300 topic: String,
301}
302
303#[derive(Deserialize)]
304#[serde(rename_all = "camelCase")]
305struct PathPayload {
306 path: String,
307 data_base64: String,
308}
309
310#[derive(Deserialize)]
311#[serde(rename_all = "camelCase")]
312struct PutObjectPayload {
313 path: String,
314 data_base64: String,
315 #[serde(default)]
316 policy: Option<MeshObjectPolicy>,
317}
318
319#[derive(Deserialize)]
320#[serde(rename_all = "camelCase")]
321struct PathOnlyPayload {
322 path: String,
323}
324
325#[derive(Deserialize)]
326#[serde(rename_all = "camelCase")]
327struct ResponsePayload {
328 request_id: String,
329 data_base64: String,
330}
331
332#[derive(Deserialize)]
333#[serde(rename_all = "camelCase")]
334struct PrivatePutObjectPayload {
335 data_base64: String,
336 #[serde(default)]
337 policy: Option<MeshObjectPolicy>,
338}
339
340#[derive(Deserialize)]
341#[serde(rename_all = "camelCase")]
342struct PrivateGetObjectPayload {
343 handle: String,
344 capability: String,
345}
346
347#[derive(Deserialize)]
348#[serde(rename_all = "camelCase")]
349struct PrivateRequestPayload {
350 handle: String,
351 capability: String,
352 data_base64: String,
353}
354
355#[derive(Deserialize)]
356#[serde(rename_all = "camelCase")]
357struct PrivateCapabilityPayload {
358 capability: String,
359}
360
361#[derive(Deserialize)]
362#[serde(rename_all = "camelCase")]
363struct PrivateControlPublishPayload {
364 capability: String,
365 envelope: MeshControlEnvelope,
366}
367
368#[derive(Deserialize)]
369#[serde(rename_all = "camelCase")]
370struct PrivateReceiptPublishPayload {
371 capability: String,
372 data_base64: String,
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use crate::executor_contract::{
379 MeshControlEnvelope, MeshControlMode, MeshObjectPolicy, MeshObjectReadReason,
380 MeshObjectResult, MeshPrivateObjectRef, MeshStatusResult,
381 };
382 use crate::first_party::mesh_runtime::MeshRuntime;
383
384 struct StaticStatusRuntime;
385
386 impl MeshRuntime for StaticStatusRuntime {
387 fn publish(&self, _topic: &str, _data_base64: &str) -> bool {
388 true
389 }
390
391 fn subscribe(&self, _topic: &str) -> bool {
392 true
393 }
394
395 fn put_object(&self, _path: &str, _data_base64: &str, _policy: MeshObjectPolicy) -> bool {
396 true
397 }
398
399 fn get_object_result(&self, _path: &str) -> MeshObjectResult {
400 MeshObjectResult::unavailable(MeshObjectReadReason::NotFound, None)
401 }
402
403 fn request(&self, _path: &str, _data_base64: &str) -> String {
404 "mesh-req-static".into()
405 }
406
407 fn respond(&self, _request_id: &str, _data_base64: &str) -> bool {
408 true
409 }
410
411 fn status(&self) -> MeshStatusResult {
412 MeshStatusResult {
413 health: "degraded".into(),
414 transport: "custom-runtime".into(),
415 pending_publishes: 2,
416 pending_queries: 3,
417 last_error: Some("submit failed".into()),
418 }
419 }
420 }
421
422 struct PrivateStaticRuntime;
423
424 impl MeshRuntime for PrivateStaticRuntime {
425 fn publish(&self, _topic: &str, _data_base64: &str) -> bool {
426 true
427 }
428
429 fn subscribe(&self, _topic: &str) -> bool {
430 true
431 }
432
433 fn put_object(&self, _path: &str, _data_base64: &str, _policy: MeshObjectPolicy) -> bool {
434 true
435 }
436
437 fn get_object_result(&self, _path: &str) -> MeshObjectResult {
438 MeshObjectResult::unavailable(MeshObjectReadReason::NotFound, None)
439 }
440
441 fn request(&self, _path: &str, _data_base64: &str) -> String {
442 "mesh-req-private".into()
443 }
444
445 fn respond(&self, _request_id: &str, _data_base64: &str) -> bool {
446 true
447 }
448
449 fn status(&self) -> MeshStatusResult {
450 MeshStatusResult {
451 health: "healthy".into(),
452 transport: "custom-runtime".into(),
453 pending_publishes: 0,
454 pending_queries: 0,
455 last_error: None,
456 }
457 }
458
459 fn supports_private(&self) -> bool {
460 true
461 }
462
463 fn put_private_object(
464 &self,
465 _data_base64: &str,
466 policy: MeshObjectPolicy,
467 ) -> MeshPrivateObjectRef {
468 MeshPrivateObjectRef {
469 handle: "mesh-private-handle-1".into(),
470 capability: "mesh-private-capability-1".into(),
471 expires_at_ms: policy.expires_at_ms,
472 }
473 }
474
475 fn get_private_object_result(&self, _handle: &str, _capability: &str) -> MeshObjectResult {
476 MeshObjectResult::found("AQID".into(), Some(1710000000000))
477 }
478
479 fn request_private(&self, _handle: &str, _capability: &str, _data_base64: &str) -> String {
480 "mesh-private-req-1".into()
481 }
482
483 fn publish_private_control(
484 &self,
485 _capability: &str,
486 _envelope: MeshControlEnvelope,
487 ) -> bool {
488 true
489 }
490
491 fn subscribe_private_control(&self, _capability: &str) -> bool {
492 true
493 }
494
495 fn publish_private_receipt(&self, _capability: &str, _data_base64: &str) -> bool {
496 true
497 }
498
499 fn subscribe_private_receipt(&self, _capability: &str) -> bool {
500 true
501 }
502 }
503
504 #[test]
505 fn mesh_extension_basics() {
506 let ext = MeshExtension::new();
507 assert_eq!(ext.namespace(), "mesh");
508 assert!(ext.inject_script().contains("window.host.ext.mesh"));
509 assert!(ext.inject_script().contains("meshPublish"));
510 assert!(ext.inject_script().contains("meshPutObject"));
511 assert!(ext.inject_script().contains("meshGetObjectResult"));
512 assert!(ext.inject_script().contains("meshRequest"));
513 assert!(ext.inject_script().contains("meshStatus"));
514 assert!(ext.inject_script().contains("meshPrivatePutObject"));
515 }
516
517 #[test]
518 fn mesh_extension_handles_core_methods() {
519 let ext = MeshExtension::new();
520
521 assert_eq!(
522 ext.handle_message("meshPublish", r#"{"topic":"room/1","dataBase64":"AQID"}"#),
523 Some("true".to_string())
524 );
525 assert_eq!(
526 ext.handle_message("meshSubscribe", r#"{"topic":"room/1"}"#),
527 Some("true".to_string())
528 );
529 assert_eq!(
530 ext.handle_message(
531 "meshPutObject",
532 r#"{"path":"mesh/object/1","dataBase64":"AQID","policy":{"expiresAtMs":18446744073709551615,"suppressPreviews":true}}"#
533 ),
534 Some("true".to_string())
535 );
536 assert_eq!(
537 ext.handle_message("meshGetObject", r#"{"path":"mesh/object/1"}"#),
538 Some(r#""AQID""#.to_string())
539 );
540 assert_eq!(
541 ext.handle_message("meshGetObjectResult", r#"{"path":"mesh/object/1"}"#),
542 Some(r#"{"dataBase64":"AQID","expiresAtMs":18446744073709551615}"#.to_string())
543 );
544
545 let request_id = ext
546 .handle_message(
547 "meshRequest",
548 r#"{"path":"mesh/object/1","dataBase64":"AQID"}"#,
549 )
550 .expect("meshRequest result");
551 assert!(request_id.contains("mesh-req-"));
552 let events = ext.drain_events();
554 assert_eq!(events.len(), 1, "expected meshReply event on cache hit");
555 assert_eq!(events[0].event, "meshReply");
556 assert!(events[0].payload_json.contains("AQID"));
557 assert_eq!(
559 ext.handle_message(
560 "meshRespond",
561 &format!(r#"{{"requestId":{},"dataBase64":"AQID"}}"#, request_id)
562 ),
563 Some("false".to_string())
564 );
565
566 let status_json = ext
567 .handle_message("meshStatus", r#"{}"#)
568 .expect("meshStatus result");
569 let status: MeshStatusResult = serde_json::from_str(&status_json).expect("status json");
570 assert_eq!(status.health, "healthy");
571 assert_eq!(status.transport, "in-memory");
572 }
573
574 #[test]
575 fn mesh_extension_supports_injected_runtime() {
576 let ext = MeshExtension::with_runtime(StaticStatusRuntime);
577 let status_json = ext
578 .handle_message("meshStatus", r#"{}"#)
579 .expect("meshStatus result");
580 let status: MeshStatusResult = serde_json::from_str(&status_json).expect("status json");
581
582 assert_eq!(status.health, "degraded");
583 assert_eq!(status.transport, "custom-runtime");
584 assert_eq!(status.pending_publishes, 2);
585 assert_eq!(status.pending_queries, 3);
586 assert_eq!(status.last_error.as_deref(), Some("submit failed"));
587 }
588
589 #[test]
590 fn mesh_extension_hides_private_surface_when_runtime_does_not_support_it() {
591 let ext = MeshExtension::with_runtime(StaticStatusRuntime);
592 assert!(!ext.inject_script().contains("meshPrivatePutObject"));
594 assert!(!ext.inject_script().contains("meshPrivateControlPublish"));
595 assert!(!ext.inject_script().contains("meshPrivateReceiptSubscribe"));
596 assert!(!ext.inject_script().contains("private:"));
597
598 assert_eq!(
600 ext.handle_message("meshPrivatePutObject", r#"{"dataBase64":"AQID"}"#,),
601 None
602 );
603 assert_eq!(
604 ext.handle_message("meshPrivateGetObject", r#"{"handle":"h","capability":"c"}"#,),
605 None
606 );
607 assert_eq!(
608 ext.handle_message(
609 "meshPrivateRequest",
610 r#"{"handle":"h","capability":"c","dataBase64":""}"#,
611 ),
612 None
613 );
614 assert_eq!(
615 ext.handle_message(
616 "meshPrivateControlPublish",
617 r#"{"capability":"c","envelope":{"mode":"encrypted","dataBase64":"AQID"}}"#,
618 ),
619 None
620 );
621 assert_eq!(
622 ext.handle_message("meshPrivateControlSubscribe", r#"{"capability":"c"}"#,),
623 None
624 );
625 assert_eq!(
626 ext.handle_message(
627 "meshPrivateReceiptPublish",
628 r#"{"capability":"c","dataBase64":"AQID"}"#,
629 ),
630 None
631 );
632 assert_eq!(
633 ext.handle_message("meshPrivateReceiptSubscribe", r#"{"capability":"c"}"#,),
634 None
635 );
636
637 assert_eq!(
639 ext.handle_message("meshPublish", r#"{"topic":"room/1","dataBase64":"AQID"}"#),
640 Some("true".to_string())
641 );
642 }
643
644 #[test]
645 fn mesh_extension_includes_private_surface_when_runtime_supports_it() {
646 let ext = MeshExtension::with_runtime(PrivateStaticRuntime);
647 assert!(ext.inject_script().contains("meshPrivatePutObject"));
648 assert!(ext.inject_script().contains("meshPrivateControlPublish"));
649 assert!(ext.inject_script().contains("meshPrivateReceiptSubscribe"));
650
651 assert_eq!(
652 ext.handle_message(
653 "meshPrivatePutObject",
654 r#"{"dataBase64":"AQID","policy":{"expiresAtMs":1710000000000}}"#,
655 ),
656 Some(r#"{"handle":"mesh-private-handle-1","capability":"mesh-private-capability-1","expiresAtMs":1710000000000}"#.to_string())
657 );
658 assert_eq!(
659 ext.handle_message(
660 "meshPrivateGetObject",
661 r#"{"handle":"mesh-private-handle-1","capability":"mesh-private-capability-1"}"#,
662 ),
663 Some(r#"{"dataBase64":"AQID","expiresAtMs":1710000000000}"#.to_string())
664 );
665 assert_eq!(
666 ext.handle_message(
667 "meshPrivateRequest",
668 r#"{"handle":"mesh-private-handle-1","capability":"mesh-private-capability-1","dataBase64":""}"#,
669 ),
670 Some(r#""mesh-private-req-1""#.to_string())
671 );
672 assert_eq!(
673 ext.handle_message(
674 "meshPrivateControlPublish",
675 r#"{"capability":"mesh-private-capability-1","envelope":{"mode":"encrypted","dataBase64":"AQID"}}"#,
676 ),
677 Some("true".to_string())
678 );
679 assert_eq!(
680 ext.handle_message(
681 "meshPrivateControlSubscribe",
682 r#"{"capability":"mesh-private-capability-1"}"#,
683 ),
684 Some("true".to_string())
685 );
686 assert_eq!(
687 ext.handle_message(
688 "meshPrivateReceiptPublish",
689 r#"{"capability":"mesh-private-capability-1","dataBase64":"AQID"}"#,
690 ),
691 Some("true".to_string())
692 );
693 assert_eq!(
694 ext.handle_message(
695 "meshPrivateReceiptSubscribe",
696 r#"{"capability":"mesh-private-capability-1"}"#,
697 ),
698 Some("true".to_string())
699 );
700
701 let _ = MeshControlEnvelope {
702 mode: MeshControlMode::Visible,
703 data_base64: "AQID".into(),
704 };
705 }
706}