Checkpoint web parity through Chromium input tasks
This commit is contained in:
47
web/app/src/workers/glb-desktop-import-test.worker.ts
Normal file
47
web/app/src/workers/glb-desktop-import-test.worker.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { compareGLBDesktopFixtureSemantics, importGLBDesktopFixtureSemantics } from "../../../protocol/glb-import";
|
||||
|
||||
interface FixtureInput {
|
||||
id: string;
|
||||
bytes: ArrayBuffer;
|
||||
sourceSha256: string;
|
||||
expected: unknown;
|
||||
}
|
||||
|
||||
const scope = self as unknown as {
|
||||
onmessage: ((event: MessageEvent<{ fixtures: FixtureInput[] }>) => void) | null;
|
||||
postMessage(message: unknown): void;
|
||||
};
|
||||
|
||||
async function sha256(bytes: ArrayBuffer): Promise<string> {
|
||||
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
||||
return Array.from(digest, (value) => value.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
scope.onmessage = async (event) => {
|
||||
try {
|
||||
const results = [];
|
||||
for (const fixture of event.data.fixtures) {
|
||||
const sourceSha256 = await sha256(fixture.bytes);
|
||||
if (sourceSha256 !== fixture.sourceSha256) throw new Error(`${fixture.id} source SHA-256 mismatch`);
|
||||
const imported = importGLBDesktopFixtureSemantics(fixture.bytes);
|
||||
const comparison = compareGLBDesktopFixtureSemantics(fixture.expected, imported);
|
||||
results.push({
|
||||
id: fixture.id,
|
||||
sourceSha256,
|
||||
compatible: comparison.compatible,
|
||||
mismatches: comparison.mismatches,
|
||||
topology: imported.meshes.reduce((sum, mesh) => sum + mesh.primitives.length, 0),
|
||||
attributes: [...new Set(imported.meshes.flatMap((mesh) => mesh.primitives.flatMap((primitive) => Object.keys(primitive.attributes))))].sort(),
|
||||
materials: imported.materials.length,
|
||||
nodes: imported.nodes.length,
|
||||
animations: imported.animations.length,
|
||||
});
|
||||
}
|
||||
scope.postMessage({ ok: true, results });
|
||||
}
|
||||
catch (error) {
|
||||
scope.postMessage({ ok: false, error: error instanceof Error ? error.message : "GLB desktop import failed" });
|
||||
}
|
||||
};
|
||||
|
||||
export {};
|
||||
33
web/app/src/workers/glb-loss-report-test.worker.ts
Normal file
33
web/app/src/workers/glb-loss-report-test.worker.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { exportGLB } from "../../../protocol/glb-export";
|
||||
import { createGLBLossReport } from "../../../protocol/glb-loss-report";
|
||||
import type { GLBAssetBuffer } from "../../../protocol/glb-export";
|
||||
import type { MeshGeometryBuffer } from "../../../protocol/web-engine";
|
||||
import type { NonMeshGeometryChunk } from "../../../protocol/nonmesh-binary";
|
||||
import type { SceneSnapshotIR } from "../../../protocol/scene-ir";
|
||||
|
||||
interface Request {
|
||||
snapshot: SceneSnapshotIR;
|
||||
geometryBuffers: MeshGeometryBuffer[];
|
||||
assetBuffers: GLBAssetBuffer[];
|
||||
nonMeshGeometryBuffers: NonMeshGeometryChunk[];
|
||||
}
|
||||
|
||||
const scope = self as unknown as {
|
||||
onmessage: ((event: MessageEvent<Request>) => void) | null;
|
||||
postMessage(message: unknown, transfer?: Transferable[]): void;
|
||||
};
|
||||
|
||||
scope.onmessage = (event) => {
|
||||
try {
|
||||
const request = event.data;
|
||||
const exported = exportGLB(request.snapshot, request.geometryBuffers, request.assetBuffers, request.nonMeshGeometryBuffers);
|
||||
const report = createGLBLossReport(request.snapshot, exported.report);
|
||||
const output = exported.glb ?? null;
|
||||
scope.postMessage({ ok: true, report, output }, output ? [output] : []);
|
||||
}
|
||||
catch (error) {
|
||||
scope.postMessage({ ok: false, error: error instanceof Error ? error.message : "GLB loss report failed" });
|
||||
}
|
||||
};
|
||||
|
||||
export {};
|
||||
26
web/app/src/workers/glb-negative-cases-test.worker.ts
Normal file
26
web/app/src/workers/glb-negative-cases-test.worker.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { importGLBSemantics } from "../../../protocol/glb-import";
|
||||
|
||||
const scope = self as unknown as {
|
||||
onmessage: ((event: MessageEvent<{ cases: Array<{ id: string; bytes: ArrayBuffer }> }>) => void) | null;
|
||||
postMessage(message: unknown): void;
|
||||
};
|
||||
|
||||
scope.onmessage = (event) => {
|
||||
try {
|
||||
const results = event.data.cases.map((candidate) => {
|
||||
try {
|
||||
importGLBSemantics(candidate.bytes);
|
||||
return { id: candidate.id, code: "ACCEPTED" };
|
||||
}
|
||||
catch (error) {
|
||||
return { id: candidate.id, code: error instanceof Error ? error.message.split(":", 1)[0] : "UNKNOWN" };
|
||||
}
|
||||
});
|
||||
scope.postMessage({ ok: true, results });
|
||||
}
|
||||
catch (error) {
|
||||
scope.postMessage({ ok: false, error: error instanceof Error ? error.message : "GLB negative worker failed" });
|
||||
}
|
||||
};
|
||||
|
||||
export {};
|
||||
106
web/app/src/workers/glb-recovery-test.worker.ts
Normal file
106
web/app/src/workers/glb-recovery-test.worker.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { exportGLB, type GLBAssetBuffer } from "../../../protocol/glb-export";
|
||||
import { importGLBSemantics } from "../../../protocol/glb-import";
|
||||
import {
|
||||
beginGLBRecoveryOperation,
|
||||
cancelGLBRecoveryOperation,
|
||||
commitGLBRecoveryOperation,
|
||||
type GLBRecoveryOperation,
|
||||
type GLBRecoveryReceipt,
|
||||
} from "../../../protocol/glb-recovery";
|
||||
import type { MeshGeometryBuffer } from "../../../protocol/web-engine";
|
||||
import type { NonMeshGeometryChunk } from "../../../protocol/nonmesh-binary";
|
||||
import type { SceneSnapshotIR } from "../../../protocol/scene-ir";
|
||||
|
||||
interface RunCommand {
|
||||
type: "run";
|
||||
requestId: string;
|
||||
operation: GLBRecoveryOperation;
|
||||
bytes: ArrayBuffer;
|
||||
snapshot?: SceneSnapshotIR;
|
||||
geometryBuffers?: MeshGeometryBuffer[];
|
||||
assetBuffers?: GLBAssetBuffer[];
|
||||
nonMeshGeometryBuffers?: NonMeshGeometryChunk[];
|
||||
baseRevision?: number;
|
||||
workerGeneration?: number;
|
||||
}
|
||||
|
||||
interface CancelCommand { type: "cancel"; targetRequestId: string; }
|
||||
|
||||
const cancelled = new Set<string>();
|
||||
const running = new Map<string, GLBRecoveryReceipt>();
|
||||
const scope = self as unknown as {
|
||||
onmessage: ((event: MessageEvent<RunCommand | CancelCommand>) => void) | null;
|
||||
postMessage(message: unknown, transfer?: Transferable[]): void;
|
||||
};
|
||||
|
||||
async function sha256Hex(value: ArrayBuffer | string): Promise<string> {
|
||||
const bytes = typeof value === "string" ? new TextEncoder().encode(value) : new Uint8Array(value);
|
||||
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
||||
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
function yieldControl(): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, 2));
|
||||
}
|
||||
|
||||
async function checkCancelled(request: RunCommand, receipt: GLBRecoveryReceipt): Promise<GLBRecoveryReceipt | undefined> {
|
||||
await yieldControl();
|
||||
if (!cancelled.has(request.requestId)) return undefined;
|
||||
running.delete(request.requestId);
|
||||
const final = cancelGLBRecoveryOperation(receipt);
|
||||
scope.postMessage({ requestId: request.requestId, ok: true, receipt: final, result: null });
|
||||
return final;
|
||||
}
|
||||
|
||||
async function run(request: RunCommand): Promise<void> {
|
||||
const inputSha256 = await sha256Hex(request.bytes);
|
||||
let receipt = beginGLBRecoveryOperation({
|
||||
operationId: request.requestId.replace(/^glb-/, "").slice(0, 96),
|
||||
operation: request.operation,
|
||||
workerGeneration: request.workerGeneration ?? 1,
|
||||
baseRevision: request.baseRevision ?? 0,
|
||||
inputBytes: request.bytes.byteLength,
|
||||
inputSha256,
|
||||
});
|
||||
running.set(request.requestId, receipt);
|
||||
try {
|
||||
for (let index = 0; index < 4; index++) {
|
||||
const cancelledReceipt = await checkCancelled(request, receipt);
|
||||
if (cancelledReceipt) return;
|
||||
}
|
||||
let output: ArrayBuffer;
|
||||
let outputBytes: number;
|
||||
if (request.operation === "IMPORT") {
|
||||
const semantics = importGLBSemantics(request.bytes);
|
||||
output = new TextEncoder().encode(JSON.stringify(semantics)).buffer;
|
||||
outputBytes = output.byteLength;
|
||||
}
|
||||
else {
|
||||
if (!request.snapshot) throw new Error("GLB_RECOVERY_INVALID: export snapshot missing");
|
||||
const exported = exportGLB(request.snapshot, request.geometryBuffers ?? [], request.assetBuffers ?? [], request.nonMeshGeometryBuffers ?? []);
|
||||
if (!exported.glb) throw new Error("GLB_EXPORT_BLOCKED: export produced no output");
|
||||
output = exported.glb;
|
||||
outputBytes = output.byteLength;
|
||||
}
|
||||
const cancelledReceipt = await checkCancelled(request, receipt);
|
||||
if (cancelledReceipt) return;
|
||||
const outputSha256 = await sha256Hex(output);
|
||||
receipt = commitGLBRecoveryOperation(receipt, { bytes: outputBytes, sha256: outputSha256 });
|
||||
running.delete(request.requestId);
|
||||
cancelled.delete(request.requestId);
|
||||
scope.postMessage({ requestId: request.requestId, ok: true, receipt, result: { output, outputSha256 } }, [output]);
|
||||
}
|
||||
catch (error) {
|
||||
running.delete(request.requestId);
|
||||
cancelled.delete(request.requestId);
|
||||
scope.postMessage({ requestId: request.requestId, ok: false, error: error instanceof Error ? error.message : "GLB recovery operation failed" });
|
||||
}
|
||||
}
|
||||
|
||||
scope.onmessage = (event: MessageEvent<RunCommand | CancelCommand>) => {
|
||||
if (event.data.type === "cancel") {
|
||||
cancelled.add(event.data.targetRequestId);
|
||||
return;
|
||||
}
|
||||
void run(event.data);
|
||||
};
|
||||
62
web/app/src/workers/io-format-recovery-test.worker.ts
Normal file
62
web/app/src/workers/io-format-recovery-test.worker.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { importOBJ, serializeOBJ } from "../../../protocol/obj-import";
|
||||
import { exportBinarySTL } from "../../../protocol/stl-export";
|
||||
import { importSTL } from "../../../protocol/stl-import";
|
||||
import { importPLY, serializePLYAscii } from "../../../protocol/ply-import";
|
||||
import { beginIOFormatRecoveryOperation, blockIOFormatRecoveryOperation, cancelIOFormatRecoveryOperation, commitIOFormatRecoveryOperation, type IOFormat, type IOFormatRecoveryReceipt } from "../../../protocol/io-format-recovery";
|
||||
|
||||
interface RunCommand { type: "run"; requestId: string; format: IOFormat; operation: "IMPORT" | "EXPORT"; bytes: ArrayBuffer; workerGeneration?: number; baseRevision?: number; }
|
||||
interface CancelCommand { type: "cancel"; targetRequestId: string; }
|
||||
const cancelled = new Set<string>();
|
||||
const running = new Map<string, IOFormatRecoveryReceipt>();
|
||||
const scope = self as unknown as { onmessage: ((event: MessageEvent<RunCommand | CancelCommand>) => void) | null; postMessage(message: unknown, transfer?: Transferable[]): void };
|
||||
|
||||
async function sha256Hex(bytes: ArrayBuffer): Promise<string> {
|
||||
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
||||
return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
const pause = () => new Promise<void>((resolve) => setTimeout(resolve, 2));
|
||||
|
||||
async function checkCancel(request: RunCommand, receipt: IOFormatRecoveryReceipt): Promise<boolean> {
|
||||
await pause();
|
||||
if (!cancelled.has(request.requestId)) return false;
|
||||
running.delete(request.requestId);
|
||||
scope.postMessage({ requestId: request.requestId, ok: true, receipt: cancelIOFormatRecoveryOperation(receipt), result: null });
|
||||
return true;
|
||||
}
|
||||
|
||||
function outputFor(request: RunCommand): ArrayBuffer {
|
||||
if (request.format === "OBJ") {
|
||||
const document = importOBJ(request.bytes);
|
||||
const serialized = serializeOBJ(document);
|
||||
return new TextEncoder().encode(serialized.obj + serialized.mtl).buffer;
|
||||
}
|
||||
if (request.format === "STL") return exportBinarySTL(importSTL(request.bytes, { variant: "STL_BINARY", unitScale: 1 }));
|
||||
return serializePLYAscii(importPLY(request.bytes, { format: "ascii" }));
|
||||
}
|
||||
|
||||
async function run(request: RunCommand): Promise<void> {
|
||||
const inputSha256 = await sha256Hex(request.bytes);
|
||||
let receipt = beginIOFormatRecoveryOperation({ operationId: request.requestId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 96), format: request.format, operation: request.operation, workerGeneration: request.workerGeneration ?? 1, baseRevision: request.baseRevision ?? 0, inputBytes: request.bytes.byteLength, inputSha256 });
|
||||
running.set(request.requestId, receipt);
|
||||
try {
|
||||
for (let index = 0; index < 4; index++) if (await checkCancel(request, receipt)) return;
|
||||
if (request.bytes.byteLength > 512 * 1024) {
|
||||
running.delete(request.requestId); scope.postMessage({ requestId: request.requestId, ok: true, receipt: blockIOFormatRecoveryOperation(receipt), result: null }); return;
|
||||
}
|
||||
const output = outputFor(request);
|
||||
if (await checkCancel(request, receipt)) return;
|
||||
const outputSha256 = await sha256Hex(output);
|
||||
receipt = commitIOFormatRecoveryOperation(receipt, { bytes: output.byteLength, sha256: outputSha256 });
|
||||
running.delete(request.requestId); cancelled.delete(request.requestId);
|
||||
scope.postMessage({ requestId: request.requestId, ok: true, receipt, result: { output, outputSha256 } }, [output]);
|
||||
}
|
||||
catch (error) {
|
||||
running.delete(request.requestId); cancelled.delete(request.requestId);
|
||||
if (error instanceof Error && /BUDGET_EXCEEDED|TRUNCATED|OUT_OF_RANGE/.test(error.message)) scope.postMessage({ requestId: request.requestId, ok: true, receipt: blockIOFormatRecoveryOperation(receipt), result: null });
|
||||
else scope.postMessage({ requestId: request.requestId, ok: false, error: error instanceof Error ? error.message : "IO format recovery operation failed" });
|
||||
}
|
||||
}
|
||||
|
||||
scope.onmessage = (event) => { if (event.data.type === "cancel") cancelled.add(event.data.targetRequestId); else void run(event.data); };
|
||||
|
||||
export {};
|
||||
26
web/app/src/workers/obj-roundtrip-test.worker.ts
Normal file
26
web/app/src/workers/obj-roundtrip-test.worker.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createOBJLossReport, importOBJ, serializeOBJ } from "../../../protocol/obj-import";
|
||||
|
||||
interface Request {
|
||||
obj: ArrayBuffer;
|
||||
mtl?: ArrayBuffer;
|
||||
textureAssets?: string[];
|
||||
}
|
||||
|
||||
const scope = self as unknown as {
|
||||
onmessage: ((event: MessageEvent<Request>) => void) | null;
|
||||
postMessage(message: unknown): void;
|
||||
};
|
||||
|
||||
scope.onmessage = (event) => {
|
||||
try {
|
||||
const imported = importOBJ(event.data.obj, event.data.mtl);
|
||||
const lossReport = createOBJLossReport(imported, event.data.textureAssets ?? []);
|
||||
const serialized = serializeOBJ(imported);
|
||||
scope.postMessage({ ok: true, imported, lossReport, obj: serialized.obj, mtl: serialized.mtl });
|
||||
}
|
||||
catch (error) {
|
||||
scope.postMessage({ ok: false, error: error instanceof Error ? error.message : "OBJ round-trip failed" });
|
||||
}
|
||||
};
|
||||
|
||||
export {};
|
||||
22
web/app/src/workers/ply-roundtrip-test.worker.ts
Normal file
22
web/app/src/workers/ply-roundtrip-test.worker.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createPLYLossReport, importPLY, serializePLYAscii } from "../../../protocol/ply-import";
|
||||
|
||||
interface Request { bytes: ArrayBuffer; format?: "ascii" | "binary_little_endian"; }
|
||||
|
||||
const scope = self as unknown as {
|
||||
onmessage: ((event: MessageEvent<Request>) => void) | null;
|
||||
postMessage(message: unknown, transfer?: Transferable[]): void;
|
||||
};
|
||||
|
||||
scope.onmessage = (event) => {
|
||||
try {
|
||||
const imported = importPLY(event.data.bytes, { format: event.data.format });
|
||||
const lossReport = createPLYLossReport(imported);
|
||||
const output = serializePLYAscii(imported);
|
||||
scope.postMessage({ ok: true, imported, lossReport, output }, [output]);
|
||||
}
|
||||
catch (error) {
|
||||
scope.postMessage({ ok: false, error: error instanceof Error ? error.message : "PLY round-trip failed" });
|
||||
}
|
||||
};
|
||||
|
||||
export {};
|
||||
25
web/app/src/workers/script-host-call-test.worker.ts
Normal file
25
web/app/src/workers/script-host-call-test.worker.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { parseScriptHostCall, SCRIPT_PERMISSIONS } from "../../../protocol/scripting-platform";
|
||||
|
||||
const digest = "a".repeat(64);
|
||||
const permissions = new Set(SCRIPT_PERMISSIONS);
|
||||
const call = (name: string, parameters: Record<string, unknown>) => ({ schemaVersion: 1, requestId: `host:${name.toLowerCase()}`, scriptId: "clean", call: name, permission: name, parameters });
|
||||
self.onmessage = () => {
|
||||
const accepted = [
|
||||
parseScriptHostCall(call("READ_MAIN", { revision: 3 }), permissions),
|
||||
parseScriptHostCall(call("READ_ASSET", { path: "//assets/model.bin", expectedSha256: digest }), permissions),
|
||||
parseScriptHostCall(call("WRITE_MAIN", { revision: 3, operation: "object.transform", payload: { objectId: "obj:1", x: 1 } }), permissions),
|
||||
parseScriptHostCall(call("WRITE_ASSET", { path: "assets/out.bin", byteLength: 4, sha256: digest }), permissions),
|
||||
parseScriptHostCall(call("SUBMIT_SERVER_JOB", { inputBlendSha256: digest, settingsSha256: digest }), permissions),
|
||||
];
|
||||
const blocked: Record<string, string> = {};
|
||||
for (const [name, input] of [
|
||||
["unknown", call("EXECUTE", {})],
|
||||
["permission", { ...call("READ_MAIN", { revision: 3 }), permission: "WRITE_MAIN" }],
|
||||
["fields", call("READ_MAIN", { revision: 3, extra: true })],
|
||||
["path", call("READ_ASSET", { path: "../escape", expectedSha256: digest })],
|
||||
] as const) {
|
||||
try { parseScriptHostCall(input, permissions); blocked[name] = "ACCEPTED"; }
|
||||
catch (error) { blocked[name] = error instanceof Error ? error.message.split(":", 1)[0] : String(error); }
|
||||
}
|
||||
self.postMessage({ accepted: accepted.map((item) => ({ call: item.call, permission: item.permission, execution: item.execution, parameters: item.parameters })), blocked });
|
||||
};
|
||||
17
web/app/src/workers/script-permission-policy-test.worker.ts
Normal file
17
web/app/src/workers/script-permission-policy-test.worker.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { parseScriptingManifest, resolveScriptPermissions } from "../../../protocol/scripting-platform";
|
||||
|
||||
const script = (permissions: string[]) => ({ id: "clean", name: "clean", entryPath: "scripts/clean.py", sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "Team", signature: "b".repeat(128), keyId: "key:new", permissions, dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false });
|
||||
const manifest = (permissions: string[]) => ({ schemaVersion: 1, scripts: [script(permissions)] });
|
||||
const decision = (permissions: string[], requested: unknown = []) => resolveScriptPermissions(manifest(permissions), "clean", requested);
|
||||
self.onmessage = () => {
|
||||
let unknownDeclaration = "ACCEPTED";
|
||||
try { parseScriptingManifest(manifest(["EXECUTE"])); } catch (error) { unknownDeclaration = error instanceof Error ? error.message.split(":", 1)[0] : String(error); }
|
||||
self.postMessage({
|
||||
defaultGrant: decision(["READ_MAIN"]),
|
||||
declaredGrant: decision(["WRITE_ASSET", "READ_MAIN"], ["READ_MAIN"]),
|
||||
escalation: decision(["READ_MAIN"], ["WRITE_MAIN"]),
|
||||
unknownRequest: decision(["READ_MAIN"], ["EXECUTE"]),
|
||||
duplicateRequest: decision(["READ_MAIN"], ["READ_MAIN", "READ_MAIN"]),
|
||||
unknownDeclaration,
|
||||
});
|
||||
};
|
||||
12
web/app/src/workers/script-sandbox-budget-test.worker.ts
Normal file
12
web/app/src/workers/script-sandbox-budget-test.worker.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { parseScriptSandboxBudget, SCRIPT_SANDBOX_BUDGET } from "../../../protocol/scripting-platform";
|
||||
|
||||
const budget = { schemaVersion: 1, cpuMs: 1000, wallMs: 5000, memoryBytes: 1024 * 1024, maxMessageBytes: 4096, maxOutputBytes: 8192 } as const;
|
||||
self.onmessage = () => {
|
||||
const accepted = parseScriptSandboxBudget(budget);
|
||||
const blocked: Record<string, string> = {};
|
||||
for (const [field, limit] of Object.entries({ cpuMs: SCRIPT_SANDBOX_BUDGET.maxCpuMs, wallMs: SCRIPT_SANDBOX_BUDGET.maxWallMs, memoryBytes: SCRIPT_SANDBOX_BUDGET.maxMemoryBytes, maxMessageBytes: SCRIPT_SANDBOX_BUDGET.maxMessageBytes, maxOutputBytes: SCRIPT_SANDBOX_BUDGET.maxOutputBytes })) {
|
||||
try { parseScriptSandboxBudget({ ...budget, [field]: limit + 1 }); blocked[field] = "ACCEPTED"; }
|
||||
catch (error) { blocked[field] = error instanceof Error ? error.message.split(":", 1)[0] : String(error); }
|
||||
}
|
||||
self.postMessage({ accepted, blocked, execution: "DISABLED" });
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { rejectLateScriptSandboxResult, terminateScriptSandboxJob } from "../../../protocol/scripting-platform";
|
||||
|
||||
interface SandboxRequest { mode: "RECEIPT" | "RUN" }
|
||||
const running = { schemaVersion: 1, jobId: "sandbox:cancel", workerGeneration: 5, baseRevision: 11, mainRevisionBefore: 11, status: "RUNNING" as const };
|
||||
|
||||
self.onmessage = (event: MessageEvent<SandboxRequest>) => {
|
||||
if (event.data?.mode === "RECEIPT") {
|
||||
const cancelled = terminateScriptSandboxJob(running, "CANCEL");
|
||||
let lateResult = "ACCEPTED";
|
||||
try { rejectLateScriptSandboxResult(cancelled); } catch (error) { lateResult = error instanceof Error ? error.message.split(":", 1)[0] : String(error); }
|
||||
self.postMessage({ type: "receipt", cancelled, lateResult });
|
||||
return;
|
||||
}
|
||||
if (event.data?.mode === "RUN") {
|
||||
setTimeout(() => self.postMessage({ type: "late-result", jobId: running.jobId, workerGeneration: running.workerGeneration, cacheKey: "sandbox-cache:cancel", payload: { revision: running.mainRevisionBefore + 1 } }), 50);
|
||||
}
|
||||
};
|
||||
52
web/app/src/workers/script-sandbox-dispose-test.worker.ts
Normal file
52
web/app/src/workers/script-sandbox-dispose-test.worker.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { createScriptSandboxDisposeReceipt } from "../testing/script-sandbox-dispose";
|
||||
|
||||
let ports: { primary: MessagePort; peer: MessagePort } | null = null;
|
||||
let timer: number | undefined;
|
||||
let controller: AbortController | null = null;
|
||||
let buffer: ArrayBuffer | null = null;
|
||||
let pendingRequests = 0;
|
||||
let cacheReferences = 0;
|
||||
let disposeCount = 0;
|
||||
let disposed = false;
|
||||
|
||||
const activeResources = () => ({
|
||||
messagePorts: ports === null ? 0 : 2,
|
||||
timers: timer === undefined ? 0 : 1,
|
||||
abortControllers: controller === null ? 0 : 1,
|
||||
transferableBuffers: buffer === null ? 0 : 1,
|
||||
pendingRequests,
|
||||
cacheReferences,
|
||||
});
|
||||
|
||||
self.onmessage = (event: MessageEvent<{ type: "init" | "dispose" }>) => {
|
||||
if (event.data?.type === "init") {
|
||||
if (disposed || ports !== null) return;
|
||||
const channel = new MessageChannel();
|
||||
ports = { primary: channel.port1, peer: channel.port2 };
|
||||
ports.primary.start();
|
||||
ports.peer.start();
|
||||
controller = new AbortController();
|
||||
buffer = new ArrayBuffer(64);
|
||||
pendingRequests = 1;
|
||||
cacheReferences = 1;
|
||||
timer = setTimeout(() => self.postMessage({ type: "late-timer" }), 50);
|
||||
self.postMessage({ type: "ready", resources: activeResources() });
|
||||
return;
|
||||
}
|
||||
if (event.data?.type === "dispose") {
|
||||
disposeCount += 1;
|
||||
if (!disposed) {
|
||||
if (timer !== undefined) { clearTimeout(timer); timer = undefined; }
|
||||
controller?.abort();
|
||||
controller = null;
|
||||
ports?.primary.close();
|
||||
ports?.peer.close();
|
||||
ports = null;
|
||||
buffer = null;
|
||||
pendingRequests = 0;
|
||||
cacheReferences = 0;
|
||||
disposed = true;
|
||||
}
|
||||
self.postMessage({ type: "disposed", receipt: createScriptSandboxDisposeReceipt(disposeCount), resources: activeResources() });
|
||||
}
|
||||
};
|
||||
24
web/app/src/workers/script-sandbox-isolation-test.worker.ts
Normal file
24
web/app/src/workers/script-sandbox-isolation-test.worker.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
interface SandboxRequest { mode: "RECEIPTS" | "CRASH" | "TIMEOUT" }
|
||||
import { rejectLateScriptSandboxResult, terminateScriptSandboxJob } from "../../../protocol/scripting-platform";
|
||||
|
||||
const running = { schemaVersion: 1, jobId: "sandbox:1", workerGeneration: 4, baseRevision: 9, mainRevisionBefore: 9, status: "RUNNING" as const };
|
||||
|
||||
self.onmessage = (event: MessageEvent<SandboxRequest>) => {
|
||||
if (event.data?.mode === "RECEIPTS") {
|
||||
const crash = terminateScriptSandboxJob(running, "CRASH");
|
||||
const timeout = terminateScriptSandboxJob(running, "TIMEOUT");
|
||||
const cancel = terminateScriptSandboxJob(running, "CANCEL");
|
||||
let lateResult = "ACCEPTED";
|
||||
try { rejectLateScriptSandboxResult(cancel); } catch (error) { lateResult = error instanceof Error ? error.message.split(":", 1)[0] : String(error); }
|
||||
self.postMessage({ crash, timeout, cancel, lateResult });
|
||||
return;
|
||||
}
|
||||
if (event.data?.mode === "CRASH") {
|
||||
// The host must convert this Worker error into a terminated job receipt.
|
||||
throw new Error("sandbox crash fixture");
|
||||
}
|
||||
if (event.data?.mode === "TIMEOUT") {
|
||||
// A real sandbox would be stopped by its wall-time supervisor before this publishes.
|
||||
setTimeout(() => self.postMessage({ type: "late-result", jobId: "sandbox:timeout" }), 50);
|
||||
}
|
||||
};
|
||||
39
web/app/src/workers/script-sandbox-recovery-test.worker.ts
Normal file
39
web/app/src/workers/script-sandbox-recovery-test.worker.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { appendScriptExecutionAudit, createScriptExecutionAudit, parseScriptExecutionAuditLog, parseScriptingManifest } from "../../../protocol/scripting-platform";
|
||||
import { createScriptSandboxRecoveryReceipt } from "../testing/script-sandbox-recovery";
|
||||
|
||||
const sourceSha256 = "a".repeat(64);
|
||||
const signature = "b".repeat(128);
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
scripts: [{ id: "script:recovery", name: "Recovery", entryPath: "scripts/recovery.py", sourceByteLength: 128, sourceSha256, publisher: "Team", signature, keyId: "key:trusted", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false }],
|
||||
};
|
||||
|
||||
self.onmessage = async (event: MessageEvent<{ type: "recover" }>) => {
|
||||
if (event.data?.type !== "recover") return;
|
||||
const parsed = parseScriptingManifest(manifest);
|
||||
const firstAudit = await createScriptExecutionAudit(parsed, "script:recovery", new Set(["key:trusted"]), { requestId: "sandbox-recovery:g4", requestedAt: "2026-08-19T00:00:00.000Z" });
|
||||
const firstLog = await appendScriptExecutionAudit({ schemaVersion: 1, entries: [] }, firstAudit);
|
||||
const secondAudit = await createScriptExecutionAudit(parsed, "script:recovery", new Set(["key:trusted"]), { requestId: "sandbox-recovery:g5", requestedAt: "2026-08-19T00:00:01.000Z" });
|
||||
const auditLog = await appendScriptExecutionAudit(firstLog, secondAudit);
|
||||
const checked = await parseScriptExecutionAuditLog(auditLog);
|
||||
const first = checked.entries[0];
|
||||
const second = checked.entries[1];
|
||||
const receipt = createScriptSandboxRecoveryReceipt({
|
||||
previousGeneration: 4,
|
||||
nextGeneration: 5,
|
||||
mainRevisionBefore: 11,
|
||||
mainRevisionAfter: 11,
|
||||
sourceSha256,
|
||||
manifestSha256: first.audit.manifestSha256,
|
||||
audit: {
|
||||
entries: 2,
|
||||
first: { sequence: 1, requestId: first.audit.requestId, previousEntrySha256: first.previousEntrySha256, entrySha256: first.entrySha256, sourceSha256: first.audit.sourceSha256, manifestSha256: first.audit.manifestSha256 },
|
||||
second: { sequence: 2, requestId: second.audit.requestId, previousEntrySha256: second.previousEntrySha256, entrySha256: second.entrySha256, sourceSha256: second.audit.sourceSha256, manifestSha256: second.audit.manifestSha256 },
|
||||
},
|
||||
});
|
||||
let replayError = "ACCEPTED";
|
||||
try { await appendScriptExecutionAudit(auditLog, secondAudit); } catch (error) { replayError = error instanceof Error ? error.message.split(":", 1)[0] : String(error); }
|
||||
let tamperError = "ACCEPTED";
|
||||
try { await parseScriptExecutionAuditLog({ ...auditLog, entries: auditLog.entries.map((entry, index) => index === 0 ? { ...entry, audit: { ...entry.audit, sourceSha256: "c".repeat(64) } } : entry) }); } catch (error) { tamperError = error instanceof Error ? error.message.split(":", 1)[0] : String(error); }
|
||||
self.postMessage({ receipt, auditLog, replayError, tamperError, manifestSha256: first.audit.manifestSha256, scriptSourceSha256: first.audit.sourceSha256 });
|
||||
};
|
||||
12
web/app/src/workers/script-sandbox-scope-test.worker.ts
Normal file
12
web/app/src/workers/script-sandbox-scope-test.worker.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { parseScriptSandboxScope } from "../../../protocol/scripting-platform";
|
||||
|
||||
const deniedScope = { schemaVersion: 1, dom: false, hostWorker: false, opfs: false, indexedDB: false, network: false } as const;
|
||||
self.onmessage = () => {
|
||||
const accepted = parseScriptSandboxScope(deniedScope);
|
||||
const blocked: Record<string, string> = {};
|
||||
for (const capability of ["dom", "hostWorker", "opfs", "indexedDB", "network"] as const) {
|
||||
try { parseScriptSandboxScope({ ...deniedScope, [capability]: true }); blocked[capability] = "ACCEPTED"; }
|
||||
catch (error) { blocked[capability] = error instanceof Error ? error.message.split(":", 1)[0] : String(error); }
|
||||
}
|
||||
self.postMessage({ accepted, blocked, execution: "DISABLED" });
|
||||
};
|
||||
17
web/app/src/workers/script-signature-negative-test.worker.ts
Normal file
17
web/app/src/workers/script-signature-negative-test.worker.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { verifyScriptManifestSignature } from "../../../protocol/scripting-platform";
|
||||
|
||||
const publicKey = "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8";
|
||||
const signature = "fc396c6c68e6f6eb38a18c147becfaec1621a167f6db0a0d76874209accf3cb80dfa1fac1528ebc1bc6b090801a3ad397cae18e6ddb41740766678711c0a8804";
|
||||
const script = (id = "clean", overrides: Record<string, unknown> = {}) => ({ id, name: id, entryPath: `scripts/${id}.py`, sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "Team", signature, keyId: "key:new", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false, ...overrides });
|
||||
const manifest = (scripts = [script()]) => ({ schemaVersion: 1, scripts });
|
||||
const key = (overrides: Record<string, unknown> = {}) => ({ keyId: "key:new", publisher: "Team", algorithm: "ED25519", publicKey, status: "ACTIVE", notBefore: "2026-01-01T00:00:00.000Z", notAfter: "2027-01-01T00:00:00.000Z", ...overrides });
|
||||
const policy = (overrides: Record<string, unknown> = {}) => ({ schemaVersion: 1, issuer: "web-trust", issuedAt: "2026-01-01T00:00:00.000Z", expiresAt: "2027-01-01T00:00:00.000Z", maxClockSkewMs: 300000, keys: [key()], ...overrides });
|
||||
self.onmessage = async () => {
|
||||
const at = "2026-08-18T12:00:00.000Z";
|
||||
const missing = await verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [] }), at);
|
||||
const expired = await verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [key({ notAfter: "2026-06-01T00:00:00.000Z" })] }), at);
|
||||
const notYetValid = await verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [key({ notBefore: "2026-09-01T00:00:00.000Z" })] }), at);
|
||||
const publisherMismatch = await verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [key({ publisher: "Other" })] }), at);
|
||||
const swapped = await verifyScriptManifestSignature(manifest([script("other")]), "other", policy(), at);
|
||||
self.postMessage({ missing, expired, notYetValid, publisherMismatch, swapped });
|
||||
};
|
||||
15
web/app/src/workers/script-signature-test.worker.ts
Normal file
15
web/app/src/workers/script-signature-test.worker.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { verifyScriptManifestSignature } from "../../../protocol/scripting-platform";
|
||||
|
||||
const publicKey = "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8";
|
||||
const signature = "fc396c6c68e6f6eb38a18c147becfaec1621a167f6db0a0d76874209accf3cb80dfa1fac1528ebc1bc6b090801a3ad397cae18e6ddb41740766678711c0a8804";
|
||||
const script = (overrides: Record<string, unknown> = {}) => ({ id: "clean", name: "clean", entryPath: "scripts/clean.py", sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "Team", signature, keyId: "key:new", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false, ...overrides });
|
||||
const policy = (overrides: Record<string, unknown> = {}) => ({ schemaVersion: 1, issuer: "web-trust", issuedAt: "2026-01-01T00:00:00.000Z", expiresAt: "2027-01-01T00:00:00.000Z", maxClockSkewMs: 300000, keys: [{ keyId: "key:new", publisher: "Team", algorithm: "ED25519", publicKey, status: "ACTIVE", notBefore: "2026-01-01T00:00:00.000Z", notAfter: "2027-01-01T00:00:00.000Z" }], ...overrides });
|
||||
const manifest = { schemaVersion: 1, scripts: [script()] };
|
||||
|
||||
self.onmessage = async () => {
|
||||
const valid = await verifyScriptManifestSignature(manifest, "clean", policy(), "2026-08-18T12:00:00.000Z");
|
||||
const sourceChanged = await verifyScriptManifestSignature({ schemaVersion: 1, scripts: [script({ sourceSha256: "d".repeat(64) })] }, "clean", policy(), "2026-08-18T12:00:00.000Z");
|
||||
const signatureChanged = await verifyScriptManifestSignature({ schemaVersion: 1, scripts: [script({ signature: `${signature.slice(0, -1)}${signature.endsWith("0") ? "1" : "0"}` })] }, "clean", policy(), "2026-08-18T12:00:00.000Z");
|
||||
const revoked = await verifyScriptManifestSignature(manifest, "clean", policy({ keys: [{ ...policy().keys[0], status: "REVOKED", revokedAt: "2026-06-01T00:00:00.000Z" }] }), "2026-08-18T12:00:00.000Z");
|
||||
self.postMessage({ valid, sourceChanged, signatureChanged, revoked });
|
||||
};
|
||||
19
web/app/src/workers/script-trust-policy-test.worker.ts
Normal file
19
web/app/src/workers/script-trust-policy-test.worker.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { parseScriptTrustPolicy, resolveScriptSigner, serializeScriptTrustPolicy } from "../../../protocol/scripting-platform";
|
||||
|
||||
const digest = "a".repeat(64);
|
||||
const signature = "b".repeat(128);
|
||||
const script = (id = "clean", overrides: Record<string, unknown> = {}) => ({ id, name: id, entryPath: `scripts/${id}.py`, sourceByteLength: 128, sourceSha256: digest, publisher: "Team", signature, keyId: "key:new", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false, ...overrides });
|
||||
const key = (keyId: string, overrides: Record<string, unknown> = {}) => ({ keyId, publisher: "Team", algorithm: "ED25519", publicKey: "c".repeat(64), status: "ACTIVE", notBefore: "2026-01-01T00:00:00.000Z", notAfter: "2027-01-01T00:00:00.000Z", ...overrides });
|
||||
const policy = (keys = [key("key:new")], overrides: Record<string, unknown> = {}) => ({ schemaVersion: 1, issuer: "web-trust", issuedAt: "2026-01-01T00:00:00.000Z", expiresAt: "2027-01-01T00:00:00.000Z", maxClockSkewMs: 300000, keys, ...overrides });
|
||||
const manifest = { schemaVersion: 1, scripts: [script()] };
|
||||
|
||||
function errorCode(value: unknown): string {
|
||||
try { parseScriptTrustPolicy(value); return "ACCEPTED"; }
|
||||
catch (error) { return error instanceof Error ? error.message : String(error); }
|
||||
}
|
||||
|
||||
self.onmessage = () => {
|
||||
const rotated = policy([key("key:new", { replaces: "key:old" }), key("key:old", { status: "REVOKED", revokedAt: "2026-06-01T00:00:00.000Z" })]);
|
||||
const parsed = parseScriptTrustPolicy(rotated);
|
||||
self.postMessage({ eligible: resolveScriptSigner(manifest, "clean", parsed, "2026-08-18T12:00:00.000Z"), revoked: resolveScriptSigner(manifest, "clean", policy([key("key:new", { status: "REVOKED", revokedAt: "2026-06-01T00:00:00.000Z" })]), "2026-08-18T12:00:00.000Z").trust, crossPublisher: errorCode(policy([key("key:new", { replaces: "key:old" }), key("key:old", { publisher: "Other" })])), policyExpired: resolveScriptSigner(manifest, "clean", policy([key("key:new")], { expiresAt: "2026-06-01T00:00:00.000Z" }), "2026-08-18T12:00:00.000Z").trust, rotationSerialized: serializeScriptTrustPolicy(parsed).length });
|
||||
};
|
||||
52
web/app/src/workers/scripting-manifest-budget-test.worker.ts
Normal file
52
web/app/src/workers/scripting-manifest-budget-test.worker.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { parseScriptingManifest, SCRIPTING_BUDGET } from "../../../protocol/scripting-platform";
|
||||
|
||||
const digest = "a".repeat(64);
|
||||
const signature = "b".repeat(128);
|
||||
const script = (id: string, overrides: Record<string, unknown> = {}) => ({
|
||||
id,
|
||||
name: id,
|
||||
entryPath: `scripts/${id}.py`,
|
||||
sourceByteLength: 128,
|
||||
sourceSha256: digest,
|
||||
publisher: "local",
|
||||
signature,
|
||||
keyId: "key:local",
|
||||
permissions: ["READ_MAIN"],
|
||||
dependencies: [],
|
||||
module: false,
|
||||
cpuMs: 1000,
|
||||
memoryBytes: 1024 * 1024,
|
||||
wallMs: 5000,
|
||||
network: false,
|
||||
autorun: false,
|
||||
driverExpressions: false,
|
||||
addonInstall: false,
|
||||
...overrides,
|
||||
});
|
||||
const manifest = (scripts = [script("clean")]) => ({ schemaVersion: 1, scripts });
|
||||
|
||||
function denied(value: unknown): string {
|
||||
try {
|
||||
parseScriptingManifest(value);
|
||||
return "ACCEPTED";
|
||||
}
|
||||
catch (error) {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
|
||||
self.onmessage = () => {
|
||||
const valid = parseScriptingManifest(manifest([
|
||||
script("base", { entryPath: "//scripts/../scripts/base.py" }),
|
||||
script("clean", { dependencies: [{ id: "base", sourceSha256: digest, sourcePath: "//deps/base.py" }] }),
|
||||
]));
|
||||
self.postMessage({
|
||||
valid: [valid.scripts.length, valid.scripts[0].entryPath, valid.scripts[1].dependencies[0].sourcePath, valid.scripts.reduce((total, item) => total + item.sourceByteLength, 0)],
|
||||
count: denied(manifest(Array.from({ length: SCRIPTING_BUDGET.maxScripts + 1 }, (_, index) => script(`script-${index}`)))),
|
||||
totalBytes: denied(manifest([script("large", { sourceByteLength: SCRIPTING_BUDGET.maxSourceBytes }), script("overflow", { sourceByteLength: 1 })])),
|
||||
module: denied(manifest([script("module", { module: true })])),
|
||||
path: denied(manifest([script("escape", { entryPath: "../escape.py" })])),
|
||||
dependency: denied(manifest([script("duplicate", { dependencies: [{ id: "base", sourceSha256: digest, sourcePath: "deps/a.py" }, { id: "base", sourceSha256: digest, sourcePath: "deps/b.py" }] }), script("base")])),
|
||||
permission: denied(manifest([script("unknown", { permissions: ["EXECUTE"] })])),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { canonicalizeScriptingManifest, serializeScriptingManifest } from "../../../protocol/scripting-platform";
|
||||
|
||||
const digest = "a".repeat(64);
|
||||
const signature = "b".repeat(128);
|
||||
const script = (id: string, overrides: Record<string, unknown> = {}) => ({
|
||||
id,
|
||||
name: id,
|
||||
entryPath: `scripts/${id}.py`,
|
||||
sourceByteLength: 128,
|
||||
sourceSha256: digest,
|
||||
publisher: "local",
|
||||
signature,
|
||||
keyId: "key:local",
|
||||
permissions: ["READ_MAIN"],
|
||||
dependencies: [],
|
||||
module: false,
|
||||
cpuMs: 1000,
|
||||
memoryBytes: 1024 * 1024,
|
||||
wallMs: 5000,
|
||||
network: false,
|
||||
autorun: false,
|
||||
driverExpressions: false,
|
||||
addonInstall: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
self.onmessage = () => {
|
||||
const first = {
|
||||
schemaVersion: 1,
|
||||
scripts: [
|
||||
script("zeta", { permissions: ["WRITE_ASSET", "READ_MAIN"], dependencies: [{ id: "alpha", sourceSha256: digest, sourcePath: "deps/alpha.py" }, { id: "beta", sourceSha256: digest, sourcePath: "deps/beta.py" }] }),
|
||||
script("alpha", { permissions: ["SUBMIT_SERVER_JOB", "READ_ASSET"] }),
|
||||
script("beta"),
|
||||
],
|
||||
};
|
||||
const second = {
|
||||
schemaVersion: 1,
|
||||
scripts: [
|
||||
{ ...first.scripts[1], permissions: [...first.scripts[1].permissions].reverse(), ignored: "removed" },
|
||||
{ ...first.scripts[0], permissions: [...first.scripts[0].permissions].reverse(), dependencies: [...first.scripts[0].dependencies].reverse() },
|
||||
first.scripts[2],
|
||||
],
|
||||
};
|
||||
const canonical = canonicalizeScriptingManifest(second);
|
||||
self.postMessage({ equal: serializeScriptingManifest(first) === serializeScriptingManifest(second), firstId: canonical.scripts[0].id, firstPermission: canonical.scripts[0].permissions[0], dependencyOrder: canonical.scripts[2].dependencies.map((dependency) => dependency.id), unknownDropped: !("ignored" in canonical.scripts[0]) });
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { appendScriptExecutionAudit, createScriptExecutionAudit, gateScriptExecution, gateServerScriptJob, parseScriptExecutionAuditLog, parseScriptingManifest, platformCapabilities, SCRIPTING_BUDGET } from "../../../protocol/scripting-platform";
|
||||
|
||||
const sha = "a".repeat(64); const signature = "b".repeat(128);
|
||||
const script = { id: "script:clean", name: "Clean", entryPath: "scripts/clean.py", sourceSha256: sha, publisher: "Team", signature, keyId: "key:trusted", permissions: ["READ_MAIN"], dependencies: [], cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false };
|
||||
const script = { id: "script:clean", name: "Clean", entryPath: "scripts/clean.py", sourceByteLength: 128, sourceSha256: sha, publisher: "Team", signature, keyId: "key:trusted", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false };
|
||||
const base = { schemaVersion: 1, scripts: [script] };
|
||||
|
||||
self.onmessage = async () => {
|
||||
|
||||
24
web/app/src/workers/stl-edge-test.worker.ts
Normal file
24
web/app/src/workers/stl-edge-test.worker.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { importSTL, type STLVariant } from "../../../protocol/stl-import";
|
||||
|
||||
interface Request {
|
||||
cases: Array<{ id: string; bytes: ArrayBuffer; variant: STLVariant; unitScale: number }>;
|
||||
}
|
||||
|
||||
const scope = self as unknown as {
|
||||
onmessage: ((event: MessageEvent<Request>) => void) | null;
|
||||
postMessage(message: unknown): void;
|
||||
};
|
||||
|
||||
scope.onmessage = (event) => {
|
||||
const results = event.data.cases.map((candidate) => {
|
||||
try {
|
||||
return { id: candidate.id, status: "ACCEPTED", result: importSTL(candidate.bytes, { variant: candidate.variant, unitScale: candidate.unitScale }) };
|
||||
}
|
||||
catch (error) {
|
||||
return { id: candidate.id, status: "BLOCKED", code: error instanceof Error ? error.message.split(":", 1)[0] : "STL_IMPORT_FAILED" };
|
||||
}
|
||||
});
|
||||
scope.postMessage({ ok: true, results });
|
||||
};
|
||||
|
||||
export {};
|
||||
23
web/app/src/workers/stl-roundtrip-test.worker.ts
Normal file
23
web/app/src/workers/stl-roundtrip-test.worker.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { exportBinarySTL, createSTLLossReport } from "../../../protocol/stl-export";
|
||||
import { importSTL } from "../../../protocol/stl-import";
|
||||
|
||||
interface Request { bytes: ArrayBuffer; unitScale: number; sourceMaterialCount: number; }
|
||||
|
||||
const scope = self as unknown as {
|
||||
onmessage: ((event: MessageEvent<Request>) => void) | null;
|
||||
postMessage(message: unknown, transfer?: Transferable[]): void;
|
||||
};
|
||||
|
||||
scope.onmessage = (event) => {
|
||||
try {
|
||||
const imported = importSTL(event.data.bytes, { variant: "STL_BINARY", unitScale: event.data.unitScale });
|
||||
const output = exportBinarySTL(imported);
|
||||
const lossReport = createSTLLossReport(event.data.sourceMaterialCount);
|
||||
scope.postMessage({ ok: true, imported, output, lossReport }, [output]);
|
||||
}
|
||||
catch (error) {
|
||||
scope.postMessage({ ok: false, error: error instanceof Error ? error.message : "STL round-trip failed" });
|
||||
}
|
||||
};
|
||||
|
||||
export {};
|
||||
@@ -751,7 +751,8 @@ async function readAssetRow(projectId: string, sha256: string): Promise<AssetRow
|
||||
return row;
|
||||
}
|
||||
|
||||
async function putAsset(projectId: string, data: ArrayBuffer, mimeType: string, sourcePath?: string): Promise<StorageAssetPutResult> {
|
||||
async function putAsset(projectId: string, data: ArrayBuffer, mimeType: string, sourcePath?: string, faultAt?: "quota"): Promise<StorageAssetPutResult> {
|
||||
if (faultAt === "quota") throw new Error("QuotaExceededError: injected OPFS quota exhaustion");
|
||||
projectLayout(projectId);
|
||||
if (data.byteLength === 0) throw new Error("Asset data is empty");
|
||||
if (!/^[A-Za-z0-9.+-]+\/[A-Za-z0-9.+-]+$/.test(mimeType)) throw new Error("Invalid asset MIME type");
|
||||
@@ -1498,7 +1499,7 @@ async function handleRequest(event: MessageEvent<StorageRequest>): Promise<void>
|
||||
else if (command.type === "saveSnapshot") result = await withProjectTransaction(command.projectId, () => saveSnapshot(command.projectId, command.revision, command.buffer, command.maxCount, command.maxBytes));
|
||||
else if (command.type === "listSnapshots") result = await listSnapshots(command.projectId);
|
||||
else if (command.type === "readSnapshot") result = await readSnapshot(command.projectId, command.revision);
|
||||
else if (command.type === "putAsset") result = await putAsset(command.projectId, command.data, command.mimeType, command.sourcePath);
|
||||
else if (command.type === "putAsset") result = await putAsset(command.projectId, command.data, command.mimeType, command.sourcePath, command.faultAt);
|
||||
else if (command.type === "readAsset") result = await readAsset(command.projectId, command.sha256);
|
||||
else if (command.type === "listAssets") result = await listAssets(command.projectId);
|
||||
else if (command.type === "commitTexturePaintTile") result = await withProjectTransaction(command.commit.target.projectId, () => commitTexturePaintTile(command.commit));
|
||||
|
||||
Reference in New Issue
Block a user