1use axum::Router;
37use axum::http::{HeaderValue, header};
38use axum::response::{IntoResponse, Response};
39use axum::routing::get;
40
41pub const BRIDGE_PATH: &str = "/__rahti/native.js";
44
45pub fn bridge_script(platform: &str, version: &str) -> String {
50 let capabilities = json_array(&crate::capabilities::capability_names());
51 let commands: Vec<&str> = crate::capabilities::commands()
52 .iter()
53 .map(|c| c.name)
54 .collect();
55 let allowed = json_array(&commands);
56 let platform = json_string(platform);
57 let version = json_string(version);
58
59 format!(
60 r#"// Generated by rahti-native. Do not edit.
61//
62// Attaches `pp.native` to the PulsePoint runtime. The runtime bundle itself is
63// untouched: this listens for the moment it publishes `pp` and adds one
64// property to it.
65(function () {{
66 "use strict";
67
68 var ALLOWED = {allowed};
69 var CAPABILITIES = {capabilities};
70
71 function tauri() {{
72 var t = window.__TAURI__;
73 return t && t.core && typeof t.core.invoke === "function" ? t.core : null;
74 }}
75
76 var api = {{
77 // `true` inside a native package, and absent everywhere else — which is
78 // why a page checks `pp.native` before it checks anything on it.
79 available: true,
80 platform: {platform},
81 version: {version},
82
83 has: function (capability) {{
84 return CAPABILITIES.indexOf(capability) !== -1 && tauri() !== null;
85 }},
86
87 // Refused here as well as in Rust. This is not the security boundary — the
88 // shell's allowlist and Tauri's capability file are — but a name that was
89 // never meant to be callable should fail where the caller can see why.
90 invoke: function (name, payload) {{
91 if (ALLOWED.indexOf(name) === -1) {{
92 return Promise.reject(
93 new Error("pp.native: `" + name + "` is not a native command this application exposes.")
94 );
95 }}
96 var core = tauri();
97 if (!core) {{
98 return Promise.reject(new Error("pp.native: the native bridge is not available."));
99 }}
100 return core.invoke(name, payload || {{}});
101 }},
102 }};
103
104 Object.freeze(api);
105
106 // The access point a page uses, and the reason it is a global rather than a
107 // property of `pp`.
108 //
109 // PulsePoint compiles the expressions in a reactive block into a function of
110 // its own making, and the `pp` visible inside that function is a scoped
111 // object it supplies — not `window.pp`. So `pp.state(...)` works there and
112 // `pp.native` cannot, without editing the shipped runtime bundle, which is
113 // not something Rahti does.
114 //
115 // `window` *is* reachable from inside a compiled block, so this is.
116 window.rahtiNative = api;
117
118 // Best-effort, and documented nowhere as the way in: in a context where
119 // `pp` *is* the global object, `pp.native` also works and reads a little
120 // better. Nothing depends on it succeeding.
121 window.__rahtiNativeAttach = "not attempted";
122
123 function attach(pp) {{
124 if (!pp) {{
125 return false;
126 }}
127 if (pp.native) {{
128 return true;
129 }}
130 try {{
131 pp.native = api;
132 }} catch (e) {{
133 window.__rahtiNativeAttach = "assign threw: " + e.message;
134 }}
135 if (!pp.native) {{
136 try {{
137 Object.defineProperty(pp, "native", {{
138 value: api,
139 configurable: true,
140 enumerable: true,
141 }});
142 }} catch (e) {{
143 window.__rahtiNativeAttach =
144 "frozen=" + Object.isFrozen(pp) + " extensible=" + Object.isExtensible(pp);
145 return false;
146 }}
147 }}
148 if (pp.native) {{
149 window.__rahtiNativeAttach = "attached";
150 }}
151 return !!pp.native;
152 }}
153
154 // The bundle assigns `globalThis.pp` when it loads, which is after this
155 // script runs. Intercepting the assignment is what lets the namespace be
156 // added without the bundle knowing anything about it.
157 if (!attach(window.pp)) {{
158 var stored;
159 try {{
160 Object.defineProperty(window, "pp", {{
161 configurable: true,
162 get: function () {{
163 return stored;
164 }},
165 set: function (value) {{
166 stored = value;
167 attach(value);
168 }},
169 }});
170 }} catch (e) {{
171 window.__rahtiNativeAttach = "trap threw: " + e.message;
172 }}
173
174 // The trap is not enough on its own, and neither is attaching once.
175 //
176 // A runtime that publishes itself with `Object.defineProperty` replaces the
177 // accessor rather than calling it, so the set never happens. And a runtime
178 // that *replaces* `window.pp` later — a richer object after mount, say —
179 // silently drops a `native` that was attached to the object before it. Both
180 // were observed: the attach reported success and `pp.native` was undefined
181 // by the time a page read it.
182 //
183 // So the attach is not an event, it is a condition that is kept true. The
184 // check is a property lookup a few times a second and costs nothing
185 // measurable; being wrong costs the whole native surface, silently.
186 setInterval(function () {{
187 if (window.pp && !window.pp.native) {{
188 attach(window.pp);
189 }}
190 }}, 250);
191 }}
192}})();
193"#
194 )
195}
196
197pub fn bridge_route(platform: &'static str, version: String) -> Router {
204 Router::new().route(
205 BRIDGE_PATH,
206 get(move || {
207 let body = bridge_script(platform, &version);
208 async move {
209 let mut response = body.into_response();
210 response.headers_mut().insert(
211 header::CONTENT_TYPE,
212 HeaderValue::from_static("text/javascript; charset=utf-8"),
213 );
214 response
215 .headers_mut()
216 .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
217 response as Response
218 }
219 }),
220 )
221}
222
223fn json_array(values: &[&str]) -> String {
224 let items: Vec<String> = values.iter().map(|v| json_string(v)).collect();
225 format!("[{}]", items.join(", "))
226}
227
228fn json_string(value: &str) -> String {
232 serde_json::to_string(value)
233 .unwrap_or_else(|_| "\"\"".to_string())
234 .replace('<', "\\u003c")
235}