@1agh/maude 0.52.2 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/studio/bin/_import-asset.mjs +1 -6
- package/apps/studio/bin/_import-brand.mjs +1 -1
- package/apps/studio/bin/_video-playwright.mjs +5 -1
- package/apps/studio/canvas-build.ts +91 -0
- package/apps/studio/canvas-lib.tsx +12 -1
- package/apps/studio/canvas-shell.tsx +60 -11
- package/apps/studio/client/app.jsx +183 -98
- package/apps/studio/client/canvas-url.js +6 -0
- package/apps/studio/client/styles/3-shell-maude.css +7 -0
- package/apps/studio/comments-overlay.tsx +59 -51
- package/apps/studio/dist/client.bundle.js +1266 -1266
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/http.ts +105 -4
- package/apps/studio/read-only-mode.ts +36 -0
- package/apps/studio/test/canvas-url.test.ts +13 -0
- package/apps/studio/test/read-only-gate.test.ts +130 -0
- package/apps/studio/test/report-proxy.test.ts +125 -0
- package/apps/studio/test/use-tool-mode.test.tsx +23 -0
- package/apps/studio/tool-palette.tsx +65 -51
- package/apps/studio/use-keyboard-discipline.tsx +6 -0
- package/apps/studio/use-tool-mode.tsx +37 -7
- package/apps/studio/whats-new.json +28 -1
- package/cli/bin/maude.mjs +0 -1
- package/package.json +8 -8
- package/cli/commands/share.mjs +0 -188
- package/cli/lib/share-plan.mjs +0 -100
- package/cli/lib/share-plan.test.mjs +0 -66
|
@@ -360,12 +360,7 @@ export function sanitizeSvgAllowlist(svgText) {
|
|
|
360
360
|
const window = new Window();
|
|
361
361
|
const doc = new window.DOMParser().parseFromString(svgText, 'image/svg+xml');
|
|
362
362
|
const root = doc.documentElement;
|
|
363
|
-
if (
|
|
364
|
-
!root ||
|
|
365
|
-
root.tagName !== 'svg' ||
|
|
366
|
-
root.namespaceURI !== SVG_NS ||
|
|
367
|
-
doc.querySelector('parsererror')
|
|
368
|
-
) {
|
|
363
|
+
if (root?.tagName !== 'svg' || root.namespaceURI !== SVG_NS || doc.querySelector('parsererror')) {
|
|
369
364
|
throw new ImportAssetError(3, 'SVG failed to parse');
|
|
370
365
|
}
|
|
371
366
|
walkAllowlist(root);
|
|
@@ -355,7 +355,7 @@ export function hardenBrandLogoSvg(sanitizedSvgText) {
|
|
|
355
355
|
const window = new Window();
|
|
356
356
|
const doc = new window.DOMParser().parseFromString(sanitizedSvgText, 'image/svg+xml');
|
|
357
357
|
const root = doc.documentElement;
|
|
358
|
-
if (
|
|
358
|
+
if (root?.tagName !== 'svg' || root.namespaceURI !== SVG_NS) {
|
|
359
359
|
throw new ImportBrandError(3, 'input is not a valid sanitized SVG document');
|
|
360
360
|
}
|
|
361
361
|
|
|
@@ -354,7 +354,11 @@ async function frameStepCapture({
|
|
|
354
354
|
});
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
|
|
357
|
+
// A comma operator hid the side effect here; the directory has to exist
|
|
358
|
+
// before anything writes a frame into it, so the effect is the point and
|
|
359
|
+
// deserves its own line.
|
|
360
|
+
if (dumpDir) mkdirSync(dumpDir, { recursive: true });
|
|
361
|
+
const dump = dumpDir ?? null;
|
|
358
362
|
const framePaths = [];
|
|
359
363
|
const t0 = Date.now();
|
|
360
364
|
|
|
@@ -79,6 +79,19 @@ export interface BuildCanvasOptions {
|
|
|
79
79
|
* detected.
|
|
80
80
|
*/
|
|
81
81
|
designRoot?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Cloud Phase 25 A1 — the SANDBOX's import allowlist.
|
|
84
|
+
*
|
|
85
|
+
* On a laptop an unrestricted resolver is correct and harmless: your code,
|
|
86
|
+
* your machine, your files. In a CELL the same resolver is a build-time read
|
|
87
|
+
* primitive pointed at a container that holds this tenant's credentials, so
|
|
88
|
+
* a tenant's own source may reach ONLY the runtime packages,
|
|
89
|
+
* `@maude/canvas-lib`, and relative paths that resolve INSIDE the design
|
|
90
|
+
* root. Anything else is a legible build error, never a silent read.
|
|
91
|
+
*
|
|
92
|
+
* Set to the absolute design root to arm it. Absent (desktop) ⇒ unchanged.
|
|
93
|
+
*/
|
|
94
|
+
restrictImportsTo?: string;
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
/**
|
|
@@ -115,6 +128,7 @@ export async function buildCanvasModule(
|
|
|
115
128
|
// in the bundle). The flatMap legacy alias for `react-dom` is no longer
|
|
116
129
|
// needed; RUNTIME_PACKAGES already lists every specifier the importmap covers.
|
|
117
130
|
const externalSpecifiers = new Set<string>(RUNTIME_PACKAGES);
|
|
131
|
+
const denials: Array<{ specifier: string; reason: string }> = [];
|
|
118
132
|
|
|
119
133
|
const built = await Bun.build({
|
|
120
134
|
entrypoints: [canvasAbsPath],
|
|
@@ -122,6 +136,12 @@ export async function buildCanvasModule(
|
|
|
122
136
|
format: 'esm',
|
|
123
137
|
minify: false,
|
|
124
138
|
splitting: false,
|
|
139
|
+
// Bun.build THROWS by default (an AggregateError whose message is the
|
|
140
|
+
// useless string "Bundle failed"), which made the `built.success` branch
|
|
141
|
+
// below dead code and hid every real diagnostic — including, once the
|
|
142
|
+
// sandbox arrived, the allowlist's own explanation of what it refused.
|
|
143
|
+
// Take the result, report it ourselves.
|
|
144
|
+
throw: false,
|
|
125
145
|
define: {
|
|
126
146
|
// Match runtime-bundle.ts: production React. Without this the canvas's
|
|
127
147
|
// `import { jsxDEV } from "react/jsx-dev-runtime"` resolves against a
|
|
@@ -154,10 +174,20 @@ export async function buildCanvasModule(
|
|
|
154
174
|
});
|
|
155
175
|
},
|
|
156
176
|
},
|
|
177
|
+
// LAST on purpose: by the time a specifier reaches this plugin, the
|
|
178
|
+
// runtime packages are external and `@maude/canvas-lib` is resolved, so
|
|
179
|
+
// what is left is exactly "everything else" — which, from a tenant's
|
|
180
|
+
// own file, must stay inside the design root.
|
|
181
|
+
...(options.restrictImportsTo ? [importAllowlist(options.restrictImportsTo, denials)] : []),
|
|
157
182
|
],
|
|
158
183
|
});
|
|
159
184
|
|
|
160
185
|
if (!built.success) {
|
|
186
|
+
// A DENIED IMPORT IS THE MESSAGE. Bun collapses a plugin throw into a bare
|
|
187
|
+
// "Bundle failed" log, so the allowlist records its own reasons and they
|
|
188
|
+
// win here — the person who wrote the canvas has to be able to act on it,
|
|
189
|
+
// and "Bundle failed" tells them nothing.
|
|
190
|
+
if (denials.length > 0) throw new Error(denials.map((d) => d.reason).join('\n'));
|
|
161
191
|
const msg = built.logs.map((l) => l.message).join('\n');
|
|
162
192
|
throw new Error(`Bun.build failed on ${canvasAbsPath}:\n${msg}`);
|
|
163
193
|
}
|
|
@@ -201,6 +231,67 @@ function buildCssInjector(slug: string, css: string): string {
|
|
|
201
231
|
return `// canvas-build: inject bundled sibling CSS so the canvas is self-contained.\n(function(){if(typeof document==="undefined")return;if(document.getElementById(${JSON.stringify(id)}))return;var s=document.createElement("style");s.id=${JSON.stringify(id)};s.dataset.canvasCss="bundled";s.textContent=${enc};document.head.appendChild(s);})();\n`;
|
|
202
232
|
}
|
|
203
233
|
|
|
234
|
+
/**
|
|
235
|
+
* The sandbox's import allowlist (Cloud Phase 25 A1).
|
|
236
|
+
*
|
|
237
|
+
* The rule is stated by IMPORTER, not by target, because the two trees have
|
|
238
|
+
* different rights: canvas-lib and its siblings are OUR code and import each
|
|
239
|
+
* other freely; a tenant's canvas may only reach inside the design root. A
|
|
240
|
+
* target-only rule would let a tenant's `../../../app/...` traversal land in
|
|
241
|
+
* the maude install and be waved through as "our" file.
|
|
242
|
+
*
|
|
243
|
+
* Denials THROW with the specifier and the reason — a rejected import must
|
|
244
|
+
* reach the person as a legible build error, never a 500 and never a silent
|
|
245
|
+
* read of something they should not have.
|
|
246
|
+
*/
|
|
247
|
+
function importAllowlist(
|
|
248
|
+
designRoot: string,
|
|
249
|
+
denials: Array<{ specifier: string; reason: string }>
|
|
250
|
+
): {
|
|
251
|
+
name: string;
|
|
252
|
+
setup: (builder: {
|
|
253
|
+
onResolve: (
|
|
254
|
+
opts: { filter: RegExp },
|
|
255
|
+
cb: (args: { path: string; importer: string }) => unknown
|
|
256
|
+
) => void;
|
|
257
|
+
}) => void;
|
|
258
|
+
} {
|
|
259
|
+
const root = path.resolve(designRoot);
|
|
260
|
+
const inRoot = (p: string) => p === root || p.startsWith(root + path.sep);
|
|
261
|
+
const deny = (specifier: string, reason: string): never => {
|
|
262
|
+
denials.push({ specifier, reason });
|
|
263
|
+
throw new Error(reason);
|
|
264
|
+
};
|
|
265
|
+
return {
|
|
266
|
+
name: 'maude-import-allowlist',
|
|
267
|
+
setup(builder) {
|
|
268
|
+
builder.onResolve({ filter: /.*/ }, (args: { path: string; importer: string }) => {
|
|
269
|
+
const importer = args.importer ? path.resolve(args.importer) : '';
|
|
270
|
+
// Our own modules (canvas-lib and its graph) resolve normally.
|
|
271
|
+
if (importer && !inRoot(importer)) return null;
|
|
272
|
+
if (!args.path.startsWith('.') && !path.isAbsolute(args.path)) {
|
|
273
|
+
deny(
|
|
274
|
+
args.path,
|
|
275
|
+
`This canvas imports "${args.path}", which is not available when it renders in a browser. ` +
|
|
276
|
+
`A canvas here can use the Maude runtime (react, motion, @maude/canvas-lib) and its own ` +
|
|
277
|
+
`project files — npm packages are not installed. Open the project in Maude Desktop if you ` +
|
|
278
|
+
`need one.`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
const resolved = path.resolve(path.dirname(importer || root), args.path);
|
|
282
|
+
if (!inRoot(resolved)) {
|
|
283
|
+
deny(
|
|
284
|
+
args.path,
|
|
285
|
+
`This canvas imports "${args.path}", which is outside the project (${path.relative(root, resolved)}). ` +
|
|
286
|
+
`A canvas may only import files inside its own design.`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return null; // inside the root — let Bun's resolver finish the job
|
|
290
|
+
});
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
204
295
|
function filterForExactPath(absPath: string): RegExp {
|
|
205
296
|
// Bun.build hands onLoad the resolved on-disk path. On macOS /tmp is a
|
|
206
297
|
// symlink to /private/tmp, so an entrypoint of "/tmp/foo.tsx" arrives in
|
|
@@ -128,6 +128,7 @@ import { scopedCdSelector, selectorIndex, shortText } from './dom-selection.ts';
|
|
|
128
128
|
// (photo/pipeline.ts, which imports pixi.js) is loaded via a DYNAMIC import
|
|
129
129
|
// inside <PhotoLayer> only when an edited photo actually mounts — see there.
|
|
130
130
|
import { isDefaultEdit, type PhotoEdit } from './photo/schema.ts';
|
|
131
|
+
import { isReadOnlyCanvas } from './read-only-mode.ts';
|
|
131
132
|
import { AgentPresenceProvider, useAgentPresence } from './use-agent-presence.tsx';
|
|
132
133
|
import { type DragState, useArtboardDrag } from './use-artboard-drag.tsx';
|
|
133
134
|
import {
|
|
@@ -757,6 +758,14 @@ function patchCanvasMeta(patch: {
|
|
|
757
758
|
layout?: { artboards: ArtboardRect[] };
|
|
758
759
|
}): void {
|
|
759
760
|
if (typeof window === 'undefined' || typeof fetch === 'undefined') return;
|
|
761
|
+
// Cloud Phase 25 C2 — a read-only session persists its CAMERA (per-user
|
|
762
|
+
// `_canvas-state/` view, DDR-115) but never the LAYOUT lane (versioned
|
|
763
|
+
// `.meta.json`). The dev-server refuses the layout lane too (http.ts);
|
|
764
|
+
// dropping it here keeps the UI from even trying.
|
|
765
|
+
if (patch.layout && isReadOnlyCanvas()) {
|
|
766
|
+
if (!patch.viewport) return;
|
|
767
|
+
patch = { viewport: patch.viewport };
|
|
768
|
+
}
|
|
760
769
|
const file = readCanvasMetaFile();
|
|
761
770
|
if (!file) return;
|
|
762
771
|
const sanitized: {
|
|
@@ -2096,7 +2105,9 @@ export function DCArtboard({
|
|
|
2096
2105
|
rectFor: (rid) => (ctx ? ctx.rectFor(rid) : null),
|
|
2097
2106
|
allRects: ctx?.artboards ?? [],
|
|
2098
2107
|
viewport: ctx?.viewport ?? null,
|
|
2099
|
-
|
|
2108
|
+
// Cloud Phase 25 C2 — a read-only canvas keeps the Move tool for
|
|
2109
|
+
// selection, but artboards don't drag (editing is absent, not hidden).
|
|
2110
|
+
enabled: !!ctx && (toolMode?.tool ?? 'move') === 'move' && !isReadOnlyCanvas(),
|
|
2100
2111
|
onCommit: (moved) => {
|
|
2101
2112
|
if (dragBus) dragBus.commitPositions(moved);
|
|
2102
2113
|
},
|
|
@@ -89,6 +89,7 @@ import {
|
|
|
89
89
|
import { ElementMarqueeOverlay } from './marquee-overlay.tsx';
|
|
90
90
|
import { MeasureOverlay } from './measure-overlay.tsx';
|
|
91
91
|
import { ParticipantsChrome } from './participants-chrome.tsx';
|
|
92
|
+
import { isReadOnlyCanvas } from './read-only-mode.ts';
|
|
92
93
|
import { mountCaret, placeCaretAt } from './text-caret.ts';
|
|
93
94
|
import { ToolPalette } from './tool-palette.tsx';
|
|
94
95
|
import { UndoHud } from './undo-hud.tsx';
|
|
@@ -1872,7 +1873,7 @@ function buildRegistry(deps: {
|
|
|
1872
1873
|
window as unknown as { __maudeConvertArtboard?: typeof postConvertArtboardToAbsolute }
|
|
1873
1874
|
).__maudeConvertArtboard = postConvertArtboardToAbsolute;
|
|
1874
1875
|
|
|
1875
|
-
|
|
1876
|
+
const registry: ContextRegistry = {
|
|
1876
1877
|
element: [
|
|
1877
1878
|
[
|
|
1878
1879
|
{
|
|
@@ -2280,6 +2281,43 @@ function buildRegistry(deps: {
|
|
|
2280
2281
|
],
|
|
2281
2282
|
overlay: [],
|
|
2282
2283
|
};
|
|
2284
|
+
return filterRegistryForReadOnly(registry);
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
/**
|
|
2288
|
+
* Cloud Phase 25 C2 — a read-only canvas keeps only the context-menu items
|
|
2289
|
+
* that READ the project (navigate, inspect, copy, export). Everything else is
|
|
2290
|
+
* ABSENT, not disabled. Deliberately an ALLOWLIST: a future item defaults to
|
|
2291
|
+
* absent for viewers (the C1 posture), rather than leaking in by omission.
|
|
2292
|
+
* `add-comment` joins when Phase 25 C3 lands comments on the cell's read-only
|
|
2293
|
+
* allowlist.
|
|
2294
|
+
*/
|
|
2295
|
+
const READ_ONLY_MENU_IDS = new Set([
|
|
2296
|
+
'fit-view',
|
|
2297
|
+
'reset-view',
|
|
2298
|
+
'fit-one',
|
|
2299
|
+
'open-timeline',
|
|
2300
|
+
'copy-css',
|
|
2301
|
+
'copy-id',
|
|
2302
|
+
'copy-style',
|
|
2303
|
+
'inspect',
|
|
2304
|
+
'select-layer',
|
|
2305
|
+
'deselect',
|
|
2306
|
+
'export-selection',
|
|
2307
|
+
'export-artboard',
|
|
2308
|
+
'export-canvas',
|
|
2309
|
+
'export-project',
|
|
2310
|
+
]);
|
|
2311
|
+
|
|
2312
|
+
function filterRegistryForReadOnly(registry: ContextRegistry): ContextRegistry {
|
|
2313
|
+
if (!isReadOnlyCanvas()) return registry;
|
|
2314
|
+
const out = {} as ContextRegistry;
|
|
2315
|
+
for (const key of Object.keys(registry) as (keyof ContextRegistry)[]) {
|
|
2316
|
+
out[key] = registry[key]
|
|
2317
|
+
.map((section) => section.filter((item) => READ_ONLY_MENU_IDS.has(item.id)))
|
|
2318
|
+
.filter((section) => section.length > 0);
|
|
2319
|
+
}
|
|
2320
|
+
return out;
|
|
2283
2321
|
}
|
|
2284
2322
|
|
|
2285
2323
|
function boundsOf(el: HTMLElement) {
|
|
@@ -3003,6 +3041,10 @@ function CanvasRouter({
|
|
|
3003
3041
|
opts?: { selectAll?: boolean }
|
|
3004
3042
|
): void {
|
|
3005
3043
|
if (editing) return;
|
|
3044
|
+
// Cloud Phase 25 C2 — inline text edit is a source write; a read-only
|
|
3045
|
+
// canvas never enters edit mode (one gate for BOTH entry points: the
|
|
3046
|
+
// dblclick drill and the annotation Text-tool click-through).
|
|
3047
|
+
if (isReadOnlyCanvas()) return;
|
|
3006
3048
|
editing = stamped;
|
|
3007
3049
|
original = stamped.textContent ?? '';
|
|
3008
3050
|
stamped.setAttribute('contenteditable', 'plaintext-only');
|
|
@@ -3248,6 +3290,11 @@ function CanvasRouter({
|
|
|
3248
3290
|
},
|
|
3249
3291
|
});
|
|
3250
3292
|
|
|
3293
|
+
// Cloud Phase 25 C2 — a read-only canvas mounts no EDIT chrome (resize /
|
|
3294
|
+
// spacing / grid handles, reorder drag, contextual + multi-artboard
|
|
3295
|
+
// toolbars). Editing is absent, not disabled. Selection, halos, measure,
|
|
3296
|
+
// annotations DISPLAY, cursors and presence all stay — they read.
|
|
3297
|
+
const readOnly = isReadOnlyCanvas();
|
|
3251
3298
|
return (
|
|
3252
3299
|
<>
|
|
3253
3300
|
{children}
|
|
@@ -3259,19 +3306,21 @@ function CanvasRouter({
|
|
|
3259
3306
|
<ElementMarqueeOverlay />
|
|
3260
3307
|
<HoverHalo el={hoverEl} />
|
|
3261
3308
|
<SelectionHalos />
|
|
3262
|
-
<ElementResizeOverlay />
|
|
3263
|
-
<SpacingHandlesOverlay />
|
|
3264
|
-
<GridTrackHandlesOverlay />
|
|
3265
|
-
<ReorderDrag />
|
|
3309
|
+
{!readOnly && <ElementResizeOverlay />}
|
|
3310
|
+
{!readOnly && <SpacingHandlesOverlay />}
|
|
3311
|
+
{!readOnly && <GridTrackHandlesOverlay />}
|
|
3312
|
+
{!readOnly && <ReorderDrag />}
|
|
3266
3313
|
<LayersLiveSync />
|
|
3267
3314
|
<GroupBbox />
|
|
3268
|
-
<EqualSpacingHandles />
|
|
3315
|
+
{!readOnly && <EqualSpacingHandles />}
|
|
3269
3316
|
<MeasureOverlay />
|
|
3270
|
-
<ContextualToolbar />
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3317
|
+
{!readOnly && <ContextualToolbar />}
|
|
3318
|
+
{!readOnly && (
|
|
3319
|
+
<MultiArtboardToolbar
|
|
3320
|
+
distributeArtboards={distributeArtboards}
|
|
3321
|
+
alignArtboards={alignArtboards}
|
|
3322
|
+
/>
|
|
3323
|
+
)}
|
|
3275
3324
|
<SnapGuideOverlay />
|
|
3276
3325
|
<PhotoPreviewBridge />
|
|
3277
3326
|
<UndoHud />
|