rahti-native 0.0.2

Run a Rahti application inside a native package: packaged paths, a loopback-only embedded server, and a per-installation session key.
Documentation
//! `pp.native` — the browser side of the capability bridge.
//!
//! ## Why this is a separate script
//!
//! PulsePoint ships as a prebuilt minified bundle, and that bundle is not
//! edited: not by hand, not by a build step, not to add a namespace. So the
//! native surface arrives beside it instead. This script attaches `native` to
//! `pp` the moment the bundle publishes it, and PulsePoint itself is byte for
//! byte the file a web project serves.
//!
//! It is framework-owned — generated from this crate rather than written into
//! the application's `public/` — so it cannot drift from [`crate::commands`],
//! and so a project that never opens a native package never serves it.
//!
//! ## The one rule for a page that uses it
//!
//! **The same page has to work as a website.** `pp.native` is undefined in a
//! browser, and a page that assumed otherwise is a page the project's web
//! deployment 500s on. Every use is guarded:
//!
//! ```javascript
//! if (pp.native?.has("choose-file")) {
//!   const file = await pp.native.invoke("choose_file", { extensions: ["png"] });
//! } else {
//!   fileInput.click();   // the web does this perfectly well already
//! }
//! ```
//!
//! ## What it does not do
//!
//! It does not wrap `window.__TAURI__` and re-export it. `invoke` refuses any
//! name that is not in the Rust allowlist before it reaches Tauri, so a page
//! cannot reach a Tauri plugin command that the application never meant to
//! expose merely because the plugin was linked in.

use axum::Router;
use axum::http::{HeaderValue, header};
use axum::response::{IntoResponse, Response};
use axum::routing::get;

/// Where the shell serves the bridge from, for a host that would rather link
/// it than inject it.
pub const BRIDGE_PATH: &str = "/__rahti/native.js";

/// The script, with the current allowlist compiled into it.
///
/// `platform` is the value `pp.native.platform` reports; the generated shell
/// passes [`crate::Platform::current`].
pub fn bridge_script(platform: &str, version: &str) -> String {
    let capabilities = json_array(&crate::capabilities::capability_names());
    let commands: Vec<&str> = crate::capabilities::commands()
        .iter()
        .map(|c| c.name)
        .collect();
    let allowed = json_array(&commands);
    let platform = json_string(platform);
    let version = json_string(version);

    format!(
        r#"// Generated by rahti-native. Do not edit.
//
// Attaches `pp.native` to the PulsePoint runtime. The runtime bundle itself is
// untouched: this listens for the moment it publishes `pp` and adds one
// property to it.
(function () {{
  "use strict";

  var ALLOWED = {allowed};
  var CAPABILITIES = {capabilities};

  function tauri() {{
    var t = window.__TAURI__;
    return t && t.core && typeof t.core.invoke === "function" ? t.core : null;
  }}

  var api = {{
    // `true` inside a native package, and absent everywhere else — which is
    // why a page checks `pp.native` before it checks anything on it.
    available: true,
    platform: {platform},
    version: {version},

    has: function (capability) {{
      return CAPABILITIES.indexOf(capability) !== -1 && tauri() !== null;
    }},

    // Refused here as well as in Rust. This is not the security boundary — the
    // shell's allowlist and Tauri's capability file are — but a name that was
    // never meant to be callable should fail where the caller can see why.
    invoke: function (name, payload) {{
      if (ALLOWED.indexOf(name) === -1) {{
        return Promise.reject(
          new Error("pp.native: `" + name + "` is not a native command this application exposes.")
        );
      }}
      var core = tauri();
      if (!core) {{
        return Promise.reject(new Error("pp.native: the native bridge is not available."));
      }}
      return core.invoke(name, payload || {{}});
    }},
  }};

  Object.freeze(api);

  // The access point a page uses, and the reason it is a global rather than a
  // property of `pp`.
  //
  // PulsePoint compiles the expressions in a reactive block into a function of
  // its own making, and the `pp` visible inside that function is a scoped
  // object it supplies — not `window.pp`. So `pp.state(...)` works there and
  // `pp.native` cannot, without editing the shipped runtime bundle, which is
  // not something Rahti does.
  //
  // `window` *is* reachable from inside a compiled block, so this is.
  window.rahtiNative = api;

  // Best-effort, and documented nowhere as the way in: in a context where
  // `pp` *is* the global object, `pp.native` also works and reads a little
  // better. Nothing depends on it succeeding.
  window.__rahtiNativeAttach = "not attempted";

  function attach(pp) {{
    if (!pp) {{
      return false;
    }}
    if (pp.native) {{
      return true;
    }}
    try {{
      pp.native = api;
    }} catch (e) {{
      window.__rahtiNativeAttach = "assign threw: " + e.message;
    }}
    if (!pp.native) {{
      try {{
        Object.defineProperty(pp, "native", {{
          value: api,
          configurable: true,
          enumerable: true,
        }});
      }} catch (e) {{
        window.__rahtiNativeAttach =
          "frozen=" + Object.isFrozen(pp) + " extensible=" + Object.isExtensible(pp);
        return false;
      }}
    }}
    if (pp.native) {{
      window.__rahtiNativeAttach = "attached";
    }}
    return !!pp.native;
  }}

  // The bundle assigns `globalThis.pp` when it loads, which is after this
  // script runs. Intercepting the assignment is what lets the namespace be
  // added without the bundle knowing anything about it.
  if (!attach(window.pp)) {{
    var stored;
    try {{
      Object.defineProperty(window, "pp", {{
        configurable: true,
        get: function () {{
          return stored;
        }},
        set: function (value) {{
          stored = value;
          attach(value);
        }},
      }});
    }} catch (e) {{
      window.__rahtiNativeAttach = "trap threw: " + e.message;
    }}

    // The trap is not enough on its own, and neither is attaching once.
    //
    // A runtime that publishes itself with `Object.defineProperty` replaces the
    // accessor rather than calling it, so the set never happens. And a runtime
    // that *replaces* `window.pp` later — a richer object after mount, say —
    // silently drops a `native` that was attached to the object before it. Both
    // were observed: the attach reported success and `pp.native` was undefined
    // by the time a page read it.
    //
    // So the attach is not an event, it is a condition that is kept true. The
    // check is a property lookup a few times a second and costs nothing
    // measurable; being wrong costs the whole native surface, silently.
    setInterval(function () {{
      if (window.pp && !window.pp.native) {{
        attach(window.pp);
      }}
    }}, 250);
  }}
}})();
"#
    )
}

/// A route serving the bridge, for a host that links it from the document
/// rather than injecting it at WebView creation.
///
/// Injection is what the generated shell does — it runs before any page script
/// and needs no route — and this exists for the case where that is not
/// available.
pub fn bridge_route(platform: &'static str, version: String) -> Router {
    Router::new().route(
        BRIDGE_PATH,
        get(move || {
            let body = bridge_script(platform, &version);
            async move {
                let mut response = body.into_response();
                response.headers_mut().insert(
                    header::CONTENT_TYPE,
                    HeaderValue::from_static("text/javascript; charset=utf-8"),
                );
                response
                    .headers_mut()
                    .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
                response as Response
            }
        }),
    )
}

fn json_array(values: &[&str]) -> String {
    let items: Vec<String> = values.iter().map(|v| json_string(v)).collect();
    format!("[{}]", items.join(", "))
}

/// A JSON string literal, with `<` neutralized for the same reason
/// `rahti::Json` does it: this text is written inside a script, and `</script>`
/// in a value would end it.
fn json_string(value: &str) -> String {
    serde_json::to_string(value)
        .unwrap_or_else(|_| "\"\"".to_string())
        .replace('<', "\\u003c")
}