tools/sieve-playground builds the vendored sieve-rs 0.7.3 (vnd.inbuxa.* names included) to WebAssembly, with a static page around it, for inbuxa Admin's Debug button on a Sieve script: until now that button sent the script to an outside site whose interpreter knows only upstream's extension names, so any script that requires vnd.inbuxa.* failed to compile there. Ported from the playground in the upstream sieve-rs repository's web/ directory (commit 549a2d57f7), which targets sieve-rs 1.x: - run.rs and handler.rs drive 0.7's event loop the way the server's delivery does, carrying on after a runtime error; llm_prompt compiles and answers false, as on a server without a model. - The expression functions are the server's own modules, included by path; tests fail if the list, ApplyString or the few functions copied from misc.rs/unicode.rs stop matching crates/common. - Built for wasm32-wasip1 rather than with wasm-bindgen: sieve-rs and mail-builder read the clock, which panics on wasm32-unknown-unknown. The page's worker answers the six WASI calls, giving the module the playground's current-time setting. shim/gethostname stands in for a crate with no WASI build. Nothing in vendor/ or crates/ changes. - The page carries no upstream names or links; storage keys and the samples are renamed to match. build.sh --admin <dir> stages the result into an inbuxa-admin checkout.
95 lines
3.2 KiB
JavaScript
95 lines
3.2 KiB
JavaScript
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
* SPDX-FileCopyrightText: 2026 Coffey Labs LLC
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*
|
|
* Modified by Coffey Labs in 2026 for INBUXA.
|
|
*/
|
|
|
|
const SHARE_PARAM = "w";
|
|
const FORMAT = 1;
|
|
export const LONG_LINK = 8000;
|
|
|
|
function toBase64Url(bytes) {
|
|
let binary = "";
|
|
const chunk = 0x8000;
|
|
for (let i = 0; i < bytes.length; i += chunk) {
|
|
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
|
|
}
|
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
}
|
|
|
|
function fromBase64Url(text) {
|
|
const base64 = text.replace(/-/g, "+").replace(/_/g, "/");
|
|
const binary = atob(base64 + "===".slice((base64.length + 3) % 4));
|
|
const bytes = new Uint8Array(binary.length);
|
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
return bytes;
|
|
}
|
|
|
|
async function pipe(bytes, stream) {
|
|
const response = new Response(new Blob([bytes]).stream().pipeThrough(stream));
|
|
return new Uint8Array(await response.arrayBuffer());
|
|
}
|
|
|
|
export function settingsDiff(settings, defaults) {
|
|
const diff = {};
|
|
for (const [key, value] of Object.entries(settings || {})) {
|
|
if (JSON.stringify(value) !== JSON.stringify(defaults[key])) diff[key] = value;
|
|
}
|
|
return diff;
|
|
}
|
|
|
|
export function portable(workspace, defaults, { includeMessages = true } = {}) {
|
|
return {
|
|
v: FORMAT,
|
|
name: workspace.name,
|
|
scripts: workspace.scripts.map(({ name, source }) => ({ name, source })),
|
|
messages: includeMessages ? workspace.messages.map(({ name, source }) => ({ name, source })) : [],
|
|
settings: settingsDiff(workspace.settings, defaults),
|
|
};
|
|
}
|
|
|
|
export async function encodeShareUrl(data) {
|
|
const json = new TextEncoder().encode(JSON.stringify(data));
|
|
const packed = await pipe(json, new CompressionStream("deflate-raw"));
|
|
const url = new URL(location.href);
|
|
url.search = "";
|
|
url.hash = `${SHARE_PARAM}=${toBase64Url(packed)}`;
|
|
return url.href;
|
|
}
|
|
|
|
export function hasSharedWorkspace() {
|
|
return new URLSearchParams(location.hash.slice(1)).has(SHARE_PARAM);
|
|
}
|
|
|
|
export async function decodeSharedWorkspace() {
|
|
const value = new URLSearchParams(location.hash.slice(1)).get(SHARE_PARAM);
|
|
if (!value) return null;
|
|
const json = await pipe(fromBase64Url(value), new DecompressionStream("deflate-raw"));
|
|
return validatePortable(JSON.parse(new TextDecoder().decode(json)));
|
|
}
|
|
|
|
export function clearShareHash() {
|
|
history.replaceState(null, "", location.pathname + location.search);
|
|
}
|
|
|
|
export function validatePortable(data) {
|
|
if (!data || typeof data !== "object" || !Array.isArray(data.scripts)) {
|
|
throw new Error("This is not a Sieve playground workspace.");
|
|
}
|
|
const clean = (items) =>
|
|
(Array.isArray(items) ? items : [])
|
|
.filter((item) => item && typeof item.source === "string")
|
|
.map((item, index) => ({ name: String(item.name || `item-${index + 1}`).slice(0, 80), source: item.source }));
|
|
const scripts = clean(data.scripts);
|
|
if (scripts.length === 0) throw new Error("The workspace has no scripts.");
|
|
return {
|
|
name: String(data.name || "Shared workspace").slice(0, 80),
|
|
scripts,
|
|
messages: clean(data.messages),
|
|
settings: data.settings && typeof data.settings === "object" ? data.settings : {},
|
|
};
|
|
}
|