Files
jcoffey-dev 136fc2540b
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 7m45s
Sieve playground: the server's interpreter in the browser
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.
2026-10-09 01:13:53 -07:00

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 : {},
};
}