@1agh/maude 0.28.1 → 0.29.0
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/README.md +2 -2
- package/{plugins/design/dev-server → apps/studio}/ai-banner.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/annotations-context-toolbar.tsx +285 -5
- package/{plugins/design/dev-server → apps/studio}/annotations-layer.tsx +1206 -119
- package/{plugins/design/dev-server → apps/studio}/api.ts +486 -32
- package/{plugins/design/dev-server → apps/studio}/artboard-activity-overlay.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/artboard-marquee.tsx +1 -1
- package/apps/studio/bin/_motion-sample-playwright.mjs +129 -0
- package/{plugins/design/dev-server → apps/studio}/bin/canvas-edit.sh +1 -1
- package/{plugins/design/dev-server → apps/studio}/bin/draw-proof.sh +37 -2
- package/{plugins/design/dev-server → apps/studio}/bin/preflight.sh +1 -1
- package/apps/studio/bin/read-annotations.mjs +438 -0
- package/apps/studio/bin/read-annotations.sh +11 -0
- package/{plugins/design/dev-server → apps/studio}/bin/runtime-health.sh +1 -1
- package/{plugins/design/dev-server → apps/studio}/bin/screenshot.sh +62 -2
- package/{plugins/design/dev-server → apps/studio}/bin/server-up.sh +9 -3
- package/apps/studio/bin/to-lottie-verify.html +68 -0
- package/apps/studio/bin/to-lottie.sh +128 -0
- package/{plugins/design/dev-server → apps/studio}/build.ts +5 -3
- package/{plugins/design/dev-server → apps/studio}/canvas-arrowheads.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-comment-mount.tsx +1 -1
- package/apps/studio/canvas-create.ts +104 -0
- package/{plugins/design/dev-server → apps/studio}/canvas-cursors.ts +46 -12
- package/{plugins/design/dev-server → apps/studio}/canvas-icons.tsx +60 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-lib-inline.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-lib-resolver.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-lib.tsx +31 -12
- package/{plugins/design/dev-server → apps/studio}/canvas-shell.tsx +56 -41
- package/{plugins/design/dev-server → apps/studio}/client/app.jsx +1713 -530
- package/apps/studio/client/canvas-url.js +81 -0
- package/{plugins/design/dev-server → apps/studio}/client/comments-overlay.css +41 -27
- package/{plugins/design/dev-server → apps/studio}/client/index.html +2 -1
- package/apps/studio/client/styles/1-tokens-maude.css +296 -0
- package/apps/studio/client/styles/3-shell-maude.css +459 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles/3-shell.css +129 -0
- package/apps/studio/client/styles/4-components-maude.css +74 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles/4-components.css +357 -34
- package/apps/studio/client/styles/5-maude-overrides.css +123 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles/_index.css +9 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles.css +1 -1
- package/apps/studio/client/tour/overlay.jsx +191 -0
- package/apps/studio/client/tour/usage-tour.js +35 -0
- package/apps/studio/client/whats-new-seen.js +52 -0
- package/apps/studio/client/whats-new.jsx +250 -0
- package/{plugins/design/dev-server → apps/studio}/commands/annotation-strokes-command.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/commands/equal-spacing-command.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/commands/move-artboards-command.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/comments-overlay.tsx +25 -6
- package/{plugins/design/dev-server → apps/studio}/context-menu.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/contextual-toolbar.tsx +4 -7
- package/{plugins/design/dev-server → apps/studio}/cursors-overlay.tsx +1 -1
- package/apps/studio/dist/client.bundle.js +22825 -0
- package/apps/studio/dist/comment-mount.js +1960 -0
- package/apps/studio/dist/styles.css +5695 -0
- package/{plugins/design/dev-server → apps/studio}/dom-selection.ts +75 -4
- package/apps/studio/draw/animate.ts +333 -0
- package/{plugins/design/dev-server → apps/studio}/draw/brush.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/composition.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/geometry.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/index.ts +4 -1
- package/{plugins/design/dev-server → apps/studio}/draw/layout.ts +1 -1
- package/apps/studio/draw/morph.ts +204 -0
- package/{plugins/design/dev-server → apps/studio}/draw/optimize.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/palette.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/primitives.ts +1 -1
- package/apps/studio/draw/serialize-animate.ts +324 -0
- package/{plugins/design/dev-server → apps/studio}/draw/serialize.ts +9 -3
- package/apps/studio/draw/test/animate.test.ts +172 -0
- package/apps/studio/draw/test/morph.test.ts +107 -0
- package/apps/studio/draw/test/serialize-animate.test.ts +142 -0
- package/{plugins/design/dev-server → apps/studio}/equal-spacing-detector.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/equal-spacing-handles.tsx +7 -29
- package/{plugins/design/dev-server → apps/studio}/export-dialog.tsx +13 -1
- package/{plugins/design/dev-server → apps/studio}/http.ts +102 -1
- package/{plugins/design/dev-server → apps/studio}/input-router.tsx +8 -1
- package/{plugins/design/dev-server → apps/studio}/inspect.ts +20 -3
- package/{plugins/design/dev-server → apps/studio}/marquee-overlay.tsx +5 -2
- package/{plugins/design/dev-server → apps/studio}/participants-chrome.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/paths.ts +15 -11
- package/{plugins/design/dev-server → apps/studio}/server.ts +18 -0
- package/{plugins/design/dev-server → apps/studio}/sync/index.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/test/annotations-layer.test.ts +63 -0
- package/{plugins/design/dev-server → apps/studio}/test/annotations-roundtrip.test.ts +97 -0
- package/apps/studio/test/annotations-text-format.test.ts +396 -0
- package/apps/studio/test/asset-api.test.ts +149 -0
- package/apps/studio/test/canvas-create-api.test.ts +361 -0
- package/{plugins/design/dev-server → apps/studio}/test/canvas-cursors.test.ts +2 -0
- package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-resolver.test.ts +1 -1
- package/apps/studio/test/canvas-media-drop.test.ts +158 -0
- package/{plugins/design/dev-server → apps/studio}/test/canvas-origin-gate.test.ts +17 -1
- package/apps/studio/test/canvas-url.test.ts +99 -0
- package/apps/studio/test/index-data-canvas-ds.test.ts +97 -0
- package/apps/studio/test/motion-sample.test.ts +35 -0
- package/{plugins/design/dev-server → apps/studio}/test/perf-harness.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/test/phase-3.6-smoke.test.ts +3 -1
- package/apps/studio/test/read-annotations.test.ts +409 -0
- package/{plugins/design/dev-server → apps/studio}/test/sanitize-annotation-svg.test.ts +108 -2
- package/{plugins/design/dev-server → apps/studio}/test/shared-doc-convergence.test.ts +1 -1
- package/apps/studio/test/tour-overlay.test.tsx +56 -0
- package/{plugins/design/dev-server → apps/studio}/test/use-annotation-resize.test.ts +43 -0
- package/{plugins/design/dev-server → apps/studio}/test/use-tool-mode.test.tsx +4 -1
- package/apps/studio/test/whats-new.test.ts +101 -0
- package/apps/studio/text-imports.d.ts +9 -0
- package/{plugins/design/dev-server → apps/studio}/tool-palette.tsx +4 -69
- package/{plugins/design/dev-server → apps/studio}/undo-hud.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/undo-stack.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-agent-presence.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-annotation-resize.tsx +40 -7
- package/{plugins/design/dev-server → apps/studio}/use-annotation-selection.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-annotations-visibility.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-artboard-drag.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-canvas-activity.tsx +1 -1
- package/apps/studio/use-canvas-media-drop.tsx +343 -0
- package/{plugins/design/dev-server → apps/studio}/use-collab.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-cursor-modifiers.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-keyboard-discipline.tsx +5 -2
- package/{plugins/design/dev-server → apps/studio}/use-selection-set.tsx +9 -1
- package/{plugins/design/dev-server → apps/studio}/use-snap-guides.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-tool-mode.tsx +3 -1
- package/{plugins/design/dev-server → apps/studio}/use-undo-stack.tsx +1 -1
- package/apps/studio/whats-new.json +139 -0
- package/apps/studio/whats-new.schema.json +70 -0
- package/apps/studio/whats-new.ts +87 -0
- package/cli/bin/maude.mjs +10 -0
- package/cli/commands/design.mjs +15 -7
- package/cli/commands/design.test.mjs +36 -0
- package/cli/commands/help.mjs +8 -2
- package/cli/commands/hub.mjs +3 -3
- package/cli/commands/hub.test.mjs +1 -1
- package/cli/commands/scenario-report.mjs +2 -9
- package/cli/lib/flow-design-integration.test.mjs +1 -1
- package/cli/lib/gitignore-block.mjs +1 -0
- package/cli/lib/plugin-cli-reachability.test.mjs +8 -7
- package/cli/lib/preflight.mjs +1 -1
- package/package.json +15 -15
- package/plugins/design/templates/brief-board.tsx.template +61 -0
- package/plugins/design/templates/canvas.tsx.template +1 -1
- package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -1
- package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
- package/plugins/flow/.claude-plugin/config.schema.json +16 -0
- package/plugins/design/dev-server/dist/client.bundle.js +0 -20692
- package/plugins/design/dev-server/dist/comment-mount.js +0 -1924
- package/plugins/design/dev-server/dist/styles.css +0 -2710
- /package/{plugins/design/dev-server → apps/studio}/activity.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_enumerate-artboards-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_html-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_pdf-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_png-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_pptx-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_pw-launch.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_screenshot-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_svg-optimize.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_svg-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/asset-sweep.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/bootstrap-check.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/check-runtime-bundles.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/draw-build.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/handoff.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/prep.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/scenario-report.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/slug.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/smoke.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/svg-optimize.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/visual-sanity.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/boot-self-heal.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-build.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-edit.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-header.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-meta.schema.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-pipeline.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/hmr.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/iframe-lazy.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/0-reset.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/1-tokens.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/2-layout.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/5-utilities.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/ai-activity.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/awareness-bridge.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/git-lifecycle.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/index.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/persistence.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/protocol.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/registry.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/room.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/config.schema.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/context.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/.min-sizes.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_decoding.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_encoding.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion_react.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/pixi-js.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom_client.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-dev-runtime.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-runtime.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_awareness.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_sync.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/yjs.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/brush.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/composition.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/geometry.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/gradient.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/layout.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/optimize.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/palette.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/primitives.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/serialize.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/examples/README.md +0 -0
- /package/{plugins/design/dev-server → apps/studio}/examples/perf-100-artboards.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/_browser-bundles.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/canva-handoff-prompt.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/canva.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/html.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/index.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/pdf.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/png.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/pptx.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/scope.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/svg.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/zip.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/fs-watch.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/handoff.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/history.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/hmr-broadcast.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/locator.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/mem.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/runtime-bundle.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/server.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/agent.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/atomic-write.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/codec.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/connection-state.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/echo-guard.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/fs-mirror.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/hubs-config.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/materialize.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/migrate-seed.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/origins.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/projection.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/status.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/untrusted.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/_helpers.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/active-state.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/activity.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/ai-activity.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/annotation-strokes-command.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/annotations-api.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/annotations-draw-modifiers.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/artboard-activity-overlay.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/binary-smoke.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/boot-self-heal.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/bundle-smoke.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-build.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-edit.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-header.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-hmr-runtime.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-inline.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-motion.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-meta-api.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-pipeline.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-route.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-annotations-bridge.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-awareness-bridge.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-bridge.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-loopback.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-protocol.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-reseed-guard.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-room.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-stress.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/comment-mount.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/comments-api.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/compile-entry.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/context-resolve-tokens.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/csp-canvas-shell.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/equal-spacing-detector.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/canva.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/endpoint.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/history.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/html.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pdf.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/png.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx-deck.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pw-launch.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/scope.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/svg.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/zip.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-20-annotations.svg +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-21-annotations.svg +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/fs-watch.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/git-lifecycle.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/handoff-static-frames.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/handoff.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/history-rollback.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/hmr-broadcast.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/hmr-classify.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/input-router.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/locator.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/marquee-overlay.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/move-artboards-command.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/participants-chrome.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/paths.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle-error-mapping.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/server-lifecycle.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-foundation.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-migrate.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-projection.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/snap-distance-pill.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-agent.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-atomic-write.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-codec.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-connection-state.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-echo-guard.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-fs-mirror.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-hardening.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-hubs-config.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-meta-codec.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-runtime.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-status.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-untrusted.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/system-endpoint.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/tool-palette.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/undo-stack.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-agent-presence.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-annotation-selection.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-artboard-drag.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-canvas-activity.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-collab.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-cursor-modifiers.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-keyboard-discipline.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-selection-set.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-snap-guides.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-undo-stack.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/ws-handshake.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/tsconfig.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/ws.ts +0 -0
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import type { Dirent } from 'node:fs';
|
|
6
|
-
import { readdir, readFile, stat as statp } from 'node:fs/promises';
|
|
6
|
+
import { mkdir, readdir, readFile, rename, stat as statp } from 'node:fs/promises';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
|
|
9
|
+
import { renderBriefBoard, validateCanvasName } from './canvas-create.ts';
|
|
9
10
|
import type { Context } from './context.ts';
|
|
10
11
|
|
|
11
12
|
const SKIP_DIRS = new Set([
|
|
@@ -95,6 +96,9 @@ export interface Comment {
|
|
|
95
96
|
id: string;
|
|
96
97
|
file: string;
|
|
97
98
|
selector: string;
|
|
99
|
+
/** Occurrence index among `querySelectorAll(selector)` — disambiguates a
|
|
100
|
+
* component repeated within one artboard. Absent on legacy comments. */
|
|
101
|
+
index?: number;
|
|
98
102
|
dom_path: string[];
|
|
99
103
|
tag: string;
|
|
100
104
|
classes: string;
|
|
@@ -129,6 +133,14 @@ export interface ExportHistoryEntry {
|
|
|
129
133
|
at: string;
|
|
130
134
|
}
|
|
131
135
|
|
|
136
|
+
export type CreateCanvasResult =
|
|
137
|
+
| { ok: true; file: string; rel: string; slug: string }
|
|
138
|
+
| { ok: false; status: number; error: string };
|
|
139
|
+
|
|
140
|
+
export type DeleteCanvasResult =
|
|
141
|
+
| { ok: true; rel: string; slug: string; trashed: string[]; trashDir: string }
|
|
142
|
+
| { ok: false; status: number; error: string };
|
|
143
|
+
|
|
132
144
|
export interface Api {
|
|
133
145
|
// File tree
|
|
134
146
|
fileSlug(file: string): string;
|
|
@@ -168,6 +180,16 @@ export interface Api {
|
|
|
168
180
|
// Annotations sidecar (Phase 5 — .design/<slug>.annotations.svg)
|
|
169
181
|
loadAnnotations(file: string): Promise<string | null>;
|
|
170
182
|
saveAnnotations(file: string, svg: string): Promise<boolean>;
|
|
183
|
+
// Phase 23 — content-addressed binary image write (drag-drop / paste / picker)
|
|
184
|
+
saveAsset(bytes: Uint8Array): Promise<SaveAssetResult>;
|
|
185
|
+
// Create a blank brief board from the browser (Phase 22 — POST /_api/canvas)
|
|
186
|
+
createCanvas(input: {
|
|
187
|
+
name?: unknown;
|
|
188
|
+
kind?: unknown;
|
|
189
|
+
group?: unknown;
|
|
190
|
+
}): Promise<CreateCanvasResult>;
|
|
191
|
+
// Soft-delete a canvas from the browser (Phase 22 — DELETE /_api/canvas)
|
|
192
|
+
deleteCanvas(input: { file?: unknown }): Promise<DeleteCanvasResult>;
|
|
171
193
|
// Aggregate data
|
|
172
194
|
buildIndexData(): Promise<unknown>;
|
|
173
195
|
buildSystemData(dsName?: string | null): Promise<unknown>;
|
|
@@ -200,8 +222,107 @@ const ANNOTATION_SVG_ELEMENTS = new Set([
|
|
|
200
222
|
'polygon',
|
|
201
223
|
'circle',
|
|
202
224
|
'text',
|
|
225
|
+
// Annotation polish (item 4a) — `tspan` carries one line of multi-line text
|
|
226
|
+
// inside a `<text>` (SVG `<text>` ignores `\n`). Inert presentational element
|
|
227
|
+
// with no script capability — same safety class as `text`. Only `x`/`dy`
|
|
228
|
+
// (geometry) survive; the attribute denylist strips any on*/style/href.
|
|
229
|
+
'tspan',
|
|
230
|
+
// Phase 23 — `image` (dropped/pasted raster). The ONLY element allowed to keep
|
|
231
|
+
// an href, and ONLY a relative assets/<sha8>.<ext> path (ASSET_IMAGE_HREF_RE) —
|
|
232
|
+
// every external / data: / javascript: / `..` href is still stripped. <image>
|
|
233
|
+
// is a passive include with no script capability. See DDR (Task 9).
|
|
234
|
+
'image',
|
|
203
235
|
]);
|
|
204
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Phase 23 — the ONLY href shape allowed to survive on an `<image>`: a relative,
|
|
239
|
+
* single-segment `assets/<name>.<ext>` path. Anchored (`^…$`), no scheme, no `/`
|
|
240
|
+
* beyond the one `assets/` segment (blocks `assets/../../etc/passwd`), no query,
|
|
241
|
+
* ext ∈ the four raster types the upload route accepts. Everything else —
|
|
242
|
+
* external, `data:`, `javascript:`, `..`, or any href on a non-image element —
|
|
243
|
+
* is stripped by Rule 3. Pairs with the asset-write caps (DDR Task 9).
|
|
244
|
+
*/
|
|
245
|
+
export const ASSET_IMAGE_HREF_RE = /^assets\/[A-Za-z0-9._-]+\.(?:png|jpe?g|webp|gif)$/;
|
|
246
|
+
|
|
247
|
+
/** Phase 23 — hard ceiling on a single uploaded asset (10 MB). */
|
|
248
|
+
export const ASSET_MAX_BYTES = 10 * 1024 * 1024;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Phase 23 security review (DDR-088 follow-up) — aggregate per-server-instance
|
|
252
|
+
* write budget for `/_api/asset`. Content-addressing dedupes IDENTICAL bytes,
|
|
253
|
+
* but a one-byte mutation (a PNG `tEXt` chunk / a single pixel) yields a fresh
|
|
254
|
+
* sha8 each time, so dedup is NOT a disk-fill defense. This caps total bytes a
|
|
255
|
+
* single dev-server instance will ever write to `assets/` — generous for real
|
|
256
|
+
* reference material, but bounds a scripted loop from the untrusted canvas
|
|
257
|
+
* origin. Overridable via `MAUDE_ASSET_SESSION_BUDGET` (bytes) for power users.
|
|
258
|
+
*/
|
|
259
|
+
export const ASSET_SESSION_BUDGET = (() => {
|
|
260
|
+
const env = Number(process.env.MAUDE_ASSET_SESSION_BUDGET);
|
|
261
|
+
return Number.isFinite(env) && env > 0 ? env : 256 * 1024 * 1024;
|
|
262
|
+
})();
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Phase 23 — content-type sniff from the first bytes (magic numbers). The
|
|
266
|
+
* declared name / extension / Content-Type is NEVER trusted — the bytes decide
|
|
267
|
+
* the stored extension (a `.png` name carrying GIF bytes is stored as `.gif`).
|
|
268
|
+
* SVG (XML/text) matches nothing here → returns null → rejected, so a
|
|
269
|
+
* script-bearing vector can't ride in through the image route. See DDR (Task 9).
|
|
270
|
+
*/
|
|
271
|
+
export function sniffImageType(bytes: Uint8Array): 'png' | 'jpg' | 'gif' | 'webp' | null {
|
|
272
|
+
const b = bytes;
|
|
273
|
+
// PNG — 89 50 4E 47 0D 0A 1A 0A
|
|
274
|
+
if (
|
|
275
|
+
b.length >= 8 &&
|
|
276
|
+
b[0] === 0x89 &&
|
|
277
|
+
b[1] === 0x50 &&
|
|
278
|
+
b[2] === 0x4e &&
|
|
279
|
+
b[3] === 0x47 &&
|
|
280
|
+
b[4] === 0x0d &&
|
|
281
|
+
b[5] === 0x0a &&
|
|
282
|
+
b[6] === 0x1a &&
|
|
283
|
+
b[7] === 0x0a
|
|
284
|
+
) {
|
|
285
|
+
return 'png';
|
|
286
|
+
}
|
|
287
|
+
// JPEG — FF D8 FF
|
|
288
|
+
if (b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return 'jpg';
|
|
289
|
+
// GIF — "GIF87a" / "GIF89a"
|
|
290
|
+
if (
|
|
291
|
+
b.length >= 6 &&
|
|
292
|
+
b[0] === 0x47 &&
|
|
293
|
+
b[1] === 0x49 &&
|
|
294
|
+
b[2] === 0x46 &&
|
|
295
|
+
b[3] === 0x38 &&
|
|
296
|
+
(b[4] === 0x37 || b[4] === 0x39) &&
|
|
297
|
+
b[5] === 0x61
|
|
298
|
+
) {
|
|
299
|
+
return 'gif';
|
|
300
|
+
}
|
|
301
|
+
// WEBP — "RIFF"????"WEBP"
|
|
302
|
+
if (
|
|
303
|
+
b.length >= 12 &&
|
|
304
|
+
b[0] === 0x52 &&
|
|
305
|
+
b[1] === 0x49 &&
|
|
306
|
+
b[2] === 0x46 &&
|
|
307
|
+
b[3] === 0x46 &&
|
|
308
|
+
b[8] === 0x57 &&
|
|
309
|
+
b[9] === 0x45 &&
|
|
310
|
+
b[10] === 0x42 &&
|
|
311
|
+
b[11] === 0x50
|
|
312
|
+
) {
|
|
313
|
+
return 'webp';
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export interface SaveAssetResult {
|
|
319
|
+
ok: boolean;
|
|
320
|
+
status?: number;
|
|
321
|
+
error?: string;
|
|
322
|
+
/** Relative `assets/<sha8>.<ext>` path on success. */
|
|
323
|
+
path?: string;
|
|
324
|
+
}
|
|
325
|
+
|
|
205
326
|
/**
|
|
206
327
|
* A3 (DDR-060 F1 re-audit) — sanitize an annotation SVG before it is persisted /
|
|
207
328
|
* synced / mirrored into the collab Y.Map. ALLOWLIST, not denylist (the F1
|
|
@@ -224,37 +345,59 @@ const ANNOTATION_SVG_ELEMENTS = new Set([
|
|
|
224
345
|
* synced file a peer / Claude-context ingests (defense-in-depth, DDR-054 §3).
|
|
225
346
|
*/
|
|
226
347
|
export function sanitizeAnnotationSvg(svg: string): string {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
348
|
+
// Phase 23 — <image> href handling. Rule 3 below strips EVERY href (that's the
|
|
349
|
+
// zero-bypass invariant). To let a legit assets/ href survive WITHOUT
|
|
350
|
+
// re-opening the denylist race, we (a) neutralize any input-supplied marker so
|
|
351
|
+
// only this pre-pass can mint one, (b) on `<image>` ONLY, hoist a regex-valid
|
|
352
|
+
// assets href into a sanitizer-inert `data-mdcc-asset` marker (an external /
|
|
353
|
+
// data: / `..` / non-image href is left as a plain href → stripped by Rule 3),
|
|
354
|
+
// then (c) after the three rules, restore the marker back to `href` — but ONLY
|
|
355
|
+
// after RE-validating the value, so a forged marker can never smuggle a
|
|
356
|
+
// scheme/traversal back in. The restored value is, by construction, a safe
|
|
357
|
+
// same-origin path. See ASSET_IMAGE_HREF_RE + DDR (Task 9).
|
|
358
|
+
const hoisted = svg
|
|
359
|
+
.replace(/\sdata-mdcc-asset\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '')
|
|
360
|
+
.replace(/<\s*(?:[\w-]+:)?image\b[^>]*>/gi, (tag) => {
|
|
361
|
+
const m = tag.match(/(?:xlink:)?href\s*=\s*"([^"]*)"|(?:xlink:)?href\s*=\s*'([^']*)'/i);
|
|
362
|
+
const val = m ? (m[1] ?? m[2] ?? '') : '';
|
|
363
|
+
if (!val || !ASSET_IMAGE_HREF_RE.test(val)) return tag;
|
|
364
|
+
return tag.replace(/(?:xlink:)?href\s*=\s*("[^"]*"|'[^']*')/i, `data-mdcc-asset="${val}"`);
|
|
365
|
+
});
|
|
366
|
+
const cleaned = hoisted
|
|
367
|
+
// 1. Remove the CONTENT of executable / instruction-bearing elements (not
|
|
368
|
+
// just their tags) so an injected script body / `@import` / prompt-
|
|
369
|
+
// injection string can't survive as inert text a future raw-renderer or
|
|
370
|
+
// Claude-context read might act on. Namespace-tolerant (`svg:script`),
|
|
371
|
+
// non-greedy to the first matching dangerous close tag.
|
|
372
|
+
.replace(
|
|
373
|
+
/<\s*(?:[\w-]+:)?(?:script|style|foreignObject|title|desc)\b[\s\S]*?<\s*\/\s*(?:[\w-]+:)?(?:script|style|foreignObject|title|desc)\s*>/gi,
|
|
374
|
+
''
|
|
375
|
+
)
|
|
376
|
+
// 2. Element allowlist — drop the markup of any tag whose LOCAL name isn't
|
|
377
|
+
// in the fixed annotation vocabulary. `[^>]*` stops at the first `>`;
|
|
378
|
+
// annotation attrs never contain a literal `>`.
|
|
379
|
+
.replace(/<\/?\s*([a-zA-Z][\w:-]*)\b[^>]*>/g, (match, rawName: string) => {
|
|
380
|
+
const local = String(rawName).split(':').pop()?.toLowerCase() ?? '';
|
|
381
|
+
return ANNOTATION_SVG_ELEMENTS.has(local) ? match : '';
|
|
382
|
+
})
|
|
383
|
+
// 3. Attribute denylist on the surviving allowlisted elements — the legit
|
|
384
|
+
// vocabulary uses no on*= / style= / *href=, so stripping them closes
|
|
385
|
+
// inline handlers, CSS url(javascript:), and entity-encoded hrefs.
|
|
386
|
+
// The leading boundary is a LOOKBEHIND on whitespace / quote / slash
|
|
387
|
+
// (not a consumed `\s`) so a handler glued to the previous attribute's
|
|
388
|
+
// closing quote — `<circle r="2"onload="…"/>`, which HTML/SVG parsers
|
|
389
|
+
// accept as a distinct attribute — is also stripped (Phase 24 security
|
|
390
|
+
// review, DDR-067). The non-consuming lookbehind leaves the preceding
|
|
391
|
+
// quote intact so the legit attribute it belonged to survives.
|
|
392
|
+
.replace(
|
|
393
|
+
/(?<=[\s"'/])(?:on[a-z]+|style|(?:[\w-]+:)?href)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi,
|
|
394
|
+
''
|
|
395
|
+
);
|
|
396
|
+
// Post-pass — restore the validated assets href on <image>. Re-validate the
|
|
397
|
+
// marker value (defense-in-depth: a forged data-mdcc-asset can only resolve to
|
|
398
|
+
// a safe same-origin assets path; anything with a scheme/traversal is dropped).
|
|
399
|
+
return cleaned.replace(/\sdata-mdcc-asset\s*=\s*"([^"]*)"/gi, (_whole, val: string) =>
|
|
400
|
+
ASSET_IMAGE_HREF_RE.test(val) ? ` href="${val}"` : ''
|
|
258
401
|
);
|
|
259
402
|
}
|
|
260
403
|
|
|
@@ -471,6 +614,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
471
614
|
id: newCommentId(),
|
|
472
615
|
file: payload.file,
|
|
473
616
|
selector: String(payload.selector || ''),
|
|
617
|
+
index: typeof payload.index === 'number' ? payload.index : undefined,
|
|
474
618
|
dom_path: Array.isArray(payload.dom_path) ? payload.dom_path.slice(0, 16) : [],
|
|
475
619
|
tag: String(payload.tag || ''),
|
|
476
620
|
classes: String(payload.classes || ''),
|
|
@@ -712,6 +856,276 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
712
856
|
return true;
|
|
713
857
|
}
|
|
714
858
|
|
|
859
|
+
// Phase 23 — content-addressed asset write. Reachable from the (potentially
|
|
860
|
+
// untrusted, DDR-054) canvas origin, so every cap is load-bearing, NOT
|
|
861
|
+
// optional (DDR Task 9):
|
|
862
|
+
// • magic-byte sniff → true type ∈ {png,jpg,gif,webp}; a header lie or an
|
|
863
|
+
// SVG (script-bearing vector) is rejected — bytes decide, name is ignored.
|
|
864
|
+
// • ≤ 10 MB ceiling (assets get their OWN cap; never routed through the 1 MB
|
|
865
|
+
// SVG-text gate in saveAnnotations).
|
|
866
|
+
// • content-addressed name `assets/<sha8-of-bytes>.<ext>` → identical drops
|
|
867
|
+
// dedupe → a malicious canvas can't fill the disk with N copies of one
|
|
868
|
+
// image, and orphan-on-delete is safe (shared content survives).
|
|
869
|
+
// • resolved-path containment assert (defense-in-depth; the name carries no
|
|
870
|
+
// user input, but a poisoned designRoot must still not escape).
|
|
871
|
+
// Running total of bytes this server instance has actually written (post-dedupe).
|
|
872
|
+
let assetBytesWritten = 0;
|
|
873
|
+
async function saveAsset(bytes: Uint8Array): Promise<SaveAssetResult> {
|
|
874
|
+
if (!bytes || bytes.length === 0) return { ok: false, status: 400, error: 'empty body' };
|
|
875
|
+
if (bytes.length > ASSET_MAX_BYTES) {
|
|
876
|
+
return { ok: false, status: 413, error: 'asset exceeds the 10 MB cap' };
|
|
877
|
+
}
|
|
878
|
+
const kind = sniffImageType(bytes);
|
|
879
|
+
if (!kind) {
|
|
880
|
+
return {
|
|
881
|
+
ok: false,
|
|
882
|
+
status: 415,
|
|
883
|
+
error: 'unsupported image type — png/jpeg/gif/webp only (SVG rejected)',
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
const sha8 = crypto.createHash('sha256').update(bytes).digest('hex').slice(0, 8);
|
|
887
|
+
const name = `${sha8}.${kind}`;
|
|
888
|
+
const assetsDir = path.join(paths.designRoot, 'assets');
|
|
889
|
+
const fileAbs = path.join(assetsDir, name);
|
|
890
|
+
// Containment backstop — the name is content-addressed (sha8 hex + sniffed
|
|
891
|
+
// ext), so there is no user-controlled path segment, but assert anyway.
|
|
892
|
+
const resolved = path.resolve(fileAbs);
|
|
893
|
+
const assetsResolved = path.resolve(assetsDir);
|
|
894
|
+
if (resolved !== path.join(assetsResolved, name)) {
|
|
895
|
+
return { ok: false, status: 400, error: 'resolved asset path escapes assets dir' };
|
|
896
|
+
}
|
|
897
|
+
try {
|
|
898
|
+
// Dedupe — identical bytes hash to the same name; skip the write if present.
|
|
899
|
+
if (!(await Bun.file(fileAbs).exists())) {
|
|
900
|
+
// Aggregate write budget (DDR-088 follow-up) — bounds a scripted
|
|
901
|
+
// one-byte-mutation disk-fill loop from the untrusted canvas origin.
|
|
902
|
+
// Only a genuinely NEW file counts (a dedupe hit is free).
|
|
903
|
+
if (assetBytesWritten + bytes.length > ASSET_SESSION_BUDGET) {
|
|
904
|
+
return {
|
|
905
|
+
ok: false,
|
|
906
|
+
status: 429,
|
|
907
|
+
error: 'asset write budget exceeded for this server session',
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
await mkdir(assetsDir, { recursive: true });
|
|
911
|
+
await Bun.write(fileAbs, bytes);
|
|
912
|
+
assetBytesWritten += bytes.length;
|
|
913
|
+
}
|
|
914
|
+
} catch (err) {
|
|
915
|
+
return { ok: false, status: 500, error: err instanceof Error ? err.message : 'write failed' };
|
|
916
|
+
}
|
|
917
|
+
return { ok: true, path: `assets/${name}` };
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Phase 22 — create a blank brief board from the browser file tree. Wired ONLY
|
|
921
|
+
// on the main origin (server.ts startMainServer); the segregated canvas origin
|
|
922
|
+
// (DDR-054) never exposes this — an untrusted canvas iframe must not be able to
|
|
923
|
+
// write arbitrary `.tsx` files. The single user-controlled value (`name`) is
|
|
924
|
+
// gated by `validateCanvasName` (path + JSX + JSON injection boundary); `group`
|
|
925
|
+
// is allowlisted to the configured canvas groups; a `resolve()` containment
|
|
926
|
+
// assert is the defense-in-depth backstop.
|
|
927
|
+
async function createCanvas(input: {
|
|
928
|
+
name?: unknown;
|
|
929
|
+
kind?: unknown;
|
|
930
|
+
group?: unknown;
|
|
931
|
+
}): Promise<CreateCanvasResult> {
|
|
932
|
+
// v1 only stamps blank boards — generation stays with `/design:new` (Claude).
|
|
933
|
+
const kind = input.kind == null || input.kind === '' ? 'brief-board' : input.kind;
|
|
934
|
+
if (kind !== 'brief-board') {
|
|
935
|
+
return { ok: false, status: 400, error: 'only kind "brief-board" is supported' };
|
|
936
|
+
}
|
|
937
|
+
const v = validateCanvasName(input.name);
|
|
938
|
+
if (!v.ok || !v.name || !v.componentName) {
|
|
939
|
+
return { ok: false, status: 400, error: v.error ?? 'invalid name' };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Group must be one of the configured canvas groups (+ the new-canvas dir).
|
|
943
|
+
const allowed = new Set<string>(cfg.canvasGroups.map((g) => g.path));
|
|
944
|
+
allowed.add(cfg.newCanvasDir || 'ui');
|
|
945
|
+
const group =
|
|
946
|
+
input.group == null || input.group === '' ? cfg.newCanvasDir || 'ui' : String(input.group);
|
|
947
|
+
if (!allowed.has(group)) {
|
|
948
|
+
return { ok: false, status: 400, error: `group must be one of: ${[...allowed].join(', ')}` };
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const groupAbs = path.join(paths.designRoot, group);
|
|
952
|
+
const fileAbs = path.join(groupAbs, `${v.name}.tsx`);
|
|
953
|
+
// Containment backstop, two layers (Phase 22 security review F2). The name
|
|
954
|
+
// regex already forbids traversal, but `group` comes from config
|
|
955
|
+
// (canvasGroups[].path / newCanvasDir), which `normalizeConfig` does NOT
|
|
956
|
+
// validate for `..`. So assert (1) the GROUP resolves inside designRoot —
|
|
957
|
+
// a config-supplied `../../etc` must not relocate the write out of the
|
|
958
|
+
// project — and (2) the file resolves inside that group. Without (1) a
|
|
959
|
+
// poisoned config would be an arbitrary-directory `.tsx` write (latent today
|
|
960
|
+
// because config isn't sync-scoped; closed now so a future config-sync can't
|
|
961
|
+
// re-open it).
|
|
962
|
+
const resolvedDesignRoot = path.resolve(paths.designRoot);
|
|
963
|
+
const resolvedGroup = path.resolve(groupAbs);
|
|
964
|
+
if (
|
|
965
|
+
resolvedGroup !== resolvedDesignRoot &&
|
|
966
|
+
!resolvedGroup.startsWith(`${resolvedDesignRoot}${path.sep}`)
|
|
967
|
+
) {
|
|
968
|
+
return { ok: false, status: 400, error: 'canvas group resolves outside the design root' };
|
|
969
|
+
}
|
|
970
|
+
if (
|
|
971
|
+
path.resolve(fileAbs) !== path.join(resolvedGroup, `${v.name}.tsx`) ||
|
|
972
|
+
!path.resolve(fileAbs).startsWith(`${resolvedGroup}${path.sep}`)
|
|
973
|
+
) {
|
|
974
|
+
return { ok: false, status: 400, error: 'resolved path escapes the canvas group' };
|
|
975
|
+
}
|
|
976
|
+
if (await Bun.file(fileAbs).exists()) {
|
|
977
|
+
return {
|
|
978
|
+
ok: false,
|
|
979
|
+
status: 409,
|
|
980
|
+
error: `a canvas named "${v.name}" already exists in ${group}`,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const rel = path.posix.join(group, `${v.name}.tsx`);
|
|
985
|
+
const slug = fileSlug(rel);
|
|
986
|
+
const dsName = cfg.defaultDesignSystem || 'project';
|
|
987
|
+
const platform = 'desktop';
|
|
988
|
+
const tsx = renderBriefBoard({
|
|
989
|
+
name: v.name,
|
|
990
|
+
componentName: v.componentName,
|
|
991
|
+
dsName,
|
|
992
|
+
platform,
|
|
993
|
+
seedHint: 'Empty brief board — annotate me',
|
|
994
|
+
historyDir: path.posix.join(paths.designRel, '_history', slug),
|
|
995
|
+
});
|
|
996
|
+
const now = new Date().toISOString();
|
|
997
|
+
const meta = {
|
|
998
|
+
kind: 'brief-board',
|
|
999
|
+
title: v.name,
|
|
1000
|
+
subtitle: 'brief board',
|
|
1001
|
+
brief: v.name,
|
|
1002
|
+
designSystem: dsName,
|
|
1003
|
+
platform,
|
|
1004
|
+
created: now,
|
|
1005
|
+
last_modified: now,
|
|
1006
|
+
};
|
|
1007
|
+
await Bun.write(fileAbs, tsx);
|
|
1008
|
+
await Bun.write(
|
|
1009
|
+
path.join(groupAbs, `${v.name}.meta.json`),
|
|
1010
|
+
`${JSON.stringify(meta, null, 2)}\n`
|
|
1011
|
+
);
|
|
1012
|
+
// designRel-prefixed path — matches the file-tree `file.path` shape so the
|
|
1013
|
+
// client can open it directly after reloadTree().
|
|
1014
|
+
return { ok: true, file: path.posix.join(paths.designRel, rel), rel, slug };
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Phase 22 — SOFT-delete a canvas (DELETE /_api/canvas). Same trust boundary as
|
|
1018
|
+
// createCanvas: main-origin-only, never the untrusted canvas iframe origin
|
|
1019
|
+
// (DDR-054). Destructive, so it MOVES the whole sidecar set to
|
|
1020
|
+
// `<designRoot>/_trash/<stamp>__<slug>/` (recoverable locally) instead of a hard
|
|
1021
|
+
// rm — pairs with the existing `_history/` + /design:rollback model. Hub
|
|
1022
|
+
// propagation of the delete is deliberately out of scope (Phase 26 consent).
|
|
1023
|
+
async function deleteCanvas(input: { file?: unknown }): Promise<DeleteCanvasResult> {
|
|
1024
|
+
const raw = input?.file;
|
|
1025
|
+
if (typeof raw !== 'string' || !raw.trim()) {
|
|
1026
|
+
return { ok: false, status: 400, error: 'file is required' };
|
|
1027
|
+
}
|
|
1028
|
+
let rel = raw.trim();
|
|
1029
|
+
try {
|
|
1030
|
+
rel = decodeURIComponent(rel);
|
|
1031
|
+
} catch {
|
|
1032
|
+
/* leave as-is */
|
|
1033
|
+
}
|
|
1034
|
+
rel = rel.replace(/^\/+/, '');
|
|
1035
|
+
const prefix = `${paths.designRel.replace(/^\/+|\/+$/g, '')}/`;
|
|
1036
|
+
if (rel.startsWith(prefix)) rel = rel.slice(prefix.length);
|
|
1037
|
+
|
|
1038
|
+
// Only a real `.tsx` canvas, no traversal.
|
|
1039
|
+
if (rel.includes('..')) return { ok: false, status: 400, error: 'invalid path' };
|
|
1040
|
+
if (!/\.tsx$/i.test(rel)) {
|
|
1041
|
+
return { ok: false, status: 400, error: 'only .tsx canvases can be deleted' };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const fileAbs = path.join(paths.designRoot, rel);
|
|
1045
|
+
const resolvedDesignRoot = path.resolve(paths.designRoot);
|
|
1046
|
+
const resolvedFile = path.resolve(fileAbs);
|
|
1047
|
+
if (
|
|
1048
|
+
resolvedFile !== resolvedDesignRoot &&
|
|
1049
|
+
!resolvedFile.startsWith(`${resolvedDesignRoot}${path.sep}`)
|
|
1050
|
+
) {
|
|
1051
|
+
return { ok: false, status: 400, error: 'path escapes the design root' };
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Must live under a NON-DS canvas group — never the design system (`system`),
|
|
1055
|
+
// never a loose root file (config.json, README). Deleting DS sources is not a
|
|
1056
|
+
// canvas operation.
|
|
1057
|
+
const deletable = cfg.canvasGroups.filter(
|
|
1058
|
+
(g) => g.label !== 'Design system' && !/^system(\/|$)/.test(g.path)
|
|
1059
|
+
);
|
|
1060
|
+
const inGroup = deletable.some((g) => {
|
|
1061
|
+
const gAbs = path.resolve(path.join(paths.designRoot, g.path));
|
|
1062
|
+
return resolvedFile.startsWith(`${gAbs}${path.sep}`);
|
|
1063
|
+
});
|
|
1064
|
+
if (!inGroup) {
|
|
1065
|
+
return {
|
|
1066
|
+
ok: false,
|
|
1067
|
+
status: 400,
|
|
1068
|
+
error: 'only canvases under a managed canvas group can be deleted',
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (!(await Bun.file(fileAbs).exists())) {
|
|
1073
|
+
return { ok: false, status: 404, error: 'canvas not found' };
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const slug = fileSlug(rel);
|
|
1077
|
+
const base = path.basename(rel).replace(/\.tsx$/i, '');
|
|
1078
|
+
const groupDir = path.dirname(fileAbs);
|
|
1079
|
+
// Filesystem-safe wall-clock stamp (no `:`) so repeated deletes of the same
|
|
1080
|
+
// canvas don't collide in _trash.
|
|
1081
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1082
|
+
const trashDir = path.join(paths.designRoot, '_trash', `${stamp}__${slug}`);
|
|
1083
|
+
await mkdir(trashDir, { recursive: true });
|
|
1084
|
+
|
|
1085
|
+
const trashed: string[] = [];
|
|
1086
|
+
const moveIfExists = async (src: string, destName: string) => {
|
|
1087
|
+
try {
|
|
1088
|
+
await statp(src); // throws if absent (works for files AND dirs)
|
|
1089
|
+
} catch {
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
try {
|
|
1093
|
+
await rename(src, path.join(trashDir, destName));
|
|
1094
|
+
trashed.push(path.relative(paths.designRoot, src));
|
|
1095
|
+
} catch {
|
|
1096
|
+
/* best-effort — a sidecar that can't move shouldn't abort the delete */
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
// Primary + the full sidecar set (annotations, meta, history, canvas-state,
|
|
1101
|
+
// comments). Flattened names so the trash dir is a self-contained bundle.
|
|
1102
|
+
await moveIfExists(fileAbs, `${base}.tsx`);
|
|
1103
|
+
await moveIfExists(path.join(groupDir, `${base}.meta.json`), `${base}.meta.json`);
|
|
1104
|
+
await moveIfExists(
|
|
1105
|
+
path.join(paths.designRoot, `${slug}.annotations.svg`),
|
|
1106
|
+
`${slug}.annotations.svg`
|
|
1107
|
+
);
|
|
1108
|
+
await moveIfExists(path.join(paths.designRoot, '_history', slug), `_history__${slug}`);
|
|
1109
|
+
await moveIfExists(
|
|
1110
|
+
path.join(paths.canvasStateDir, `${slug}.json`),
|
|
1111
|
+
`_canvas-state__${slug}.json`
|
|
1112
|
+
);
|
|
1113
|
+
await moveIfExists(path.join(paths.commentsDir, `${slug}.json`), `_comments__${slug}.json`);
|
|
1114
|
+
|
|
1115
|
+
await Bun.write(
|
|
1116
|
+
path.join(trashDir, '_trash-manifest.json'),
|
|
1117
|
+
`${JSON.stringify({ canvas: rel, slug, deletedAt: new Date().toISOString(), trashed }, null, 2)}\n`
|
|
1118
|
+
);
|
|
1119
|
+
|
|
1120
|
+
return {
|
|
1121
|
+
ok: true,
|
|
1122
|
+
rel,
|
|
1123
|
+
slug,
|
|
1124
|
+
trashed,
|
|
1125
|
+
trashDir: path.relative(paths.repoRoot, trashDir),
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
715
1129
|
async function saveCanvasState(file: string, state: Record<string, unknown>) {
|
|
716
1130
|
if (!state || typeof state !== 'object') return;
|
|
717
1131
|
const safe: Record<string, unknown> = {};
|
|
@@ -732,6 +1146,28 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
732
1146
|
async function buildIndexData() {
|
|
733
1147
|
const groups = [];
|
|
734
1148
|
|
|
1149
|
+
// DDR-093 — per-canvas design-system map (repo-relative canvas path → DS
|
|
1150
|
+
// name) so the client's canvasUrl() injects each UI canvas's OWN tokens
|
|
1151
|
+
// instead of unconditionally designSystems[0]. Populated from the canvas
|
|
1152
|
+
// groups below; returned on the payload and folded into the client cfg.
|
|
1153
|
+
const canvasDesignSystems: Record<string, string> = {};
|
|
1154
|
+
const defaultDs = cfg.defaultDesignSystem || cfg.designSystems?.[0]?.name || 'project';
|
|
1155
|
+
// A file under `system/<folder>/` belongs to the DS that owns that folder —
|
|
1156
|
+
// path-authoritative, because specimens/ui_kits rarely carry a sidecar
|
|
1157
|
+
// `designSystem`. Returns null for `ui/` canvases (resolved via sidecar).
|
|
1158
|
+
const ownerDsName = (repoRelPath: string): string | null => {
|
|
1159
|
+
let r = repoRelPath;
|
|
1160
|
+
const prefix = `${paths.designRel}/`;
|
|
1161
|
+
if (r.startsWith(prefix)) r = r.slice(prefix.length);
|
|
1162
|
+
const m = r.match(/^system\/([^/]+)\//);
|
|
1163
|
+
if (!m) return null;
|
|
1164
|
+
const folder = m[1];
|
|
1165
|
+
const owner = (cfg.designSystems ?? []).find(
|
|
1166
|
+
(d) => d.path === `system/${folder}` || d.path.endsWith(`/${folder}`)
|
|
1167
|
+
);
|
|
1168
|
+
return owner?.name ?? null;
|
|
1169
|
+
};
|
|
1170
|
+
|
|
735
1171
|
// PROJECT — top-level .design/ files (README.md, INDEX.md, config.json, …).
|
|
736
1172
|
// These are non-HTML so they're listed but not openable in the iframe; the
|
|
737
1173
|
// sidebar can still display them as context (mirrors CV-08 mock).
|
|
@@ -777,6 +1213,20 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
777
1213
|
const filePaths = isDs
|
|
778
1214
|
? await findFiles(groupAbs, groupRel, ['.tsx', '.html', '.md', '.css', '.json'])
|
|
779
1215
|
: await findFiles(groupAbs, groupRel, ['.tsx', '.html', '.css', '.json']);
|
|
1216
|
+
// DDR-093 — record each `.tsx` canvas's design system. canvasUrl() only
|
|
1217
|
+
// injects tokens for `.tsx`, so skip everything else. Path-owned DS wins
|
|
1218
|
+
// (system/<ds>/…); otherwise the sidecar's `meta.designSystem`, defaulting
|
|
1219
|
+
// to the project default when the canvas declares none.
|
|
1220
|
+
for (const fp of filePaths) {
|
|
1221
|
+
if (!fp.endsWith('.tsx')) continue;
|
|
1222
|
+
let dsName = ownerDsName(fp);
|
|
1223
|
+
if (!dsName) {
|
|
1224
|
+
const meta = await loadCanvasMeta(fp);
|
|
1225
|
+
const declared = meta?.designSystem;
|
|
1226
|
+
dsName = typeof declared === 'string' && declared.trim() ? declared : defaultDs;
|
|
1227
|
+
}
|
|
1228
|
+
canvasDesignSystems[fp] = dsName;
|
|
1229
|
+
}
|
|
780
1230
|
// Strip down to `g.path` so per-DS folders (`project`, `beta`, …) show
|
|
781
1231
|
// up as the top-level dirs inside the DS section. Single-DS configs get
|
|
782
1232
|
// a wrapper folder too — slightly more verbose, but consistent with
|
|
@@ -837,6 +1287,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
837
1287
|
projectLabel: ctx.projectLabel,
|
|
838
1288
|
designRoot: paths.designRel,
|
|
839
1289
|
groups,
|
|
1290
|
+
canvasDesignSystems,
|
|
840
1291
|
};
|
|
841
1292
|
}
|
|
842
1293
|
|
|
@@ -1078,6 +1529,9 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1078
1529
|
patchCanvasMeta,
|
|
1079
1530
|
loadAnnotations,
|
|
1080
1531
|
saveAnnotations,
|
|
1532
|
+
saveAsset,
|
|
1533
|
+
createCanvas,
|
|
1534
|
+
deleteCanvas,
|
|
1081
1535
|
buildIndexData,
|
|
1082
1536
|
buildSystemData,
|
|
1083
1537
|
loadExportHistory,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file artboard-activity-overlay.tsx — Phase 13 / DDR-029 overlay.
|
|
3
|
-
* @scope
|
|
3
|
+
* @scope apps/studio/artboard-activity-overlay.tsx
|
|
4
4
|
* @purpose The "agent works here" chrome for a single artboard: an animated
|
|
5
5
|
* rim + a top-right `editing — <label>` badge. Rendered by
|
|
6
6
|
* `DCArtboard` as a world-coord sibling of the `<article>`, so it
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file artboard-marquee.tsx — Task 24 sub-item (Wave 2 follow-up, G8)
|
|
3
|
-
* @scope
|
|
3
|
+
* @scope apps/studio/artboard-marquee.tsx
|
|
4
4
|
* @purpose Drag-to-lasso multi-select for artboards. Bare-button pointerdown
|
|
5
5
|
* on empty world (NOT inside any artboard, NOT on floating chrome)
|
|
6
6
|
* starts a marquee. 4 px drag threshold suppresses click jitter.
|