mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-25 04:44:12 +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:
@@ -108,12 +108,44 @@ function bootOnce(): Promise<void> {
|
||||
return booted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Template functions, which live somewhere this worker cannot reach.
|
||||
*
|
||||
* Rendering is the engine's, and the engine is here. The functions it calls
|
||||
* come from plugins, which run in a sandbox the tab owns — so the engine is
|
||||
* handed a function that asks the tab. It asks the port that started the
|
||||
* render, not every port, because only that tab is waiting on the answer and
|
||||
* only its sandbox has the plugins the render was started against.
|
||||
*
|
||||
* A failure comes back as a rejection, which the engine turns into a render
|
||||
* error naming the function. That matters: rendering `${[ uuid.v4() ]}` to an
|
||||
* empty string and sending it would be worse than not sending at all.
|
||||
*/
|
||||
const pendingTemplateFunctions = new Map<number, (result: string | Error) => void>();
|
||||
let nextTemplateFunctionId = 1;
|
||||
|
||||
function templateBridge(port: MessagePort): (name: string, args: string) => Promise<string> {
|
||||
return (name, args) =>
|
||||
new Promise<string>((resolve, reject) => {
|
||||
const id = nextTemplateFunctionId++;
|
||||
pendingTemplateFunctions.set(id, (r) => (r instanceof Error ? reject(r) : resolve(r)));
|
||||
send(port, { type: "template_function", id, name, args });
|
||||
});
|
||||
}
|
||||
|
||||
async function handle(port: MessagePort, message: ToWorker): Promise<void> {
|
||||
if (message.type === "goodbye") {
|
||||
ports.delete(port);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === "template_function_result") {
|
||||
const settle = pendingTemplateFunctions.get(message.id);
|
||||
pendingTemplateFunctions.delete(message.id);
|
||||
settle?.(message.error != null ? new Error(message.error) : (message.value ?? ""));
|
||||
return;
|
||||
}
|
||||
|
||||
// Every command waits for boot rather than the tab having to. Tabs post
|
||||
// the moment they load; the port queues; this drains once the DB is open.
|
||||
try {
|
||||
@@ -122,7 +154,8 @@ async function handle(port: MessagePort, message: ToWorker): Promise<void> {
|
||||
send(port, { type: "error", id: message.id, message: bootError ?? "Database failed to open" });
|
||||
return;
|
||||
}
|
||||
const { rpc, blob_get, blob_put, blob_delete, prepare_http_send } = engine!;
|
||||
const { rpc, blob_get, blob_put, blob_delete, prepare_http_send, render_template } =
|
||||
engine!;
|
||||
|
||||
try {
|
||||
switch (message.type) {
|
||||
@@ -143,10 +176,15 @@ async function handle(port: MessagePort, message: ToWorker): Promise<void> {
|
||||
return;
|
||||
}
|
||||
case "prepare_http_send": {
|
||||
const prepared = await prepare_http_send(message.payload);
|
||||
const prepared = await prepare_http_send(message.payload, templateBridge(port));
|
||||
send(port, { type: "result", id: message.id, result: prepared });
|
||||
return;
|
||||
}
|
||||
case "render_template": {
|
||||
const rendered = await render_template(message.payload, templateBridge(port));
|
||||
send(port, { type: "result", id: message.id, result: rendered });
|
||||
return;
|
||||
}
|
||||
case "blob_get": {
|
||||
const bytes = blob_get(message.blobId);
|
||||
if (bytes == null) {
|
||||
|
||||
Reference in New Issue
Block a user