mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-22 03:13:58 +02:00
Run plugins in a QuickJS sandbox in the browser
Adds packages/plugin-sandbox: QuickJS-ng compiled to wasm, running in a dedicated worker, with a runtime shell inside it that loads a plugin bundle and answers the same InternalEventPayload events the Node runtime answers. Plugins are unmodified. Wires the browser host's template function, authentication, cURL import and template render commands to it, and relaxes TemplateCallback's Send bound on wasm32 so the engine's renderer can call back out to a plugin.
This commit is contained in:
@@ -8,12 +8,29 @@ use std::future::Future;
|
||||
|
||||
const MAX_DEPTH: usize = 50;
|
||||
|
||||
/// `Send`, except where nothing can be.
|
||||
///
|
||||
/// Rendering a template function is a host call, and on a host with one thread
|
||||
/// it is a call into JavaScript: the future holds a `JsFuture` and the callback
|
||||
/// holds the `Rc` connection pool, neither of which is `Send` nor can be made
|
||||
/// so. Every other host spawns rendering onto a thread pool and needs the bound.
|
||||
/// So the bound belongs to the targets that can keep it, rather than to the
|
||||
/// trait every host must implement.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub trait MaybeSend: Send {}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl<T: Send> MaybeSend for T {}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub trait MaybeSend {}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl<T> MaybeSend for T {}
|
||||
|
||||
pub trait TemplateCallback {
|
||||
fn run(
|
||||
&self,
|
||||
fn_name: &str,
|
||||
args: HashMap<String, serde_json::Value>,
|
||||
) -> impl Future<Output = Result<String>> + Send;
|
||||
) -> impl Future<Output = Result<String>> + MaybeSend;
|
||||
|
||||
fn transform_arg(&self, fn_name: &str, arg_name: &str, arg_value: &str) -> Result<String>;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
md5 = "0.7"
|
||||
yaak-lifecycle = { workspace = true }
|
||||
yaak-models = { workspace = true }
|
||||
# No default features: the template exports belong to @yaakapp-internal/templates, not this module
|
||||
|
||||
@@ -3,4 +3,12 @@
|
||||
// This is loaded by the SharedWorker in packages/platform/src/web/worker.ts and
|
||||
// nowhere else: it owns a SQLite database, and there must be exactly one of it
|
||||
// per origin.
|
||||
export { blob_delete, blob_get, blob_put, boot, prepare_http_send, rpc } from "./pkg";
|
||||
export {
|
||||
blob_delete,
|
||||
blob_get,
|
||||
blob_put,
|
||||
boot,
|
||||
prepare_http_send,
|
||||
render_template,
|
||||
rpc,
|
||||
} from "./pkg";
|
||||
|
||||
Vendored
+18
-3
@@ -31,10 +31,25 @@ export function boot(): Promise<void>;
|
||||
* settings, the cookie jar. Nothing here touches a socket. What comes back is what the tab
|
||||
* posts to the Yaak server.
|
||||
*
|
||||
* Refuses, with a message the user can act on, when the request needs something this host
|
||||
* doesn't have: an authentication plugin, or a template function.
|
||||
* `plugins` is the template function bridge — a JavaScript function taking a name and its
|
||||
* JSON arguments and resolving to the rendered string. Passing nothing is allowed and makes
|
||||
* every template function a refusal naming it.
|
||||
*
|
||||
* Authentication is *not* applied here even though it is part of preparing a send. It is
|
||||
* applied to the rendered request by the caller, because the plugin that applies it wants to
|
||||
* see the request as it will be sent, and the caller is the side that knows that.
|
||||
*/
|
||||
export function prepare_http_send(payload: any): Promise<any>;
|
||||
export function prepare_http_send(payload: any, plugins: any): Promise<any>;
|
||||
|
||||
/**
|
||||
* Render one template string against an environment chain.
|
||||
*
|
||||
* What `cmd_render_template` does on the desktop, for the same callers: the value previews
|
||||
* under an editor, and anywhere the app shows what a template will become. `ignore_error`
|
||||
* picks the same behaviour it picks there — a preview shows an empty string where a send
|
||||
* would refuse, because a half-typed template is not yet a mistake.
|
||||
*/
|
||||
export function render_template(payload: any, plugins: any): Promise<any>;
|
||||
|
||||
/**
|
||||
* Run one command as `label` (the calling tab's identity, which stands in for
|
||||
|
||||
@@ -66,15 +66,37 @@ export function boot() {
|
||||
* Resolve and render a request for sending, exactly as the desktop does before it puts the
|
||||
* request on the network: the environment chain, inherited headers and auth, request
|
||||
* settings, the cookie jar. Nothing here touches a socket. What comes back is what the tab
|
||||
* posts to the Yaak server.
|
||||
* posts to the send proxy.
|
||||
*
|
||||
* Refuses, with a message the user can act on, when the request needs something this host
|
||||
* doesn't have: an authentication plugin, or a template function.
|
||||
* `plugins` is the template function bridge — a JavaScript function taking a name and its
|
||||
* JSON arguments and resolving to the rendered string. Passing nothing is allowed and makes
|
||||
* every template function a refusal naming it.
|
||||
*
|
||||
* Authentication is *not* applied here even though it is part of preparing a send. It is
|
||||
* applied to the rendered request by the caller, because the plugin that applies it wants to
|
||||
* see the request as it will be sent, and the caller is the side that knows that.
|
||||
* @param {any} payload
|
||||
* @param {any} plugins
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function prepare_http_send(payload) {
|
||||
const ret = wasm.prepare_http_send(payload);
|
||||
export function prepare_http_send(payload, plugins) {
|
||||
const ret = wasm.prepare_http_send(payload, plugins);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render one template string against an environment chain.
|
||||
*
|
||||
* What `cmd_render_template` does on the desktop, for the same callers: the value previews
|
||||
* under an editor, and anywhere the app shows what a template will become. `ignore_error`
|
||||
* picks the same behaviour it picks there — a preview shows an empty string where a send
|
||||
* would refuse, because a half-typed template is not yet a mistake.
|
||||
* @param {any} payload
|
||||
* @param {any} plugins
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function render_template(payload, plugins) {
|
||||
const ret = wasm.render_template(payload, plugins);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -225,6 +247,10 @@ export function __wbg_call_dfde26266607c996() { return handleError(function (arg
|
||||
const ret = arg0.call(arg1, arg2);
|
||||
return ret;
|
||||
}, arguments); }
|
||||
export function __wbg_call_faa0a261f288f846() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||
const ret = arg0.call(arg1, arg2, arg3);
|
||||
return ret;
|
||||
}, arguments); }
|
||||
export function __wbg_clear_bb1b3ff877b62598() { return handleError(function (arg0) {
|
||||
const ret = arg0.clear();
|
||||
return ret;
|
||||
@@ -669,6 +695,10 @@ export function __wbg_then_837494e384b37459(arg0, arg1) {
|
||||
const ret = arg0.then(arg1);
|
||||
return ret;
|
||||
}
|
||||
export function __wbg_then_bd927500e8905df2(arg0, arg1, arg2) {
|
||||
const ret = arg0.then(arg1, arg2);
|
||||
return ret;
|
||||
}
|
||||
export function __wbg_toString_1dda136fd8f30a5f(arg0) {
|
||||
const ret = arg0.toString();
|
||||
return ret;
|
||||
@@ -697,22 +727,22 @@ export function __wbg_warn_b6f36cac66fc96a4(arg0, arg1) {
|
||||
console.warn(arg0, arg1);
|
||||
}
|
||||
export function __wbindgen_cast_0000000000000001(arg0, arg1) {
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1117, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1140, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha1c2fa93df0107f3);
|
||||
return ret;
|
||||
}
|
||||
export function __wbindgen_cast_0000000000000002(arg0, arg1) {
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 212, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 229, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha7903b6e296dd8f4);
|
||||
return ret;
|
||||
}
|
||||
export function __wbindgen_cast_0000000000000003(arg0, arg1) {
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("IDBVersionChangeEvent")], shim_idx: 83, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h4381d8e749fe46cf);
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("IDBVersionChangeEvent")], shim_idx: 74, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h20ab1db2d80221ce);
|
||||
return ret;
|
||||
}
|
||||
export function __wbindgen_cast_0000000000000004(arg0, arg1) {
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 210, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 227, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__ha1b480b83daa641f);
|
||||
return ret;
|
||||
}
|
||||
@@ -765,8 +795,8 @@ function wasm_bindgen__convert__closures_____invoke__ha1c2fa93df0107f3(arg0, arg
|
||||
}
|
||||
}
|
||||
|
||||
function wasm_bindgen__convert__closures_____invoke__h4381d8e749fe46cf(arg0, arg1, arg2) {
|
||||
const ret = wasm.wasm_bindgen__convert__closures_____invoke__h4381d8e749fe46cf(arg0, arg1, arg2);
|
||||
function wasm_bindgen__convert__closures_____invoke__h20ab1db2d80221ce(arg0, arg1, arg2) {
|
||||
const ret = wasm.wasm_bindgen__convert__closures_____invoke__h20ab1db2d80221ce(arg0, arg1, arg2);
|
||||
if (ret[1]) {
|
||||
throw takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
Binary file not shown.
+3
-2
@@ -5,7 +5,8 @@ export const blob_delete: (a: number, b: number) => [number, number];
|
||||
export const blob_get: (a: number, b: number) => [number, number, number, number];
|
||||
export const blob_put: (a: number, b: number, c: number, d: number) => [number, number];
|
||||
export const boot: () => any;
|
||||
export const prepare_http_send: (a: any) => any;
|
||||
export const prepare_http_send: (a: any, b: any) => any;
|
||||
export const render_template: (a: any, b: any) => any;
|
||||
export const rpc: (a: number, b: number, c: any, d: number, e: number) => [number, number, number];
|
||||
export const rust_sqlite_wasm_abort: () => void;
|
||||
export const rust_sqlite_wasm_assert_fail: (a: number, b: number, c: number, d: number) => void;
|
||||
@@ -18,7 +19,7 @@ export const rust_sqlite_wasm_realloc: (a: number, b: number) => number;
|
||||
export const sqlite3_os_end: () => number;
|
||||
export const sqlite3_os_init: () => number;
|
||||
export const wasm_bindgen__convert__closures_____invoke__ha1c2fa93df0107f3: (a: number, b: number, c: any) => [number, number];
|
||||
export const wasm_bindgen__convert__closures_____invoke__h4381d8e749fe46cf: (a: number, b: number, c: any) => [number, number];
|
||||
export const wasm_bindgen__convert__closures_____invoke__h20ab1db2d80221ce: (a: number, b: number, c: any) => [number, number];
|
||||
export const wasm_bindgen__convert__closures_____invoke__h2cf3f4cce3b29948: (a: number, b: number, c: any, d: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__ha7903b6e296dd8f4: (a: number, b: number, c: any) => void;
|
||||
export const wasm_bindgen__convert__closures_____invoke__ha1b480b83daa641f: (a: number, b: number) => void;
|
||||
|
||||
+151
-33
@@ -232,6 +232,14 @@ struct PersistSendCookiesReq {
|
||||
after: Vec<Cookie>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PluginKeyValueReq {
|
||||
plugin_name: String,
|
||||
key: String,
|
||||
value: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct InsertResponseEventsReq {
|
||||
@@ -415,6 +423,35 @@ fn dispatch(
|
||||
to_json(())
|
||||
}
|
||||
|
||||
// A plugin's own storage, namespaced by plugin name exactly as the desktop namespaces
|
||||
// it (`build_shared_reply` in crates/yaak/src/plugin_events.rs), so a plugin that keeps
|
||||
// a token here finds it under the same key on either host.
|
||||
"web_plugin_kv_get" => {
|
||||
let req: PluginKeyValueReq = from_js(payload)?;
|
||||
let found = host.queries.connect().get_plugin_key_value(&req.plugin_name, &req.key);
|
||||
to_json(found.map(|kv| kv.value))
|
||||
}
|
||||
|
||||
"web_plugin_kv_set" => {
|
||||
let req: PluginKeyValueReq = from_js(payload)?;
|
||||
host.queries.connect().set_plugin_key_value(
|
||||
&req.plugin_name,
|
||||
&req.key,
|
||||
&req.value.unwrap_or_default(),
|
||||
);
|
||||
to_json(())
|
||||
}
|
||||
|
||||
"web_plugin_kv_delete" => {
|
||||
let req: PluginKeyValueReq = from_js(payload)?;
|
||||
let deleted = host
|
||||
.queries
|
||||
.connect()
|
||||
.delete_plugin_key_value(&req.plugin_name, &req.key)
|
||||
.map_err(js_error)?;
|
||||
to_json(deleted)
|
||||
}
|
||||
|
||||
other => Err(js_error(format!("yaak-web: `{other}` is not a command this host answers"))),
|
||||
}
|
||||
}
|
||||
@@ -439,6 +476,10 @@ struct PreparedHttpSend {
|
||||
/// The request with inherited headers and authentication applied and every template
|
||||
/// rendered. What the proxy sends, and what the response records as its request.
|
||||
request: HttpRequest,
|
||||
/// Identifies whichever model the authentication was inherited from, hashed the way the
|
||||
/// desktop hashes it. Plugins key their stored state on it — an OAuth token cache belongs
|
||||
/// to the folder that declared the auth, not to each request under it.
|
||||
auth_context_id: String,
|
||||
settings: HttpSendSettings,
|
||||
/// The `* Setting name=value` timeline lines the desktop writes at the top of a send,
|
||||
/// sources and all. The tab records them before the proxy's own events.
|
||||
@@ -447,22 +488,51 @@ struct PreparedHttpSend {
|
||||
cookie_jar: Option<CookieJar>,
|
||||
}
|
||||
|
||||
/// A template callback for a host with no plugins. Variables render; a function is a clear
|
||||
/// refusal naming the function, so the user knows what the request needs rather than seeing
|
||||
/// an empty string sent in its place.
|
||||
struct NoPluginsCallback;
|
||||
/// A template callback that calls a template function wherever the caller keeps them.
|
||||
///
|
||||
/// The desktop's equivalent (`PluginTemplateCallback`) reaches a plugin process; this one
|
||||
/// reaches a JavaScript function the worker installed, which forwards to the sandbox and back.
|
||||
/// Both hand the renderer the same thing — a string, or an error naming what failed — so a
|
||||
/// template renders identically on either host or fails for the same reason.
|
||||
///
|
||||
/// Without a function to call, a template function is a clear refusal naming it, which tells
|
||||
/// the user what the request needs rather than sending an empty string in its place.
|
||||
struct JsTemplateCallback {
|
||||
call: Option<js_sys::Function>,
|
||||
}
|
||||
|
||||
impl TemplateCallback for NoPluginsCallback {
|
||||
impl TemplateCallback for JsTemplateCallback {
|
||||
fn run(
|
||||
&self,
|
||||
fn_name: &str,
|
||||
_args: HashMap<String, serde_json::Value>,
|
||||
) -> impl std::future::Future<Output = yaak_templates::error::Result<String>> + Send {
|
||||
let message = format!(
|
||||
"This request uses the template function \"{fn_name}\", which needs plugins. \
|
||||
Plugins aren't available in the browser yet"
|
||||
);
|
||||
async move { Err(yaak_templates::error::Error::RenderError(message)) }
|
||||
args: HashMap<String, serde_json::Value>,
|
||||
) -> impl std::future::Future<Output = yaak_templates::error::Result<String>> {
|
||||
// Built before the async block so the future holds only owned values.
|
||||
let call = self.call.clone();
|
||||
let fn_name = fn_name.to_string();
|
||||
let args = serde_json::to_string(&args).unwrap_or_else(|_| "{}".into());
|
||||
|
||||
async move {
|
||||
use yaak_templates::error::Error::RenderError;
|
||||
|
||||
let Some(call) = call else {
|
||||
return Err(RenderError(format!(
|
||||
"This request uses the template function \"{fn_name}\", which needs plugins. \
|
||||
No plugin provides it"
|
||||
)));
|
||||
};
|
||||
|
||||
let promise = call
|
||||
.call2(&JsValue::NULL, &JsValue::from_str(&fn_name), &JsValue::from_str(&args))
|
||||
.map_err(|e| RenderError(js_message(&e)))?;
|
||||
let value = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(promise))
|
||||
.await
|
||||
.map_err(|e| RenderError(js_message(&e)))?;
|
||||
|
||||
value.as_string().ok_or_else(|| {
|
||||
RenderError(format!("Template function \"{fn_name}\" did not return a string"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn transform_arg(
|
||||
@@ -475,19 +545,40 @@ impl TemplateCallback for NoPluginsCallback {
|
||||
}
|
||||
}
|
||||
|
||||
/// The message out of a rejected promise or a thrown error, without the `Error:` wrapper.
|
||||
fn js_message(value: &JsValue) -> String {
|
||||
if let Some(text) = value.as_string() {
|
||||
return text;
|
||||
}
|
||||
let message = js_sys::Reflect::get(value, &JsValue::from_str("message"))
|
||||
.ok()
|
||||
.and_then(|m| m.as_string());
|
||||
message.unwrap_or_else(|| format!("{value:?}"))
|
||||
}
|
||||
|
||||
/// The template function bridge, or none when the caller passed nothing.
|
||||
fn template_callback(plugins: JsValue) -> JsTemplateCallback {
|
||||
JsTemplateCallback { call: plugins.dyn_into::<js_sys::Function>().ok() }
|
||||
}
|
||||
|
||||
/// Resolve and render a request for sending, exactly as the desktop does before it puts the
|
||||
/// request on the network: the environment chain, inherited headers and auth, request
|
||||
/// settings, the cookie jar. Nothing here touches a socket. What comes back is what the tab
|
||||
/// posts to the Yaak server.
|
||||
///
|
||||
/// Refuses, with a message the user can act on, when the request needs something this host
|
||||
/// doesn't have: an authentication plugin, or a template function.
|
||||
/// `plugins` is the template function bridge — a JavaScript function taking a name and its
|
||||
/// JSON arguments and resolving to the rendered string. Passing nothing is allowed and makes
|
||||
/// every template function a refusal naming it.
|
||||
///
|
||||
/// Authentication is *not* applied here even though it is part of preparing a send. It is
|
||||
/// applied to the rendered request by the caller, because the plugin that applies it wants to
|
||||
/// see the request as it will be sent, and the caller is the side that knows that.
|
||||
#[wasm_bindgen]
|
||||
pub async fn prepare_http_send(payload: JsValue) -> Result<JsValue> {
|
||||
pub async fn prepare_http_send(payload: JsValue, plugins: JsValue) -> Result<JsValue> {
|
||||
let req: PrepareHttpSendReq = from_js(payload)?;
|
||||
|
||||
// Everything from the database first, then release the host borrow before rendering.
|
||||
let (request, environment_chain, settings, cookie_jar) = with_host(|host| {
|
||||
let (request, environment_chain, settings, cookie_jar, auth_context_id) = with_host(|host| {
|
||||
let db = host.queries.connect();
|
||||
let request = db.get_http_request(&req.request_id).map_err(js_error)?;
|
||||
let environment_chain = db
|
||||
@@ -497,7 +588,7 @@ pub async fn prepare_http_send(payload: JsValue) -> Result<JsValue> {
|
||||
req.environment_id.as_deref(),
|
||||
)
|
||||
.map_err(js_error)?;
|
||||
let (authentication_type, authentication, _auth_context_id) =
|
||||
let (authentication_type, authentication, auth_context_id) =
|
||||
db.resolve_auth_for_http_request(&request).map_err(js_error)?;
|
||||
let headers = db.resolve_headers_for_http_request(&request).map_err(js_error)?;
|
||||
let settings = db.resolve_settings_for_http_request(&request).map_err(js_error)?;
|
||||
@@ -506,34 +597,21 @@ pub async fn prepare_http_send(payload: JsValue) -> Result<JsValue> {
|
||||
None => None,
|
||||
};
|
||||
let request = HttpRequest { authentication_type, authentication, headers, ..request };
|
||||
Ok((request, environment_chain, settings, cookie_jar))
|
||||
Ok((request, environment_chain, settings, cookie_jar, auth_context_id))
|
||||
})?;
|
||||
|
||||
let rendered = render_http_request(
|
||||
&request,
|
||||
environment_chain,
|
||||
&NoPluginsCallback,
|
||||
&template_callback(plugins),
|
||||
&RenderOptions::throw(),
|
||||
)
|
||||
.await
|
||||
.map_err(js_error)?;
|
||||
|
||||
// Authentication is applied by a plugin on the desktop. There is no plugin here, and a
|
||||
// request sent without the auth it asked for is worse than one refused with the reason.
|
||||
let auth_disabled =
|
||||
rendered.authentication.get("disabled").and_then(|v| v.as_bool()) == Some(true);
|
||||
if let Some(auth_type) = rendered.authentication_type.as_deref()
|
||||
&& auth_type != "none"
|
||||
&& !auth_disabled
|
||||
{
|
||||
return Err(js_error(format!(
|
||||
"This request uses {auth_type} authentication, which needs plugins. \
|
||||
Plugins aren't available in the browser yet"
|
||||
)));
|
||||
}
|
||||
|
||||
let prepared = PreparedHttpSend {
|
||||
request: rendered,
|
||||
auth_context_id: format!("{:x}", md5::compute(auth_context_id)),
|
||||
settings: HttpSendSettings::from(&settings),
|
||||
setting_events: settings.timeline_events(),
|
||||
cookie_jar,
|
||||
@@ -544,6 +622,46 @@ pub async fn prepare_http_send(payload: JsValue) -> Result<JsValue> {
|
||||
prepared.serialize(&serde_wasm_bindgen::Serializer::json_compatible()).map_err(js_error)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RenderTemplateReq {
|
||||
template: String,
|
||||
workspace_id: String,
|
||||
environment_id: Option<String>,
|
||||
ignore_error: Option<bool>,
|
||||
}
|
||||
|
||||
/// Render one template string against an environment chain.
|
||||
///
|
||||
/// What `cmd_render_template` does on the desktop, for the same callers: the value previews
|
||||
/// under an editor, and anywhere the app shows what a template will become. `ignore_error`
|
||||
/// picks the same behaviour it picks there — a preview shows an empty string where a send
|
||||
/// would refuse, because a half-typed template is not yet a mistake.
|
||||
#[wasm_bindgen]
|
||||
pub async fn render_template(payload: JsValue, plugins: JsValue) -> Result<JsValue> {
|
||||
let req: RenderTemplateReq = from_js(payload)?;
|
||||
|
||||
let environment_chain = with_host(|host| {
|
||||
host.queries
|
||||
.connect()
|
||||
.resolve_environments(&req.workspace_id, None, req.environment_id.as_deref())
|
||||
.map_err(js_error)
|
||||
})?;
|
||||
|
||||
let vars = yaak_models::render::make_vars_hashmap(environment_chain);
|
||||
let options = if req.ignore_error == Some(true) {
|
||||
RenderOptions::return_empty()
|
||||
} else {
|
||||
RenderOptions::throw()
|
||||
};
|
||||
|
||||
let rendered =
|
||||
yaak_templates::parse_and_render(&req.template, &vars, &template_callback(plugins), &options)
|
||||
.await
|
||||
.map_err(js_error)?;
|
||||
to_json(rendered).map(|v| JsValue::from_str(v.as_str().unwrap_or_default()))
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Blobs */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
Reference in New Issue
Block a user