Checkpoint web parity through Chromium input tasks
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions

This commit is contained in:
mes123456
2026-08-19 10:39:03 -04:00
parent 5a11045ca5
commit 380cbed4ff
634 changed files with 41862 additions and 212 deletions

View File

@@ -50,6 +50,8 @@ import {
} from "../volume/nanovdb-viewport";
import { composePaintColorPatch, composePaintWeightPatch } from "../../../protocol/paint";
import { createDefaultWebWorkspaceState, reduceUICommand, type EditorType, type UICommand, type WorkspaceId } from "../../../protocol/ui-schema";
import { createIMECompositionState, reduceIMEComposition, shouldBlockOperatorShortcuts, type IMECompositionEvent } from "../../../protocol/ime-composition";
import { observeKeyboardEvent, type KeyboardObservation } from "../../../protocol/keyboard-contract";
import { createInitialUserActionStates, reduceUserActionStates, type UserActionIdentity, type UserActionKind } from "../../../protocol/user-action-state";
import { acquireProjectAction, createProjectActionMutexState, releaseProjectAction, type ProjectActionConflict, type ProjectActionIdentity } from "../../../protocol/project-action-mutex";
import { DEFAULT_FILE_READ_YIELD_BYTES, FileByteReadError, readFileBytes, type FileReadPhase } from "../../../protocol/file-byte-reader";
@@ -58,8 +60,17 @@ import { acceptHistoryTransaction, acceptMainSave, acceptMainTransaction, create
import type { WorkerFault } from "../../../protocol/worker-fault";
import { normalizeRecentProjects, RECENT_PROJECTS_SCHEMA_VERSION, type RecentProjectBackend, type RecentProjectIssue, type RecentProjectRecord } from "../../../protocol/recent-projects";
import { appendAppDiagnostic, createAppDiagnosticEntry, createAppDiagnosticReport, type AppDiagnosticArea, type AppDiagnosticCode, type AppDiagnosticContextValue, type AppDiagnosticEntry } from "../../../protocol/diagnostic-report";
import ioFormatUIRegistryJSON from "../capabilities/io-format-ui-registry.json";
import { filterIOFormatOperatorCommands, gateIOFormatFileSelection, ioFormatUIAccept, parseIOFormatUIRegistry, type IOFormatUICommandRef } from "../../../protocol/io-format-ui-gate";
import ioFormatRuntimeReceiptFreshnessJSON from "../capabilities/io-format-runtime-receipts-freshness.json";
import ioFormatRuntimeReceiptFreshnessExpectedJSON from "../capabilities/io-format-runtime-receipts-freshness-expected.json";
import { parseIOFormatReceiptFreshness, resolveFreshIOFormatRuntimeRoute, type IOFormatReceiptFreshnessExpectedIR } from "../../../protocol/io-format-receipt-freshness";
import "./app-shell.css";
const IO_FORMAT_UI_REGISTRY = parseIOFormatUIRegistry(ioFormatUIRegistryJSON);
const IO_FORMAT_RUNTIME_RECEIPTS = parseIOFormatReceiptFreshness(ioFormatRuntimeReceiptFreshnessJSON);
const IO_FORMAT_RUNTIME_RECEIPT_EXPECTED = ioFormatRuntimeReceiptFreshnessExpectedJSON as unknown as IOFormatReceiptFreshnessExpectedIR;
function recentProjectIssueMessage(code: RecentProjectIssue["code"]): string {
if (code === "MISSING") return "项目内容缺失";
if (code === "HASH_MISMATCH") return "项目内容校验失败";
@@ -948,6 +959,7 @@ interface OperatorCommand {
id: string;
label: string;
keywords: string;
ioFormat?: IOFormatUICommandRef;
execute: () => void;
}
@@ -1047,6 +1059,8 @@ export function App() {
const [wasmStatus, setWasmStatus] = useState("WASM ABI: starting");
const [storageStatus, setStorageStatus] = useState("Storage: starting");
const [manifestStatus, setManifestStatus] = useState("Manifest: checking");
const [imeComposition, setIMEComposition] = useState(createIMECompositionState);
const [keyboardObservation, setKeyboardObservation] = useState<KeyboardObservation | null>(null);
const [snapshot, setSnapshot] = useState<SceneSnapshotIR | null>(null);
const [selectedObjectIds, setSelectedObjectIds] = useState<Set<string>>(() => new Set());
const [meshSelection, setMeshSelection] = useState<MeshEditSelection>({ meshId: null, mode: "FACE", indices: new Set() });
@@ -1070,6 +1084,7 @@ export function App() {
const [recentProjectIssues, setRecentProjectIssues] = useState<RecentProjectIssue[]>([]);
const [storageBudget, setStorageBudget] = useState<StorageBudgetResult | null>(null);
const [diagnostics, setDiagnostics] = useState<AppDiagnosticEntry[]>([]);
const imeCompositionRef = useRef(imeComposition);
const webClientRef = useRef<WebEngineClient | null>(null);
const storageClientRef = useRef<StorageClient | null>(null);
const autosaveRef = useRef<AutosaveScheduler | null>(null);
@@ -1142,6 +1157,22 @@ export function App() {
dispatchUI({ type: "toggleMenu", menu: menu ?? undefined });
window.requestAnimationFrame(() => { if (menu) menuTriggerRefs.current[menu]?.focus(); });
};
useEffect(() => {
const handleComposition = (event: CompositionEvent): void => {
const type = event.type as IMECompositionEvent["type"];
const next = reduceIMEComposition(imeCompositionRef.current, { type, data: event.data });
imeCompositionRef.current = next;
setIMEComposition(next);
};
window.addEventListener("compositionstart", handleComposition);
window.addEventListener("compositionupdate", handleComposition);
window.addEventListener("compositionend", handleComposition);
return () => {
window.removeEventListener("compositionstart", handleComposition);
window.removeEventListener("compositionupdate", handleComposition);
window.removeEventListener("compositionend", handleComposition);
};
}, []);
const nextUserActionIdentity = <Kind extends UserActionKind>(kind: Kind): UserActionIdentity & { kind: Kind } => (
{ kind, actionId: `${kind}:${++userActionSequenceRef.current}` }
);
@@ -1676,6 +1707,12 @@ export function App() {
const onKeyDown = (event: KeyboardEvent): void => {
const target = event.target as HTMLElement | null;
const interactiveTarget = target?.closest("button, a, input, textarea, select, option, [contenteditable='true'], [role='button'], [role='menuitem'], [role='tab']");
try { setKeyboardObservation(observeKeyboardEvent(event)); }
catch { setKeyboardObservation(null); }
if (shouldBlockOperatorShortcuts(imeCompositionRef.current, event.isComposing)) {
if (!interactiveTarget) event.preventDefault();
return;
}
if (event.key === "F3") {
if (interactiveTarget && target?.matches("input, textarea, select, [contenteditable='true']")) return;
event.preventDefault();
@@ -2312,6 +2349,12 @@ export function App() {
};
const reportGLBExport = async (): Promise<void> => {
const identity = beginUserAction("EXPORT");
const runtimeRoute = resolveFreshIOFormatRuntimeRoute(IO_FORMAT_RUNTIME_RECEIPTS, IO_FORMAT_RUNTIME_RECEIPT_EXPECTED, { format: "GLB", operation: "EXPORT" });
if (runtimeRoute.status !== "READY") {
failUserAction(identity, "EXPORT_BLOCKED");
setEngineStatus(recordDiagnostic("EXPORT", "IO_FORMAT_UNSUPPORTED", runtimeRoute, { format: "GLB", operation: "EXPORT" }));
return;
}
if (!snapshot) {
failUserAction(identity, "EXPORT_PROJECT_UNAVAILABLE");
setEngineStatus(recordDiagnostic("EXPORT", "GLB_PROJECT_UNAVAILABLE", { code: "EXPORT_PROJECT_UNAVAILABLE", message: "No SceneIR snapshot is open" }));
@@ -2384,7 +2427,7 @@ export function App() {
if (workerFaultTestMode === "storage") storageClientRef.current?.crashForTest();
else webClientRef.current?.crashForTest();
};
const operatorCommands: OperatorCommand[] = [
const operatorCommands = filterIOFormatOperatorCommands<OperatorCommand>([
...(["Layout", "Modeling", "Animation"] as WorkspaceId[]).filter((id) => id !== workspace).map((id) => ({ id: `workspace.${id}`, label: `Switch to ${id}`, keywords: "workspace", execute: () => dispatchUI({ type: "switchWorkspace", workspaceId: id }) })),
{ id: "mode.toggle", label: uiState.context.mode === "Object" ? "Enter Edit Mode" : "Exit Edit Mode", keywords: "mode tab", execute: () => dispatchUI({ type: "setMode", mode: uiState.context.mode === "Object" ? "Edit" : "Object" }) },
...(snapshot && uiState.context.mode === "Object" ? [{ id: "object.add-cube", label: "Add Cube", keywords: "object primitive mesh", execute: () => { void applyEditCommand({ type: "createPrimitive", primitive: "CUBE", location: [0, 0, 0] }); } }] : []),
@@ -2394,11 +2437,12 @@ export function App() {
{ id: "edit.redo", label: "Redo", keywords: "history", execute: () => { void applyEditCommand({ type: "redo" }); } },
{ id: "file.save", label: "Save Project", keywords: "file blend", execute: () => { void saveBlend(); } },
{ id: "file.close", label: "Close Project", keywords: "file", execute: closeProject },
{ id: "file.export-glb", label: "Export GLB", keywords: "file export glb", ioFormat: { format: "GLB", operation: "EXPORT", execution: "LOCAL" } as const, execute: () => { void reportGLBExport(); } },
] : []),
];
], IO_FORMAT_UI_REGISTRY).filter((command) => !command.ioFormat || resolveFreshIOFormatRuntimeRoute(IO_FORMAT_RUNTIME_RECEIPTS, IO_FORMAT_RUNTIME_RECEIPT_EXPECTED, { format: command.ioFormat.format, operation: command.ioFormat.operation }).status === "READY");
return (
<main className="blender-app" data-workspace={workspace} data-ui-revision={uiState.context.revision}
<main className="blender-app" data-workspace={workspace} data-ime-composing={imeComposition.composing ? "true" : "false"} data-ime-revision={imeComposition.revision} data-ime-last-event={imeComposition.lastEvent} data-key-key={keyboardObservation?.key} data-key-code={keyboardObservation?.code} data-key-location={keyboardObservation?.location} data-key-modifiers={`${keyboardObservation?.shiftKey ? "S" : ""}${keyboardObservation?.ctrlKey ? "C" : ""}${keyboardObservation?.altKey ? "A" : ""}${keyboardObservation?.metaKey ? "M" : ""}`} data-key-dead={keyboardObservation?.deadKey ? "true" : "false"} data-ui-revision={uiState.context.revision}
data-user-action-import-status={userActions.IMPORT.status} data-user-action-import-id={userActions.IMPORT.identity?.actionId} data-user-action-import-error={userActions.IMPORT.errorCode ?? undefined}
data-user-action-open-status={userActions.OPEN.status} data-user-action-open-id={userActions.OPEN.identity?.actionId} data-user-action-open-error={userActions.OPEN.errorCode ?? undefined}
data-user-action-save-status={userActions.SAVE.status} data-user-action-save-id={userActions.SAVE.identity?.actionId} data-user-action-save-error={userActions.SAVE.errorCode ?? undefined}
@@ -2437,7 +2481,7 @@ export function App() {
{(["Layout", "Modeling", "Animation"] as WorkspaceId[]).map((item) => <button key={item} className={item === workspace ? "workspace-tab active" : "workspace-tab"} type="button" aria-current={item === workspace ? "page" : undefined} onClick={() => dispatchUI({ type: "switchWorkspace", workspaceId: item })}>{item}</button>)}
</nav>
<div className="topbar-actions"><select aria-label="最近项目" data-testid="recent-projects" defaultValue="" onChange={(event) => { const projectId = event.target.value; const displayName = event.target.selectedOptions[0]?.textContent ?? projectId; if (!projectId) return; void recoverCachedProject(projectId, displayName); event.currentTarget.value = ""; }}><option value=""> ({recentProjects.length})</option>{recentProjects.map((project) => <option key={project.projectId} value={project.projectId}>{project.displayName}</option>)}</select><button type="button" aria-label="打开 .blend" onClick={() => fileInputRef.current?.click()}></button><button type="button" aria-label="关闭项目" disabled={!snapshot} onClick={closeProject}></button><button type="button" aria-label="恢复项目" onClick={() => void recoverCachedProject()}></button><button type="button" aria-label="保存项目" onClick={() => void saveBlend()}></button><button type="button" aria-label="导出 GLB" onClick={() => void reportGLBExport()}>GLB</button><button type="button" aria-label="导出诊断报告" data-testid="export-diagnostics" disabled={diagnostics.length === 0} onClick={exportDiagnosticReport}></button><button type="button" aria-label="撤销" onClick={() => void applyEditCommand({ type: "undo" })}></button><button type="button" aria-label="重做" onClick={() => void applyEditCommand({ type: "redo" })}></button><button ref={operatorSearchTriggerRef} type="button" aria-label="操作搜索" aria-haspopup="dialog" onClick={() => dispatchUI({ type: "toggleOperatorSearch", open: true })}>F3</button>{workerFaultTestMode ? <button type="button" data-testid="inject-worker-crash" aria-label="注入 Worker 崩溃" onClick={injectWorkerCrash}>Fault</button> : null}</div>
<input ref={fileInputRef} className="file-input-hidden" type="file" accept=".blend,application/octet-stream" aria-label="打开 .blend 文件" data-testid="blend-file-input" onChange={(event) => { const file = event.target.files?.[0]; if (file) void openBlendFile(file); event.target.value = ""; }} />
<input ref={fileInputRef} className="file-input-hidden" type="file" accept={ioFormatUIAccept(IO_FORMAT_UI_REGISTRY)} aria-label="打开 .blend 文件" data-testid="blend-file-input" data-io-format-import-routes={IO_FORMAT_UI_REGISTRY.importRoutes.map((route) => route.format).join(",")} onChange={(event) => { const file = event.target.files?.[0]; if (file) { const gate = gateIOFormatFileSelection(file.name, IO_FORMAT_UI_REGISTRY); if (gate.status === "BLOCKED") setEngineStatus(recordDiagnostic("ENGINE", "IO_FORMAT_UNSUPPORTED", gate, { fileName: file.name })); else void openBlendFile(file); } event.target.value = ""; }} />
</header>
{workerFault ? <div className="worker-fault-banner" role="alert" data-testid="worker-fault-banner"><span>{workerFault.source === "engine" ? "Engine Worker" : "Storage Worker"} stopped; current project list and scene are retained.</span><button type="button" data-testid="restart-and-recover" onClick={() => void restartWorkersAndRecover()} disabled={workerRecoveryStatus === "RUNNING"}>{workerRecoveryStatus === "RUNNING" ? "恢复中..." : "重启并恢复"}</button>{workerRecoveryStatus === "FAILED" ? <span data-testid="worker-recovery-error">Recovery failed; retry is safe.</span> : null}</div> : null}
{recentProjectIssues.length > 0 ? <div className="recent-project-repair-banner" role="alert" data-testid="recent-project-repair-banner"><span> {recentProjectIssues.length} </span><div className="recent-project-repair-list">{recentProjectIssues.map((issue) => <div key={issue.project.projectId} className="recent-project-repair-item" data-project-id={issue.project.projectId} data-issue-code={issue.code}><span>{issue.project.displayName}: {recentProjectIssueMessage(issue.code)}</span><button type="button" data-testid={`remove-recent-project-${issue.project.projectId}`} aria-label={`移除失效项目引用 ${issue.project.displayName}`} onClick={() => void removeInvalidRecentProject(issue.project.projectId)}></button></div>)}</div></div> : null}

View File

@@ -0,0 +1,326 @@
{
"parentBindingSha256": "7f765bf16b62466f579b3de00751a0e012fef1c788f229c8e9675a57caa18aad",
"parentReceiptSetSha256": "f2c9a77e2cfad0ef3574b804fc06fb22972c07f55c72b0e6db2e359c4d95f57b",
"inventorySha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4",
"boundReceiptSetSha256": "2015d719a2046f76dda3daf7c8ae7a3fc5183a499489ef06a0c33f166c6ea0b9",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6",
"runtime": {
"binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82",
"blenderVersion": "5.2.0 LTS",
"buildBranch": "unknown",
"buildCommitTimestamp": 0,
"buildDate": "2026-07-24",
"buildHash": "unknown",
"buildOptions": {
"alembic": false,
"io_ply": true,
"io_stl": true,
"io_wavefront_obj": true,
"usd": false
},
"buildPlatform": "Linux",
"buildTime": "08:03:47",
"buildType": "Release",
"versionTuple": [
5,
2,
0
]
},
"receiptIdentities": [
{
"format": "GLTF",
"family": "GLTF",
"operation": "IMPORT",
"operator": "import_scene.gltf",
"registered": true,
"rnaIdentifier": "IMPORT_SCENE_OT_gltf",
"buildOption": null,
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"GLTF_SEPARATE"
],
"extensions": [
".gltf"
],
"sourceSha256": "843c7eb040189ccd7fcccfb697c4ecfd35d405add50008967b7ca5a89e4dcde7",
"settingsSha256": "01a5fbe96ab7af4bf38c35a3723a7619233299086e400ff5064dbd91e9d586e8",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "GLTF",
"family": "GLTF",
"operation": "EXPORT",
"operator": "export_scene.gltf",
"registered": true,
"rnaIdentifier": "EXPORT_SCENE_OT_gltf",
"buildOption": null,
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"GLTF_SEPARATE"
],
"extensions": [
".gltf"
],
"sourceSha256": "a1c2dea067898071d6d4f0fc4f83e81df920bc572a9250ed01229d618ef15a37",
"settingsSha256": "df7db92e5ea9a4d52a81dc8092f48ca9dfda80594e500b05f3787352891962f9",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "GLB",
"family": "GLTF",
"operation": "IMPORT",
"operator": "import_scene.gltf",
"registered": true,
"rnaIdentifier": "IMPORT_SCENE_OT_gltf",
"buildOption": null,
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"GLB"
],
"extensions": [
".glb"
],
"sourceSha256": "d78e336432dc578727992b8ca53368e3ac49ffdafeb1c16847fe48c54e35a079",
"settingsSha256": "b909a16f4103dfad49997c597c80ad3e71a932989f8a4594eb4f019223bd56e6",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "GLB",
"family": "GLTF",
"operation": "EXPORT",
"operator": "export_scene.gltf",
"registered": true,
"rnaIdentifier": "EXPORT_SCENE_OT_gltf",
"buildOption": null,
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"GLB"
],
"extensions": [
".glb"
],
"sourceSha256": "0c2820eb9d7b82a1cd1cb208f75f263a527c58576952d4bd934cf0f7eb29ee3e",
"settingsSha256": "3b375dcb889e594e61da9d8e912037d8fa0220ea876e7465e6c37da4f5b21cee",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "OBJ",
"family": "OBJ",
"operation": "IMPORT",
"operator": "wm.obj_import",
"registered": true,
"rnaIdentifier": "WM_OT_obj_import",
"buildOption": "io_wavefront_obj",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"OBJ"
],
"extensions": [
".obj"
],
"sourceSha256": "42f9e3b35f753e43100aaea618ed306f1bf062fb7bb44af841a2e2d599449fbd",
"settingsSha256": "4e879a3018ffcf529c28fb54e09efe5f3829b501a2b001da86f9a9b4b303bccb",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "OBJ",
"family": "OBJ",
"operation": "EXPORT",
"operator": "wm.obj_export",
"registered": true,
"rnaIdentifier": "WM_OT_obj_export",
"buildOption": "io_wavefront_obj",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"OBJ"
],
"extensions": [
".obj"
],
"sourceSha256": "b4328f82639bdfefb016aec15629135ebd080e0a5696759dd670ab85168b7c60",
"settingsSha256": "f7660d5742890d2f05e8da803f5d8616233570a4f06960b85a2142efd9396d2f",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "STL",
"family": "STL",
"operation": "IMPORT",
"operator": "wm.stl_import",
"registered": true,
"rnaIdentifier": "WM_OT_stl_import",
"buildOption": "io_stl",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"STL_BINARY",
"STL_ASCII"
],
"extensions": [
".stl"
],
"sourceSha256": "f6bb7a058c246914f5f077665aab1f3d45c60b176f917b15a54522d12164c703",
"settingsSha256": "b01bd0b819aa72cf1de817b3e9bca2df03b9ceac41f639f738176973fea9accb",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "STL",
"family": "STL",
"operation": "EXPORT",
"operator": "wm.stl_export",
"registered": true,
"rnaIdentifier": "WM_OT_stl_export",
"buildOption": "io_stl",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"STL_BINARY",
"STL_ASCII"
],
"extensions": [
".stl"
],
"sourceSha256": "8be11ddd9bd68dcddc676529d30abcc236289f25ece32e137fd79bcd5ff3286d",
"settingsSha256": "5f1797ab8291fb3d84a8c1a892aa692a120a7db4ef84c9d3a9b1e78d5a6d92b7",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "PLY",
"family": "PLY",
"operation": "IMPORT",
"operator": "wm.ply_import",
"registered": true,
"rnaIdentifier": "WM_OT_ply_import",
"buildOption": "io_ply",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"PLY"
],
"extensions": [
".ply"
],
"sourceSha256": "2c8483351e293c5e0450f55902c24e3346bf95d53243ebf194fccc1efc1caa80",
"settingsSha256": "390cbc8a4843d88bd567a7f7d51b9e0d5853992bfc7a66c4a2fd335ed33d25a3",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "PLY",
"family": "PLY",
"operation": "EXPORT",
"operator": "wm.ply_export",
"registered": true,
"rnaIdentifier": "WM_OT_ply_export",
"buildOption": "io_ply",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"PLY"
],
"extensions": [
".ply"
],
"sourceSha256": "2f6cf92d5a813083b206b9c38a4442f82685b1c5740f313b90a0a7b60604a2b5",
"settingsSha256": "cd4bf21d72086001a02377cdc5c7f22856cbd1e04b261e37484260f3fc2ea6ae",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "USD",
"family": "USD",
"operation": "IMPORT",
"operator": "wm.usd_import",
"registered": false,
"rnaIdentifier": null,
"buildOption": "usd",
"buildOptionEnabled": null,
"runtimeStatus": "OPERATOR_UNREGISTERED",
"variants": [
"USD",
"USDA",
"USDC",
"USDZ"
],
"extensions": [
".usd",
".usda",
".usdc",
".usdz"
],
"sourceSha256": "a8f79c9243ffa9ba0ba8e3e948c198fdffa727bac578269d8ec041f56cad1c5d",
"settingsSha256": "5a396eb68ddd5943df9834996b1e6b993ad7e66b019308aa318517980b518390",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "USD",
"family": "USD",
"operation": "EXPORT",
"operator": "wm.usd_export",
"registered": false,
"rnaIdentifier": null,
"buildOption": "usd",
"buildOptionEnabled": null,
"runtimeStatus": "OPERATOR_UNREGISTERED",
"variants": [
"USD",
"USDA",
"USDC",
"USDZ"
],
"extensions": [
".usd",
".usda",
".usdc",
".usdz"
],
"sourceSha256": "cf6a9737773c27faf82cd8b3d4df84a9b671e1c873cd5041f12d70926ec62abf",
"settingsSha256": "5a396eb68ddd5943df9834996b1e6b993ad7e66b019308aa318517980b518390",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "ALEMBIC",
"family": "ALEMBIC",
"operation": "IMPORT",
"operator": "wm.alembic_import",
"registered": false,
"rnaIdentifier": null,
"buildOption": "alembic",
"buildOptionEnabled": null,
"runtimeStatus": "OPERATOR_UNREGISTERED",
"variants": [
"ALEMBIC"
],
"extensions": [
".abc"
],
"sourceSha256": "7471c261ab8520df718399e69f6f8d73be11fb76f1717eac9a407964fc2f7ee3",
"settingsSha256": "fa2669dd3c464f8312581faf665690709dc503fdfb5662642a6e635bd2e91e55",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "ALEMBIC",
"family": "ALEMBIC",
"operation": "EXPORT",
"operator": "wm.alembic_export",
"registered": false,
"rnaIdentifier": null,
"buildOption": "alembic",
"buildOptionEnabled": null,
"runtimeStatus": "OPERATOR_UNREGISTERED",
"variants": [
"ALEMBIC"
],
"extensions": [
".abc"
],
"sourceSha256": "8d983f6f8c5bb722d2b12c47c56115ae4bdff0a173a9fb9f4d93f083bea8e513",
"settingsSha256": "fa2669dd3c464f8312581faf665690709dc503fdfb5662642a6e635bd2e91e55",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
}
]
}

View File

@@ -0,0 +1,332 @@
{
"schemaVersion": 1,
"task": "M12-05F",
"parentBindingSha256": "7f765bf16b62466f579b3de00751a0e012fef1c788f229c8e9675a57caa18aad",
"boundReceiptSetSha256": "2015d719a2046f76dda3daf7c8ae7a3fc5183a499489ef06a0c33f166c6ea0b9",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6",
"bound": {
"schemaVersion": 1,
"task": "M12-05E",
"parentReceiptSetSha256": "f2c9a77e2cfad0ef3574b804fc06fb22972c07f55c72b0e6db2e359c4d95f57b",
"inventorySha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4",
"runtime": {
"binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82",
"blenderVersion": "5.2.0 LTS",
"buildBranch": "unknown",
"buildCommitTimestamp": 0,
"buildDate": "2026-07-24",
"buildHash": "unknown",
"buildOptions": {
"alembic": false,
"io_ply": true,
"io_stl": true,
"io_wavefront_obj": true,
"usd": false
},
"buildPlatform": "Linux",
"buildTime": "08:03:47",
"buildType": "Release",
"versionTuple": [
5,
2,
0
]
},
"receipts": [
{
"format": "GLTF",
"family": "GLTF",
"operation": "IMPORT",
"operator": "import_scene.gltf",
"registered": true,
"rnaIdentifier": "IMPORT_SCENE_OT_gltf",
"buildOption": null,
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"GLTF_SEPARATE"
],
"extensions": [
".gltf"
],
"sourceSha256": "843c7eb040189ccd7fcccfb697c4ecfd35d405add50008967b7ca5a89e4dcde7",
"settingsSha256": "01a5fbe96ab7af4bf38c35a3723a7619233299086e400ff5064dbd91e9d586e8",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "GLTF",
"family": "GLTF",
"operation": "EXPORT",
"operator": "export_scene.gltf",
"registered": true,
"rnaIdentifier": "EXPORT_SCENE_OT_gltf",
"buildOption": null,
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"GLTF_SEPARATE"
],
"extensions": [
".gltf"
],
"sourceSha256": "a1c2dea067898071d6d4f0fc4f83e81df920bc572a9250ed01229d618ef15a37",
"settingsSha256": "df7db92e5ea9a4d52a81dc8092f48ca9dfda80594e500b05f3787352891962f9",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "GLB",
"family": "GLTF",
"operation": "IMPORT",
"operator": "import_scene.gltf",
"registered": true,
"rnaIdentifier": "IMPORT_SCENE_OT_gltf",
"buildOption": null,
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"GLB"
],
"extensions": [
".glb"
],
"sourceSha256": "d78e336432dc578727992b8ca53368e3ac49ffdafeb1c16847fe48c54e35a079",
"settingsSha256": "b909a16f4103dfad49997c597c80ad3e71a932989f8a4594eb4f019223bd56e6",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "GLB",
"family": "GLTF",
"operation": "EXPORT",
"operator": "export_scene.gltf",
"registered": true,
"rnaIdentifier": "EXPORT_SCENE_OT_gltf",
"buildOption": null,
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"GLB"
],
"extensions": [
".glb"
],
"sourceSha256": "0c2820eb9d7b82a1cd1cb208f75f263a527c58576952d4bd934cf0f7eb29ee3e",
"settingsSha256": "3b375dcb889e594e61da9d8e912037d8fa0220ea876e7465e6c37da4f5b21cee",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "OBJ",
"family": "OBJ",
"operation": "IMPORT",
"operator": "wm.obj_import",
"registered": true,
"rnaIdentifier": "WM_OT_obj_import",
"buildOption": "io_wavefront_obj",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"OBJ"
],
"extensions": [
".obj"
],
"sourceSha256": "42f9e3b35f753e43100aaea618ed306f1bf062fb7bb44af841a2e2d599449fbd",
"settingsSha256": "4e879a3018ffcf529c28fb54e09efe5f3829b501a2b001da86f9a9b4b303bccb",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "OBJ",
"family": "OBJ",
"operation": "EXPORT",
"operator": "wm.obj_export",
"registered": true,
"rnaIdentifier": "WM_OT_obj_export",
"buildOption": "io_wavefront_obj",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"OBJ"
],
"extensions": [
".obj"
],
"sourceSha256": "b4328f82639bdfefb016aec15629135ebd080e0a5696759dd670ab85168b7c60",
"settingsSha256": "f7660d5742890d2f05e8da803f5d8616233570a4f06960b85a2142efd9396d2f",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "STL",
"family": "STL",
"operation": "IMPORT",
"operator": "wm.stl_import",
"registered": true,
"rnaIdentifier": "WM_OT_stl_import",
"buildOption": "io_stl",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"STL_BINARY",
"STL_ASCII"
],
"extensions": [
".stl"
],
"sourceSha256": "f6bb7a058c246914f5f077665aab1f3d45c60b176f917b15a54522d12164c703",
"settingsSha256": "b01bd0b819aa72cf1de817b3e9bca2df03b9ceac41f639f738176973fea9accb",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "STL",
"family": "STL",
"operation": "EXPORT",
"operator": "wm.stl_export",
"registered": true,
"rnaIdentifier": "WM_OT_stl_export",
"buildOption": "io_stl",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"STL_BINARY",
"STL_ASCII"
],
"extensions": [
".stl"
],
"sourceSha256": "8be11ddd9bd68dcddc676529d30abcc236289f25ece32e137fd79bcd5ff3286d",
"settingsSha256": "5f1797ab8291fb3d84a8c1a892aa692a120a7db4ef84c9d3a9b1e78d5a6d92b7",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "PLY",
"family": "PLY",
"operation": "IMPORT",
"operator": "wm.ply_import",
"registered": true,
"rnaIdentifier": "WM_OT_ply_import",
"buildOption": "io_ply",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"PLY"
],
"extensions": [
".ply"
],
"sourceSha256": "2c8483351e293c5e0450f55902c24e3346bf95d53243ebf194fccc1efc1caa80",
"settingsSha256": "390cbc8a4843d88bd567a7f7d51b9e0d5853992bfc7a66c4a2fd335ed33d25a3",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "PLY",
"family": "PLY",
"operation": "EXPORT",
"operator": "wm.ply_export",
"registered": true,
"rnaIdentifier": "WM_OT_ply_export",
"buildOption": "io_ply",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"PLY"
],
"extensions": [
".ply"
],
"sourceSha256": "2f6cf92d5a813083b206b9c38a4442f82685b1c5740f313b90a0a7b60604a2b5",
"settingsSha256": "cd4bf21d72086001a02377cdc5c7f22856cbd1e04b261e37484260f3fc2ea6ae",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "USD",
"family": "USD",
"operation": "IMPORT",
"operator": "wm.usd_import",
"registered": false,
"rnaIdentifier": null,
"buildOption": "usd",
"buildOptionEnabled": null,
"runtimeStatus": "OPERATOR_UNREGISTERED",
"variants": [
"USD",
"USDA",
"USDC",
"USDZ"
],
"extensions": [
".usd",
".usda",
".usdc",
".usdz"
],
"sourceSha256": "a8f79c9243ffa9ba0ba8e3e948c198fdffa727bac578269d8ec041f56cad1c5d",
"settingsSha256": "5a396eb68ddd5943df9834996b1e6b993ad7e66b019308aa318517980b518390",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "USD",
"family": "USD",
"operation": "EXPORT",
"operator": "wm.usd_export",
"registered": false,
"rnaIdentifier": null,
"buildOption": "usd",
"buildOptionEnabled": null,
"runtimeStatus": "OPERATOR_UNREGISTERED",
"variants": [
"USD",
"USDA",
"USDC",
"USDZ"
],
"extensions": [
".usd",
".usda",
".usdc",
".usdz"
],
"sourceSha256": "cf6a9737773c27faf82cd8b3d4df84a9b671e1c873cd5041f12d70926ec62abf",
"settingsSha256": "5a396eb68ddd5943df9834996b1e6b993ad7e66b019308aa318517980b518390",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "ALEMBIC",
"family": "ALEMBIC",
"operation": "IMPORT",
"operator": "wm.alembic_import",
"registered": false,
"rnaIdentifier": null,
"buildOption": "alembic",
"buildOptionEnabled": null,
"runtimeStatus": "OPERATOR_UNREGISTERED",
"variants": [
"ALEMBIC"
],
"extensions": [
".abc"
],
"sourceSha256": "7471c261ab8520df718399e69f6f8d73be11fb76f1717eac9a407964fc2f7ee3",
"settingsSha256": "fa2669dd3c464f8312581faf665690709dc503fdfb5662642a6e635bd2e91e55",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
},
{
"format": "ALEMBIC",
"family": "ALEMBIC",
"operation": "EXPORT",
"operator": "wm.alembic_export",
"registered": false,
"rnaIdentifier": null,
"buildOption": "alembic",
"buildOptionEnabled": null,
"runtimeStatus": "OPERATOR_UNREGISTERED",
"variants": [
"ALEMBIC"
],
"extensions": [
".abc"
],
"sourceSha256": "8d983f6f8c5bb722d2b12c47c56115ae4bdff0a173a9fb9f4d93f083bea8e513",
"settingsSha256": "fa2669dd3c464f8312581faf665690709dc503fdfb5662642a6e635bd2e91e55",
"runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6"
}
]
}
}

View File

@@ -0,0 +1,282 @@
{
"schemaVersion": 1,
"task": "M12-05D",
"inventorySha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4",
"runtime": {
"binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82",
"blenderVersion": "5.2.0 LTS",
"buildBranch": "unknown",
"buildCommitTimestamp": 0,
"buildDate": "2026-07-24",
"buildHash": "unknown",
"buildOptions": {
"alembic": false,
"io_ply": true,
"io_stl": true,
"io_wavefront_obj": true,
"usd": false
},
"buildPlatform": "Linux",
"buildTime": "08:03:47",
"buildType": "Release",
"versionTuple": [
5,
2,
0
]
},
"receipts": [
{
"format": "GLTF",
"family": "GLTF",
"operation": "IMPORT",
"operator": "import_scene.gltf",
"registered": true,
"rnaIdentifier": "IMPORT_SCENE_OT_gltf",
"buildOption": null,
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"GLTF_SEPARATE"
],
"extensions": [
".gltf"
]
},
{
"format": "GLTF",
"family": "GLTF",
"operation": "EXPORT",
"operator": "export_scene.gltf",
"registered": true,
"rnaIdentifier": "EXPORT_SCENE_OT_gltf",
"buildOption": null,
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"GLTF_SEPARATE"
],
"extensions": [
".gltf"
]
},
{
"format": "GLB",
"family": "GLTF",
"operation": "IMPORT",
"operator": "import_scene.gltf",
"registered": true,
"rnaIdentifier": "IMPORT_SCENE_OT_gltf",
"buildOption": null,
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"GLB"
],
"extensions": [
".glb"
]
},
{
"format": "GLB",
"family": "GLTF",
"operation": "EXPORT",
"operator": "export_scene.gltf",
"registered": true,
"rnaIdentifier": "EXPORT_SCENE_OT_gltf",
"buildOption": null,
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"GLB"
],
"extensions": [
".glb"
]
},
{
"format": "OBJ",
"family": "OBJ",
"operation": "IMPORT",
"operator": "wm.obj_import",
"registered": true,
"rnaIdentifier": "WM_OT_obj_import",
"buildOption": "io_wavefront_obj",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"OBJ"
],
"extensions": [
".obj"
]
},
{
"format": "OBJ",
"family": "OBJ",
"operation": "EXPORT",
"operator": "wm.obj_export",
"registered": true,
"rnaIdentifier": "WM_OT_obj_export",
"buildOption": "io_wavefront_obj",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"OBJ"
],
"extensions": [
".obj"
]
},
{
"format": "STL",
"family": "STL",
"operation": "IMPORT",
"operator": "wm.stl_import",
"registered": true,
"rnaIdentifier": "WM_OT_stl_import",
"buildOption": "io_stl",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"STL_BINARY",
"STL_ASCII"
],
"extensions": [
".stl"
]
},
{
"format": "STL",
"family": "STL",
"operation": "EXPORT",
"operator": "wm.stl_export",
"registered": true,
"rnaIdentifier": "WM_OT_stl_export",
"buildOption": "io_stl",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"STL_BINARY",
"STL_ASCII"
],
"extensions": [
".stl"
]
},
{
"format": "PLY",
"family": "PLY",
"operation": "IMPORT",
"operator": "wm.ply_import",
"registered": true,
"rnaIdentifier": "WM_OT_ply_import",
"buildOption": "io_ply",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"PLY"
],
"extensions": [
".ply"
]
},
{
"format": "PLY",
"family": "PLY",
"operation": "EXPORT",
"operator": "wm.ply_export",
"registered": true,
"rnaIdentifier": "WM_OT_ply_export",
"buildOption": "io_ply",
"buildOptionEnabled": true,
"runtimeStatus": "AVAILABLE",
"variants": [
"PLY"
],
"extensions": [
".ply"
]
},
{
"format": "USD",
"family": "USD",
"operation": "IMPORT",
"operator": "wm.usd_import",
"registered": false,
"rnaIdentifier": null,
"buildOption": "usd",
"buildOptionEnabled": null,
"runtimeStatus": "OPERATOR_UNREGISTERED",
"variants": [
"USD",
"USDA",
"USDC",
"USDZ"
],
"extensions": [
".usd",
".usda",
".usdc",
".usdz"
]
},
{
"format": "USD",
"family": "USD",
"operation": "EXPORT",
"operator": "wm.usd_export",
"registered": false,
"rnaIdentifier": null,
"buildOption": "usd",
"buildOptionEnabled": null,
"runtimeStatus": "OPERATOR_UNREGISTERED",
"variants": [
"USD",
"USDA",
"USDC",
"USDZ"
],
"extensions": [
".usd",
".usda",
".usdc",
".usdz"
]
},
{
"format": "ALEMBIC",
"family": "ALEMBIC",
"operation": "IMPORT",
"operator": "wm.alembic_import",
"registered": false,
"rnaIdentifier": null,
"buildOption": "alembic",
"buildOptionEnabled": null,
"runtimeStatus": "OPERATOR_UNREGISTERED",
"variants": [
"ALEMBIC"
],
"extensions": [
".abc"
]
},
{
"format": "ALEMBIC",
"family": "ALEMBIC",
"operation": "EXPORT",
"operator": "wm.alembic_export",
"registered": false,
"rnaIdentifier": null,
"buildOption": "alembic",
"buildOptionEnabled": null,
"runtimeStatus": "OPERATOR_UNREGISTERED",
"variants": [
"ALEMBIC"
],
"extensions": [
".abc"
]
}
]
}

View File

@@ -0,0 +1,17 @@
{
"schemaVersion": 1,
"task": "M12-05C",
"parentMatrixSha256": "139c9d764736da176b32414ecda840c07eb0ba5f3864da2dc08ce04bae161366",
"projectFileAccept": ".blend,application/octet-stream",
"importRoutes": [],
"exportRoutes": [
{
"format": "GLB",
"operation": "EXPORT",
"execution": "LOCAL",
"extensions": [
".glb"
]
}
]
}

View File

@@ -0,0 +1 @@
export { gateLinkedDataMutation } from "../../protocol/library-linked-mutation";

View File

@@ -0,0 +1 @@
export { applyOverrideWriter } from "../../protocol/library-override-writer";

View File

@@ -135,8 +135,8 @@ export class StorageClient {
return this.request({ type: "readSnapshot", projectId, revision }) as Promise<StorageSnapshotReadResult>;
}
putAsset(projectId: string, data: ArrayBuffer, mimeType: string, sourcePath?: string): Promise<StorageAssetPutResult> {
return this.request({ type: "putAsset", projectId, data, mimeType, sourcePath }, [data]) as Promise<StorageAssetPutResult>;
putAsset(projectId: string, data: ArrayBuffer, mimeType: string, sourcePath?: string, faultAt?: "quota"): Promise<StorageAssetPutResult> {
return this.request({ type: "putAsset", projectId, data, mimeType, sourcePath, faultAt }, [data]) as Promise<StorageAssetPutResult>;
}
readAsset(projectId: string, sha256: string): Promise<StorageAssetReadResult> {

View File

@@ -0,0 +1,7 @@
export {
GLB_RECOVERY_SCHEMA_VERSION,
beginGLBRecoveryOperation,
blockGLBRecoveryForQuota,
parseGLBRecoveryReceipt,
recoverGLBRecoveryOperation,
} from "../../../protocol/glb-recovery";

View File

@@ -0,0 +1,9 @@
export {
IO_FORMAT_RECOVERY_SCHEMA_VERSION,
beginIOFormatRecoveryOperation,
blockIOFormatRecoveryOperation,
cancelIOFormatRecoveryOperation,
commitIOFormatRecoveryOperation,
parseIOFormatRecoveryReceipt,
recoverIOFormatRecoveryOperation,
} from "../../../protocol/io-format-recovery";

View File

@@ -0,0 +1,27 @@
export const SCRIPT_SANDBOX_DISPOSE_SCHEMA = 1 as const;
export interface ScriptSandboxDisposeReceipt {
schemaVersion: typeof SCRIPT_SANDBOX_DISPOSE_SCHEMA;
disposeCount: number;
idempotent: boolean;
resources: {
messagePorts: 0;
timers: 0;
abortControllers: 0;
transferableBuffers: 0;
pendingRequests: 0;
cacheReferences: 0;
};
lateTimerMessages: 0;
}
export function createScriptSandboxDisposeReceipt(disposeCount: number): ScriptSandboxDisposeReceipt {
if (!Number.isSafeInteger(disposeCount) || disposeCount < 1) throw new Error("SCRIPT_SANDBOX_DISPOSE_INVALID: dispose count must be positive");
return {
schemaVersion: SCRIPT_SANDBOX_DISPOSE_SCHEMA,
disposeCount,
idempotent: disposeCount > 1,
resources: { messagePorts: 0, timers: 0, abortControllers: 0, transferableBuffers: 0, pendingRequests: 0, cacheReferences: 0 },
lateTimerMessages: 0,
};
}

View File

@@ -0,0 +1,56 @@
export const SCRIPT_SANDBOX_RECOVERY_SCHEMA = 1 as const;
export interface ScriptSandboxRecoveryAuditEntry {
sequence: 1 | 2;
requestId: string;
previousEntrySha256: string | null;
entrySha256: string;
sourceSha256: string;
manifestSha256: string;
}
export interface ScriptSandboxRecoveryReceipt {
schemaVersion: typeof SCRIPT_SANDBOX_RECOVERY_SCHEMA;
operation: "SCRIPT_SANDBOX_RECOVERY";
previousGeneration: number;
nextGeneration: number;
mainRevisionBefore: number;
mainRevisionAfter: number;
sourceSha256: string;
manifestSha256: string;
audit: {
entries: 2;
first: ScriptSandboxRecoveryAuditEntry;
second: ScriptSandboxRecoveryAuditEntry;
};
recovered: true;
execution: "DISABLED";
}
const SHA256 = /^[a-f0-9]{64}$/;
const REQUEST_ID = /^[-A-Za-z0-9:_./]{1,256}$/;
function assertDigest(value: string, name: string): void {
if (!SHA256.test(value)) throw new Error(`SCRIPT_SANDBOX_RECOVERY_INVALID: ${name} must be a SHA-256 digest`);
}
function assertEntry(entry: ScriptSandboxRecoveryAuditEntry, expectedSequence: 1 | 2, sourceSha256: string, manifestSha256: string): void {
if (entry.sequence !== expectedSequence || !REQUEST_ID.test(entry.requestId)) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit sequence or request id is invalid");
if (expectedSequence === 1 ? entry.previousEntrySha256 !== null : !entry.previousEntrySha256) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit previous hash is invalid");
assertDigest(entry.entrySha256, `audit[${expectedSequence}].entrySha256`);
if (entry.previousEntrySha256 !== null) assertDigest(entry.previousEntrySha256, `audit[${expectedSequence}].previousEntrySha256`);
if (entry.sourceSha256 !== sourceSha256 || entry.manifestSha256 !== manifestSha256) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit source or manifest hash drifted");
}
export function createScriptSandboxRecoveryReceipt(input: Omit<ScriptSandboxRecoveryReceipt, "schemaVersion" | "operation" | "recovered" | "execution">): ScriptSandboxRecoveryReceipt {
if (!Number.isSafeInteger(input.previousGeneration) || !Number.isSafeInteger(input.nextGeneration) || input.nextGeneration !== input.previousGeneration + 1) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: generation must advance once");
if (!Number.isSafeInteger(input.mainRevisionBefore) || input.mainRevisionBefore < 0 || input.mainRevisionAfter !== input.mainRevisionBefore) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: Main revision changed during recovery");
assertDigest(input.sourceSha256, "sourceSha256");
assertDigest(input.manifestSha256, "manifestSha256");
if (input.audit.entries !== 2) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit entry count is invalid");
assertEntry(input.audit.first, 1, input.sourceSha256, input.manifestSha256);
assertEntry(input.audit.second, 2, input.sourceSha256, input.manifestSha256);
if (input.audit.second.previousEntrySha256 !== input.audit.first.entrySha256) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit hash chain is not continuous");
if (input.audit.first.requestId === input.audit.second.requestId) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit request id is replayed");
return { schemaVersion: SCRIPT_SANDBOX_RECOVERY_SCHEMA, operation: "SCRIPT_SANDBOX_RECOVERY", ...input, recovered: true, execution: "DISABLED" };
}

View File

@@ -7,6 +7,8 @@ import { cloneMeshGeometryBuffers } from "../../../protocol/mesh-geometry-delta"
import { nonMeshChunkTransferables } from "../../../protocol/nonmesh-binary";
import type { OffscreenViewportRequest, OffscreenViewportResponse } from "./offscreen-viewport-protocol";
import { PBR_PROFILE, PBR_SHADOW_PROFILE, PBR_TONE_MAPPING } from "./pbr";
import { resolveViewportPixelMetrics, viewportNDC } from "../../../protocol/viewport-dpr";
import { observePointerEvent } from "../../../protocol/pointer-contract";
import type { NonMeshElementKind } from "./nonmesh";
import type { GreasePencilPointPreview, GreasePencilPointRef } from "./grease-pencil";
import type { CurveGizmoFrameIR, CurveGizmoHandleIR } from "../../../protocol/nonmesh-interaction";
@@ -118,7 +120,7 @@ export class OffscreenViewportRenderer implements ViewportBackend {
canvas: offscreen,
width: Math.max(1, canvas.clientWidth),
height: Math.max(1, canvas.clientHeight),
pixelRatio: Math.min(window.devicePixelRatio || 1, 2),
pixelRatio: resolveViewportPixelMetrics(1, 1, window.devicePixelRatio).pixelRatio,
};
this.worker.postMessage(request, [offscreen]);
this.resizeObserver = new ResizeObserver(() => this.resize());
@@ -237,15 +239,21 @@ export class OffscreenViewportRenderer implements ViewportBackend {
}
private resize(): void {
const metrics = resolveViewportPixelMetrics(Math.max(1, this.canvas.clientWidth), Math.max(1, this.canvas.clientHeight), window.devicePixelRatio);
this.canvas.dataset.viewportCssSize = `${metrics.cssWidth}x${metrics.cssHeight}`;
this.canvas.dataset.viewportBackingSize = `${metrics.backingWidth}x${metrics.backingHeight}`;
this.canvas.dataset.viewportPixelRatio = String(metrics.pixelRatio);
this.worker.postMessage({
type: "resize",
width: Math.max(1, this.canvas.clientWidth),
height: Math.max(1, this.canvas.clientHeight),
pixelRatio: Math.min(window.devicePixelRatio || 1, 2),
width: metrics.cssWidth,
height: metrics.cssHeight,
pixelRatio: metrics.pixelRatio,
} satisfies OffscreenViewportRequest);
}
private pointerDown = (event: PointerEvent): void => {
try { this.canvas.dataset.lastPointer = JSON.stringify(observePointerEvent(event)); }
catch { this.canvas.dataset.lastPointer = "BLOCKED"; }
this.pointer = { id: event.pointerId, x: event.clientX, y: event.clientY, moved: false };
try {
this.canvas.setPointerCapture(event.pointerId);
@@ -266,12 +274,13 @@ export class OffscreenViewportRenderer implements ViewportBackend {
};
private pointerUp = (event: PointerEvent): void => {
try { this.canvas.dataset.lastPointer = JSON.stringify(observePointerEvent(event)); }
catch { this.canvas.dataset.lastPointer = "BLOCKED"; }
if (!this.pointer || this.pointer.id !== event.pointerId) return;
if (!this.pointer.moved) {
const bounds = this.canvas.getBoundingClientRect();
const x = ((event.clientX - bounds.left) / Math.max(1, bounds.width)) * 2 - 1;
const y = -((event.clientY - bounds.top) / Math.max(1, bounds.height)) * 2 + 1;
this.worker.postMessage({ type: "pick", x, y, additive: event.shiftKey || event.ctrlKey || event.metaKey, baseSelectionRevision: this.greasePencilSelectionRevision } satisfies OffscreenViewportRequest);
const ndc = viewportNDC(event.clientX, event.clientY, bounds);
this.worker.postMessage({ type: "pick", x: ndc.x, y: ndc.y, additive: event.shiftKey || event.ctrlKey || event.metaKey, baseSelectionRevision: this.greasePencilSelectionRevision } satisfies OffscreenViewportRequest);
}
this.pointer = null;
};

View File

@@ -69,6 +69,8 @@ import {
import { VIEWPORT_DEFAULT_ORBIT, VIEWPORT_ORBIT_MAX_DISTANCE, VIEWPORT_ORBIT_MIN_DISTANCE, VIEWPORT_ORBIT_ROTATE_SENSITIVITY, VIEWPORT_ORBIT_ZOOM_SENSITIVITY, orbitPosition, orbitStateFromPosition } from "../../../protocol/viewport-camera";
import { validatePaintDepthVisibilityRequest, type PaintDepthVisibilityRequestIR, type PaintDepthVisibilityResultIR } from "../../../protocol/paint-depth-visibility";
import { samplePaintDepthVisibilityGPU } from "./paint-depth-visibility";
import { resolveViewportPixelMetrics, viewportNDC } from "../../../protocol/viewport-dpr";
import { observePointerEvent } from "../../../protocol/pointer-contract";
export function collectMeshInstanceGroups(snapshot: SceneSnapshotIR, minimumSize = 2): Map<string, string[]> {
const groups = new Map<string, string[]>();
@@ -137,7 +139,7 @@ export class ViewportRenderer {
this.raycaster.params.Points.threshold = 0.14;
this.renderer = new WebGLRenderer({ canvas, antialias: true, alpha: false, preserveDrawingBuffer: true });
configurePBRRenderer(this.renderer);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
this.renderer.setPixelRatio(resolveViewportPixelMetrics(1, 1, window.devicePixelRatio).pixelRatio);
this.renderer.setClearColor(new Color("#25272b"));
this.canvas.dataset.rendererBackend = "webgl-pbr";
this.canvas.dataset.pbrProfile = PBR_PROFILE;
@@ -170,6 +172,8 @@ export class ViewportRenderer {
this.resizeObserver = new ResizeObserver(() => this.resize());
this.resizeObserver.observe(canvas);
this.canvas.addEventListener("click", this.handleClick);
this.canvas.addEventListener("pointerdown", this.handlePointerObservation);
this.canvas.addEventListener("pointercancel", this.handlePointerObservation);
this.canvas.addEventListener("webglcontextlost", this.handleContextLost);
this.canvas.addEventListener("webglcontextrestored", this.handleContextRestored);
this.resize();
@@ -793,10 +797,14 @@ export class ViewportRenderer {
private resize(): void {
const width = Math.max(1, this.canvas.clientWidth);
const height = Math.max(1, this.canvas.clientHeight);
const metrics = resolveViewportPixelMetrics(width, height, window.devicePixelRatio);
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.controls.rotateSpeed = VIEWPORT_ORBIT_ROTATE_SENSITIVITY * height / (2 * Math.PI);
this.renderer.setSize(width, height, false);
this.canvas.dataset.viewportCssSize = `${metrics.cssWidth}x${metrics.cssHeight}`;
this.canvas.dataset.viewportBackingSize = `${metrics.backingWidth}x${metrics.backingHeight}`;
this.canvas.dataset.viewportPixelRatio = String(metrics.pixelRatio);
this.publishCameraState();
}
@@ -810,12 +818,14 @@ export class ViewportRenderer {
}
private handleClick = (event: MouseEvent): void => {
if ("pointerType" in event) {
try { this.canvas.dataset.lastPointer = JSON.stringify(observePointerEvent(event as MouseEvent & { pointerType?: string; pointerId?: number; pressure?: number; tiltX?: number; tiltY?: number; buttons?: number; type?: string })); }
catch { this.canvas.dataset.lastPointer = "BLOCKED"; }
}
const bounds = this.canvas.getBoundingClientRect();
if (bounds.width <= 0 || bounds.height <= 0) return;
this.pointer.set(
((event.clientX - bounds.left) / bounds.width) * 2 - 1,
-((event.clientY - bounds.top) / bounds.height) * 2 + 1,
);
const ndc = viewportNDC(event.clientX, event.clientY, bounds);
this.pointer.set(ndc.x, ndc.y);
this.raycaster.setFromCamera(this.pointer, this.camera);
const hits = this.raycaster.intersectObjects(this.importedRoot.children, true);
const greasePencilHit = this.editMode
@@ -886,6 +896,11 @@ export class ViewportRenderer {
if (typeof objectId === "string") this.onSelect?.(objectId, additive);
};
private handlePointerObservation = (event: PointerEvent): void => {
try { this.canvas.dataset.lastPointer = JSON.stringify(observePointerEvent(event)); }
catch { this.canvas.dataset.lastPointer = "BLOCKED"; }
};
private renderLoop = (): void => {
if (this.disposed) return;
if (!this.contextLost) {
@@ -912,7 +927,7 @@ export class ViewportRenderer {
this.contextLost = false;
this.canvas.dataset.deviceStatus = "restoring";
configurePBRRenderer(this.renderer);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
this.renderer.setPixelRatio(resolveViewportPixelMetrics(1, 1, window.devicePixelRatio).pixelRatio);
this.renderer.setClearColor(new Color("#25272b"));
this.resize();
this.volumeRenderCache.clear();
@@ -949,6 +964,8 @@ export class ViewportRenderer {
window.cancelAnimationFrame(this.animationFrame);
this.resizeObserver.disconnect();
this.canvas.removeEventListener("click", this.handleClick);
this.canvas.removeEventListener("pointerdown", this.handlePointerObservation);
this.canvas.removeEventListener("pointercancel", this.handlePointerObservation);
this.canvas.removeEventListener("webglcontextlost", this.handleContextLost);
this.canvas.removeEventListener("webglcontextrestored", this.handleContextRestored);
this.controls.dispose();

View 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 {};

View 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 {};

View 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 {};

View 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);
};

View 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 {};

View 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 {};

View 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 {};

View 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 });
};

View 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,
});
};

View 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" });
};

View File

@@ -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);
}
};

View 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() });
}
};

View 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);
}
};

View 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 });
};

View 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" });
};

View 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 });
};

View 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 });
};

View 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 });
};

View 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"] })])),
});
};

View File

@@ -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]) });
};

View File

@@ -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 () => {

View 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 {};

View 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 {};

View File

@@ -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));