The Debug button on a Sieve script sent a copy to an outside site, whose interpreter knows only upstream's extension names: a script that requires vnd.inbuxa.expressions or vnd.inbuxa.while failed to compile there, and the button took admins to another project's site. The playground now ships inside the console and runs the server's own interpreter. sieve-playground/ is built by inbuxa-server's tools/sieve-playground (SOURCE.md names the commit); sieve-playground.ts copies it, with Monaco 0.52.2 from node_modules, into sieve-playground/<content hash>/ (the server caches every file but the console's index.html for good) and gives the console its path. The script stays on the console's origin, so the warning dialog before opening it is gone. Strings (en.json): sievepad.failed reworded, sievepad.debugHint added, the three warning-dialog strings removed. webui.zip grows from 1.3 MB to 3.4 MB: 0.9 MB of compressed WebAssembly, the rest Monaco's editor core.
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 : {},
|
|
};
|
|
}
|