@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
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// /_api/canvas — Phase 22 UI add-on. Create a blank brief board from the browser.
|
|
2
|
+
//
|
|
3
|
+
// Two layers:
|
|
4
|
+
// (1) the pure name-validation boundary (the ONE security gate — path +
|
|
5
|
+
// JSX + JSON injection), tested directly;
|
|
6
|
+
// (2) the POST endpoint round-trip (boots a real server against a sandbox),
|
|
7
|
+
// covering happy path, the rejection matrix, group allowlist, duplicate.
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from 'bun:test';
|
|
10
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
import { componentNameFrom, renderBriefBoard, validateCanvasName } from '../canvas-create.ts';
|
|
14
|
+
import { bootServer, killProc, makeSandbox, nextPort } from './_helpers.ts';
|
|
15
|
+
|
|
16
|
+
describe('canvas-create / validateCanvasName (the security boundary)', () => {
|
|
17
|
+
test('accepts ordinary names (incl. spaces + accents)', () => {
|
|
18
|
+
for (const n of ['Onboarding Brief', 'checkout-flow', 'Krok_1', 'Přihlášení', 'A1']) {
|
|
19
|
+
const v = validateCanvasName(n);
|
|
20
|
+
expect(v.ok).toBe(true);
|
|
21
|
+
expect(v.name).toBe(n);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('trims surrounding whitespace', () => {
|
|
26
|
+
const v = validateCanvasName(' Onboarding ');
|
|
27
|
+
expect(v.ok).toBe(true);
|
|
28
|
+
expect(v.name).toBe('Onboarding');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// The rejection matrix — path traversal, separators, and template/JSON
|
|
32
|
+
// injection vectors must ALL be refused by the one allowlist regex.
|
|
33
|
+
const REJECT: Array<[string, unknown]> = [
|
|
34
|
+
['empty', ''],
|
|
35
|
+
['whitespace only', ' '],
|
|
36
|
+
['non-string', 123],
|
|
37
|
+
['null', null],
|
|
38
|
+
['parent traversal', '../evil'],
|
|
39
|
+
['nested traversal', 'a/../../etc/passwd'],
|
|
40
|
+
['forward slash', 'ui/Sneaky'],
|
|
41
|
+
['back slash', 'ui\\Sneaky'],
|
|
42
|
+
['leading dot', '.hidden'],
|
|
43
|
+
['dot-dot', '..'],
|
|
44
|
+
['JSX break-out', 'X</div><script>alert(1)</script>'],
|
|
45
|
+
['brace injection', 'A{2+2}B'],
|
|
46
|
+
['angle bracket', 'a<b'],
|
|
47
|
+
['double quote (JSON break-out)', 'a"b'],
|
|
48
|
+
['backslash (JSON break-out)', 'a\\b'],
|
|
49
|
+
['leading hyphen', '-dash'],
|
|
50
|
+
['too long (61)', 'x'.repeat(61)],
|
|
51
|
+
['tab', 'a\tb'],
|
|
52
|
+
['newline', 'a\nb'],
|
|
53
|
+
['null byte', 'a\u0000b'],
|
|
54
|
+
];
|
|
55
|
+
for (const [label, value] of REJECT) {
|
|
56
|
+
test(`rejects ${label}`, () => {
|
|
57
|
+
expect(validateCanvasName(value).ok).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test('componentNameFrom yields a valid identifier', () => {
|
|
62
|
+
expect(componentNameFrom('Onboarding Brief')).toBe('OnboardingBrief');
|
|
63
|
+
expect(componentNameFrom('checkout-flow v2')).toBe('CheckoutFlowV2');
|
|
64
|
+
expect(componentNameFrom('123')).toMatch(/^Board/); // digit-leading → prefixed
|
|
65
|
+
// Unicode letters survive (review #1) — not ASCII-mangled to "PIhlEn".
|
|
66
|
+
expect(componentNameFrom('Přihlášení')).toBe('Přihlášení');
|
|
67
|
+
expect(componentNameFrom('123abc')).toBe('Board123abc'); // digit-leading → prefixed, body kept
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('collapses internal double-spaces so the slug methods agree (review #4)', () => {
|
|
71
|
+
const v = validateCanvasName('Two Spaces');
|
|
72
|
+
expect(v.ok).toBe(true);
|
|
73
|
+
expect(v.name).toBe('Two Spaces'); // " +" → " " ; a tab would still be rejected
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('renderBriefBoard substitutes every placeholder (no {{…}} left)', () => {
|
|
77
|
+
const out = renderBriefBoard({
|
|
78
|
+
name: 'Onboarding Brief',
|
|
79
|
+
componentName: 'OnboardingBrief',
|
|
80
|
+
dsName: 'project',
|
|
81
|
+
platform: 'desktop',
|
|
82
|
+
seedHint: 'seed',
|
|
83
|
+
historyDir: '.design/_history/ui-onboarding_brief',
|
|
84
|
+
});
|
|
85
|
+
expect(out).not.toMatch(/\{\{[A-Z_]+\}\}/);
|
|
86
|
+
expect(out).toContain('export default function OnboardingBrief()');
|
|
87
|
+
expect(out).toContain('@kind brief-board');
|
|
88
|
+
expect(out).toContain('Onboarding Brief');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('/_api/canvas — POST round-trip', () => {
|
|
93
|
+
test('creates a brief board (.tsx + .meta.json), returns 201 + the tree path', async () => {
|
|
94
|
+
const { root, designRoot } = makeSandbox();
|
|
95
|
+
const port = nextPort();
|
|
96
|
+
const proc = await bootServer(root, port);
|
|
97
|
+
try {
|
|
98
|
+
const r = await fetch(`http://localhost:${port}/_api/canvas`, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
101
|
+
body: JSON.stringify({ name: 'Onboarding Brief', kind: 'brief-board' }),
|
|
102
|
+
});
|
|
103
|
+
expect(r.status).toBe(201);
|
|
104
|
+
const j = (await r.json()) as { ok: boolean; file: string; rel: string; slug: string };
|
|
105
|
+
expect(j.ok).toBe(true);
|
|
106
|
+
expect(j.file).toBe('.design/ui/Onboarding Brief.tsx');
|
|
107
|
+
expect(j.rel).toBe('ui/Onboarding Brief.tsx');
|
|
108
|
+
expect(j.slug).toBe('ui-onboarding_brief');
|
|
109
|
+
|
|
110
|
+
const tsx = join(designRoot, 'ui', 'Onboarding Brief.tsx');
|
|
111
|
+
const meta = join(designRoot, 'ui', 'Onboarding Brief.meta.json');
|
|
112
|
+
expect(existsSync(tsx)).toBe(true);
|
|
113
|
+
expect(existsSync(meta)).toBe(true);
|
|
114
|
+
expect(readFileSync(tsx, 'utf8')).toContain('export default function OnboardingBrief()');
|
|
115
|
+
expect(readFileSync(tsx, 'utf8')).toContain('Drop sticky notes');
|
|
116
|
+
const m = JSON.parse(readFileSync(meta, 'utf8'));
|
|
117
|
+
expect(m.kind).toBe('brief-board');
|
|
118
|
+
expect(m.title).toBe('Onboarding Brief');
|
|
119
|
+
expect(m.brief_sha).toBeUndefined(); // a board has no generation brief
|
|
120
|
+
} finally {
|
|
121
|
+
await killProc(proc);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('defaults kind to brief-board when omitted', async () => {
|
|
126
|
+
const { root } = makeSandbox();
|
|
127
|
+
const port = nextPort();
|
|
128
|
+
const proc = await bootServer(root, port);
|
|
129
|
+
try {
|
|
130
|
+
const r = await fetch(`http://localhost:${port}/_api/canvas`, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: { 'Content-Type': 'application/json' },
|
|
133
|
+
body: JSON.stringify({ name: 'No Kind' }),
|
|
134
|
+
});
|
|
135
|
+
expect(r.status).toBe(201);
|
|
136
|
+
} finally {
|
|
137
|
+
await killProc(proc);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('rejects a traversal name with 400 and writes nothing', async () => {
|
|
142
|
+
const { root, designRoot } = makeSandbox();
|
|
143
|
+
const port = nextPort();
|
|
144
|
+
const proc = await bootServer(root, port);
|
|
145
|
+
try {
|
|
146
|
+
const r = await fetch(`http://localhost:${port}/_api/canvas`, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: { 'Content-Type': 'application/json' },
|
|
149
|
+
body: JSON.stringify({ name: '../../escape' }),
|
|
150
|
+
});
|
|
151
|
+
expect(r.status).toBe(400);
|
|
152
|
+
// No file landed anywhere near the design root's parent.
|
|
153
|
+
expect(existsSync(join(designRoot, '..', 'escape.tsx'))).toBe(false);
|
|
154
|
+
} finally {
|
|
155
|
+
await killProc(proc);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('rejects an unknown group with 400', async () => {
|
|
160
|
+
const { root } = makeSandbox();
|
|
161
|
+
const port = nextPort();
|
|
162
|
+
const proc = await bootServer(root, port);
|
|
163
|
+
try {
|
|
164
|
+
const r = await fetch(`http://localhost:${port}/_api/canvas`, {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: { 'Content-Type': 'application/json' },
|
|
167
|
+
body: JSON.stringify({ name: 'Valid', group: 'etc' }),
|
|
168
|
+
});
|
|
169
|
+
expect(r.status).toBe(400);
|
|
170
|
+
} finally {
|
|
171
|
+
await killProc(proc);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Phase 22 security review F2 — a config whose newCanvasDir TRAVERSES out of
|
|
176
|
+
// the design root must NOT let the write escape, even though the traversing
|
|
177
|
+
// group is technically "allowlisted" (it came from config). The designRoot
|
|
178
|
+
// containment assert is the backstop.
|
|
179
|
+
test('a config with a traversing newCanvasDir cannot escape the design root', async () => {
|
|
180
|
+
const { root, designRoot } = makeSandbox();
|
|
181
|
+
// Poison the config: newCanvasDir points outside the design root.
|
|
182
|
+
writeFileSync(
|
|
183
|
+
join(designRoot, 'config.json'),
|
|
184
|
+
JSON.stringify({
|
|
185
|
+
name: 'test',
|
|
186
|
+
designRoot: '.design',
|
|
187
|
+
newCanvasDir: '../../../../tmp',
|
|
188
|
+
canvasGroups: [
|
|
189
|
+
{ label: 'System', path: 'system' },
|
|
190
|
+
{ label: 'UI', path: 'ui' },
|
|
191
|
+
],
|
|
192
|
+
})
|
|
193
|
+
);
|
|
194
|
+
const port = nextPort();
|
|
195
|
+
const proc = await bootServer(root, port);
|
|
196
|
+
try {
|
|
197
|
+
const r = await fetch(`http://localhost:${port}/_api/canvas`, {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
headers: { 'Content-Type': 'application/json' },
|
|
200
|
+
body: JSON.stringify({ name: 'Escape' }),
|
|
201
|
+
});
|
|
202
|
+
expect(r.status).toBe(400);
|
|
203
|
+
// Nothing was written up at the traversal target.
|
|
204
|
+
expect(existsSync(join(root, '..', '..', 'tmp', 'Escape.tsx'))).toBe(false);
|
|
205
|
+
} finally {
|
|
206
|
+
await killProc(proc);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('rejects a non-brief-board kind with 400', async () => {
|
|
211
|
+
const { root } = makeSandbox();
|
|
212
|
+
const port = nextPort();
|
|
213
|
+
const proc = await bootServer(root, port);
|
|
214
|
+
try {
|
|
215
|
+
const r = await fetch(`http://localhost:${port}/_api/canvas`, {
|
|
216
|
+
method: 'POST',
|
|
217
|
+
headers: { 'Content-Type': 'application/json' },
|
|
218
|
+
body: JSON.stringify({ name: 'Valid', kind: 'canvas' }),
|
|
219
|
+
});
|
|
220
|
+
expect(r.status).toBe(400);
|
|
221
|
+
} finally {
|
|
222
|
+
await killProc(proc);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('a duplicate name returns 409', async () => {
|
|
227
|
+
const { root } = makeSandbox();
|
|
228
|
+
const port = nextPort();
|
|
229
|
+
const proc = await bootServer(root, port);
|
|
230
|
+
try {
|
|
231
|
+
const body = JSON.stringify({ name: 'Dup Board' });
|
|
232
|
+
const first = await fetch(`http://localhost:${port}/_api/canvas`, {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers: { 'Content-Type': 'application/json' },
|
|
235
|
+
body,
|
|
236
|
+
});
|
|
237
|
+
expect(first.status).toBe(201);
|
|
238
|
+
const second = await fetch(`http://localhost:${port}/_api/canvas`, {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers: { 'Content-Type': 'application/json' },
|
|
241
|
+
body,
|
|
242
|
+
});
|
|
243
|
+
expect(second.status).toBe(409);
|
|
244
|
+
} finally {
|
|
245
|
+
await killProc(proc);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('unknown method (GET) returns 405', async () => {
|
|
250
|
+
const { root } = makeSandbox();
|
|
251
|
+
const port = nextPort();
|
|
252
|
+
const proc = await bootServer(root, port);
|
|
253
|
+
try {
|
|
254
|
+
const r = await fetch(`http://localhost:${port}/_api/canvas`, { method: 'GET' });
|
|
255
|
+
expect(r.status).toBe(405);
|
|
256
|
+
} finally {
|
|
257
|
+
await killProc(proc);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('/_api/canvas — DELETE (soft-delete)', () => {
|
|
263
|
+
async function createBoard(port: number, name: string) {
|
|
264
|
+
const r = await fetch(`http://localhost:${port}/_api/canvas`, {
|
|
265
|
+
method: 'POST',
|
|
266
|
+
headers: { 'Content-Type': 'application/json' },
|
|
267
|
+
body: JSON.stringify({ name }),
|
|
268
|
+
});
|
|
269
|
+
return (await r.json()) as { file: string; slug: string };
|
|
270
|
+
}
|
|
271
|
+
const del = (port: number, file: string) =>
|
|
272
|
+
fetch(`http://localhost:${port}/_api/canvas?file=${encodeURIComponent(file)}`, {
|
|
273
|
+
method: 'DELETE',
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('soft-deletes a canvas: moves .tsx + sidecars to _trash, gone from ui/', async () => {
|
|
277
|
+
const { root, designRoot } = makeSandbox();
|
|
278
|
+
const port = nextPort();
|
|
279
|
+
const proc = await bootServer(root, port);
|
|
280
|
+
try {
|
|
281
|
+
const created = await createBoard(port, 'Trash Me');
|
|
282
|
+
// Seed an annotation sidecar to prove the whole set travels.
|
|
283
|
+
writeFileSync(
|
|
284
|
+
join(designRoot, `${created.slug}.annotations.svg`),
|
|
285
|
+
'<svg xmlns="http://www.w3.org/2000/svg" data-mdcc-annotations="1"></svg>'
|
|
286
|
+
);
|
|
287
|
+
const tsx = join(designRoot, 'ui', 'Trash Me.tsx');
|
|
288
|
+
expect(existsSync(tsx)).toBe(true);
|
|
289
|
+
|
|
290
|
+
const r = await del(port, created.file);
|
|
291
|
+
expect(r.status).toBe(200);
|
|
292
|
+
const j = (await r.json()) as { ok: boolean; trashed: string[] };
|
|
293
|
+
expect(j.ok).toBe(true);
|
|
294
|
+
expect(j.trashed.some((t) => t.endsWith('Trash Me.tsx'))).toBe(true);
|
|
295
|
+
expect(j.trashed.some((t) => t.endsWith('.annotations.svg'))).toBe(true);
|
|
296
|
+
|
|
297
|
+
// Gone from the group; present (recoverable) under _trash/ with a manifest.
|
|
298
|
+
expect(existsSync(tsx)).toBe(false);
|
|
299
|
+
expect(existsSync(join(designRoot, `${created.slug}.annotations.svg`))).toBe(false);
|
|
300
|
+
const trashDirs = readdirSync(join(designRoot, '_trash'));
|
|
301
|
+
expect(trashDirs.length).toBe(1);
|
|
302
|
+
const td = join(designRoot, '_trash', trashDirs[0] as string);
|
|
303
|
+
expect(existsSync(join(td, 'Trash Me.tsx'))).toBe(true);
|
|
304
|
+
expect(existsSync(join(td, '_trash-manifest.json'))).toBe(true);
|
|
305
|
+
} finally {
|
|
306
|
+
await killProc(proc);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('refuses to delete a design-system file (400, untouched)', async () => {
|
|
311
|
+
const { root, designRoot } = makeSandbox();
|
|
312
|
+
const port = nextPort();
|
|
313
|
+
const proc = await bootServer(root, port);
|
|
314
|
+
try {
|
|
315
|
+
mkdirSync(join(designRoot, 'system', 'project'), { recursive: true });
|
|
316
|
+
const dsFile = join(designRoot, 'system', 'project', 'Spec.tsx');
|
|
317
|
+
writeFileSync(dsFile, 'export default function S(){return null}');
|
|
318
|
+
const r = await del(port, '.design/system/project/Spec.tsx');
|
|
319
|
+
expect(r.status).toBe(400);
|
|
320
|
+
expect(existsSync(dsFile)).toBe(true);
|
|
321
|
+
} finally {
|
|
322
|
+
await killProc(proc);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('refuses a non-.tsx root file like config.json (400)', async () => {
|
|
327
|
+
const { root } = makeSandbox();
|
|
328
|
+
const port = nextPort();
|
|
329
|
+
const proc = await bootServer(root, port);
|
|
330
|
+
try {
|
|
331
|
+
const r = await del(port, '.design/config.json');
|
|
332
|
+
expect(r.status).toBe(400);
|
|
333
|
+
} finally {
|
|
334
|
+
await killProc(proc);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('a missing canvas returns 404', async () => {
|
|
339
|
+
const { root } = makeSandbox();
|
|
340
|
+
const port = nextPort();
|
|
341
|
+
const proc = await bootServer(root, port);
|
|
342
|
+
try {
|
|
343
|
+
const r = await del(port, 'ui/Nope.tsx');
|
|
344
|
+
expect(r.status).toBe(404);
|
|
345
|
+
} finally {
|
|
346
|
+
await killProc(proc);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('a traversal file is rejected (400)', async () => {
|
|
351
|
+
const { root } = makeSandbox();
|
|
352
|
+
const port = nextPort();
|
|
353
|
+
const proc = await bootServer(root, port);
|
|
354
|
+
try {
|
|
355
|
+
const r = await del(port, 'ui/../../escape.tsx');
|
|
356
|
+
expect(r.status).toBe(400);
|
|
357
|
+
} finally {
|
|
358
|
+
await killProc(proc);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
});
|
|
@@ -11,6 +11,7 @@ const ALL_TOOLS = [
|
|
|
11
11
|
'hand',
|
|
12
12
|
'comment',
|
|
13
13
|
'pen',
|
|
14
|
+
'highlighter',
|
|
14
15
|
'shape',
|
|
15
16
|
'rect',
|
|
16
17
|
'ellipse',
|
|
@@ -41,6 +42,7 @@ describe('canvas-cursors / TOOL_CURSORS', () => {
|
|
|
41
42
|
hand: 'grab',
|
|
42
43
|
comment: 'crosshair',
|
|
43
44
|
pen: 'crosshair',
|
|
45
|
+
highlighter: 'crosshair',
|
|
44
46
|
shape: 'crosshair',
|
|
45
47
|
rect: 'crosshair',
|
|
46
48
|
ellipse: 'crosshair',
|
|
@@ -28,7 +28,7 @@ afterAll(async () => {
|
|
|
28
28
|
describe('canvas-lib-resolver / canvasLibPath', () => {
|
|
29
29
|
test('returns the dev-server-internal canvas-lib.tsx path', () => {
|
|
30
30
|
const p = canvasLibPath();
|
|
31
|
-
expect(p.endsWith('
|
|
31
|
+
expect(p.endsWith('apps/studio/canvas-lib.tsx')).toBe(true);
|
|
32
32
|
expect(existsSync(p)).toBe(true);
|
|
33
33
|
});
|
|
34
34
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// use-canvas-media-drop — Phase 23 pure helpers (no DOM).
|
|
2
|
+
//
|
|
3
|
+
// Covers the drop/paste dispatch matrix + the URL helpers. The http(s) scheme
|
|
4
|
+
// gate (isHttpUrl) is security-relevant — a link chip must never carry a
|
|
5
|
+
// javascript:/data: URL into window.open.
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from 'bun:test';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
anchorTextFromHtml,
|
|
11
|
+
classifyMediaPayload,
|
|
12
|
+
firstHttpUrl,
|
|
13
|
+
isHttpUrl,
|
|
14
|
+
linkDomain,
|
|
15
|
+
type MediaPayload,
|
|
16
|
+
normalizeUrl,
|
|
17
|
+
prettifyUrl,
|
|
18
|
+
} from '../use-canvas-media-drop.tsx';
|
|
19
|
+
|
|
20
|
+
const empty: MediaPayload = { files: [], uriList: '', html: '', plain: '' };
|
|
21
|
+
const fakeFile = (type: string): File => ({ type, name: 'x', size: 1 }) as unknown as File;
|
|
22
|
+
|
|
23
|
+
describe('use-canvas-media-drop / isHttpUrl (scheme gate)', () => {
|
|
24
|
+
test('accepts http + https only', () => {
|
|
25
|
+
expect(isHttpUrl('https://example.com')).toBe(true);
|
|
26
|
+
expect(isHttpUrl('http://example.com/a/b?c=1')).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
test('rejects javascript: / data: / file: / mailto: / garbage', () => {
|
|
29
|
+
expect(isHttpUrl('javascript:alert(1)')).toBe(false);
|
|
30
|
+
expect(isHttpUrl('data:text/html,<script>')).toBe(false);
|
|
31
|
+
expect(isHttpUrl('file:///etc/passwd')).toBe(false);
|
|
32
|
+
expect(isHttpUrl('mailto:a@b.com')).toBe(false);
|
|
33
|
+
expect(isHttpUrl('not a url')).toBe(false);
|
|
34
|
+
expect(isHttpUrl('')).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('use-canvas-media-drop / normalizeUrl (no scheme required)', () => {
|
|
39
|
+
test('keeps an explicit http(s) URL', () => {
|
|
40
|
+
expect(normalizeUrl('https://example.com/x')).toBe('https://example.com/x');
|
|
41
|
+
expect(normalizeUrl('http://example.com')).toBe('http://example.com');
|
|
42
|
+
});
|
|
43
|
+
test('prepends https:// to a bare domain', () => {
|
|
44
|
+
expect(normalizeUrl('example.com')).toBe('https://example.com');
|
|
45
|
+
expect(normalizeUrl('www.example.com/blog/post?x=1')).toBe(
|
|
46
|
+
'https://www.example.com/blog/post?x=1'
|
|
47
|
+
);
|
|
48
|
+
expect(normalizeUrl('sub.example.io:8080/path')).toBe('https://sub.example.io:8080/path');
|
|
49
|
+
});
|
|
50
|
+
test('rejects dangerous schemes even though it is lenient about http(s)', () => {
|
|
51
|
+
expect(normalizeUrl('javascript:alert(1)')).toBeNull();
|
|
52
|
+
expect(normalizeUrl('data:text/html,x')).toBeNull();
|
|
53
|
+
expect(normalizeUrl('file:///etc/passwd')).toBeNull();
|
|
54
|
+
expect(normalizeUrl('mailto:a@b.com')).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
test('does NOT mistake a bare decimal / non-domain for a URL', () => {
|
|
57
|
+
expect(normalizeUrl('3.5')).toBeNull();
|
|
58
|
+
expect(normalizeUrl('v1.2')).toBeNull();
|
|
59
|
+
expect(normalizeUrl('hello world')).toBeNull();
|
|
60
|
+
expect(normalizeUrl('')).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('use-canvas-media-drop / firstHttpUrl', () => {
|
|
65
|
+
test('a bare-domain line becomes an https URL (no scheme needed to paste)', () => {
|
|
66
|
+
expect(firstHttpUrl('example.com')).toBe('https://example.com');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('skips uri-list comment lines and returns the first real URL', () => {
|
|
70
|
+
expect(firstHttpUrl('# a comment\nhttps://example.com/p\nhttps://other.com')).toBe(
|
|
71
|
+
'https://example.com/p'
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
test('finds an inline URL in a prose paste', () => {
|
|
75
|
+
expect(firstHttpUrl('see https://example.com/post for details')).toBe(
|
|
76
|
+
'https://example.com/post'
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
test('returns null for non-URL text', () => {
|
|
80
|
+
expect(firstHttpUrl('just some words')).toBeNull();
|
|
81
|
+
expect(firstHttpUrl('javascript:alert(1)')).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('use-canvas-media-drop / linkDomain + prettifyUrl', () => {
|
|
86
|
+
test('linkDomain strips www.', () => {
|
|
87
|
+
expect(linkDomain('https://www.example.com/x')).toBe('example.com');
|
|
88
|
+
expect(linkDomain('https://sub.example.io')).toBe('sub.example.io');
|
|
89
|
+
});
|
|
90
|
+
test('prettifyUrl yields host + path (no protocol, no trailing slash)', () => {
|
|
91
|
+
expect(prettifyUrl('https://www.example.com/blog/post/')).toBe('example.com/blog/post');
|
|
92
|
+
expect(prettifyUrl('https://example.com/')).toBe('example.com');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('use-canvas-media-drop / anchorTextFromHtml', () => {
|
|
97
|
+
test('extracts the anchor text from a text/html payload', () => {
|
|
98
|
+
expect(anchorTextFromHtml('<a href="https://x.io">Cool Post</a>')).toBe('Cool Post');
|
|
99
|
+
});
|
|
100
|
+
test('strips inner markup and collapses whitespace', () => {
|
|
101
|
+
expect(anchorTextFromHtml('<a href="#"><b>Bold</b> title</a>')).toBe('Bold title');
|
|
102
|
+
});
|
|
103
|
+
test('returns null when there is no anchor', () => {
|
|
104
|
+
expect(anchorTextFromHtml('<div>no link</div>')).toBeNull();
|
|
105
|
+
expect(anchorTextFromHtml('')).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('use-canvas-media-drop / classifyMediaPayload (dispatch matrix)', () => {
|
|
110
|
+
test('an image file wins (even when a uri-list is also present)', () => {
|
|
111
|
+
const intent = classifyMediaPayload({
|
|
112
|
+
...empty,
|
|
113
|
+
files: [fakeFile('image/png')],
|
|
114
|
+
uriList: 'https://example.com',
|
|
115
|
+
});
|
|
116
|
+
expect(intent?.kind).toBe('image');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('a non-image file is ignored', () => {
|
|
120
|
+
expect(classifyMediaPayload({ ...empty, files: [fakeFile('application/pdf')] })).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('a URL drop becomes a link with the anchor text as the title', () => {
|
|
124
|
+
const intent = classifyMediaPayload({
|
|
125
|
+
...empty,
|
|
126
|
+
uriList: 'https://example.com/post',
|
|
127
|
+
html: '<a href="https://example.com/post">Read this</a>',
|
|
128
|
+
});
|
|
129
|
+
expect(intent).toEqual({ kind: 'link', url: 'https://example.com/post', title: 'Read this' });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('a URL drop with no anchor text falls back to the prettified URL', () => {
|
|
133
|
+
const intent = classifyMediaPayload({ ...empty, uriList: 'https://example.com/post' });
|
|
134
|
+
expect(intent).toEqual({
|
|
135
|
+
kind: 'link',
|
|
136
|
+
url: 'https://example.com/post',
|
|
137
|
+
title: 'example.com/post',
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('a bare-domain paste (no scheme) becomes a link normalized to https', () => {
|
|
142
|
+
const intent = classifyMediaPayload({ ...empty, plain: 'example.com/post' });
|
|
143
|
+
expect(intent).toEqual({
|
|
144
|
+
kind: 'link',
|
|
145
|
+
url: 'https://example.com/post',
|
|
146
|
+
title: 'example.com/post',
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('a plain-text (non-media) paste classifies as null — caller leaves it alone', () => {
|
|
151
|
+
expect(classifyMediaPayload({ ...empty, plain: 'hello world' })).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('a javascript: URL is never classified as a link', () => {
|
|
155
|
+
expect(classifyMediaPayload({ ...empty, plain: 'javascript:alert(1)' })).toBeNull();
|
|
156
|
+
expect(classifyMediaPayload({ ...empty, uriList: 'javascript:alert(1)' })).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -51,7 +51,16 @@ describe('canvas-origin gate — A1/A2 traversal + privilege containment', () =>
|
|
|
51
51
|
expect(await code('/.design/%2e%2e%2fsecret-outside.css')).toBe(403);
|
|
52
52
|
|
|
53
53
|
// (2) Privileged / out-of-scope routes stay 403 on the canvas origin.
|
|
54
|
-
|
|
54
|
+
// /_api/canvas (Phase 22 file-write) is deliberately NOT in the canvas
|
|
55
|
+
// origin's route allowlist — an untrusted canvas iframe must never create
|
|
56
|
+
// arbitrary .tsx files. It is reachable ONLY from the main origin.
|
|
57
|
+
for (const p of [
|
|
58
|
+
'/_config',
|
|
59
|
+
'/_sync-status',
|
|
60
|
+
'/_api/export',
|
|
61
|
+
'/_api/canvas',
|
|
62
|
+
'/package.json',
|
|
63
|
+
]) {
|
|
55
64
|
expect(await code(p)).toBe(403);
|
|
56
65
|
}
|
|
57
66
|
|
|
@@ -60,6 +69,13 @@ describe('canvas-origin gate — A1/A2 traversal + privilege containment', () =>
|
|
|
60
69
|
expect(await code('/_canvas-shell.html')).toBe(200);
|
|
61
70
|
// A real .tsx canvas under designRoot transpiles + serves (200).
|
|
62
71
|
expect(await code('/.design/ui/Gate.tsx')).toBe(200);
|
|
72
|
+
// Phase 23 — the capped image-upload route IS reachable from the canvas
|
|
73
|
+
// origin (it must be — drag-drop/paste/picker run inside the iframe). A GET
|
|
74
|
+
// returns 405 (method-not-allowed) which proves the route is REACHED via
|
|
75
|
+
// the canvas server's explicit `routes` allowlist — NOT 403 (gate-blocked)
|
|
76
|
+
// or 404 (fell through to file-serve). Guards the dual-allowlist invariant:
|
|
77
|
+
// CANVAS_SAFE_API + the startCanvasServer `routes` map must stay in sync.
|
|
78
|
+
expect(await code('/_api/asset')).toBe(405);
|
|
63
79
|
|
|
64
80
|
// (4) The shell carries the hardened CSP (A6).
|
|
65
81
|
const shell = await fetch(`${canvas}/_canvas-shell.html`);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// DDR-093 — canvasUrl() token-resolution. Regression guard for the
|
|
2
|
+
// "non-default-DS UI canvas renders unstyled" bug: every `ui/*.tsx` authored
|
|
3
|
+
// under a non-default design system used to load designSystems[0]'s tokens
|
|
4
|
+
// (scoped to a different `.<rootClass>` subtree) so every `var(--*)` went
|
|
5
|
+
// undefined and the canvas rendered white.
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from 'bun:test';
|
|
8
|
+
|
|
9
|
+
import { canvasUrl, urlOf } from '../client/canvas-url.js';
|
|
10
|
+
|
|
11
|
+
// Mirrors this repo's real multi-DS config: `project` is the default (ds0),
|
|
12
|
+
// `maude` is the non-default one that the bug broke.
|
|
13
|
+
const cfg = {
|
|
14
|
+
designRel: '.design',
|
|
15
|
+
designSystems: [
|
|
16
|
+
{ name: 'project', path: 'system/project', tokensCssRel: 'system/project/colors_and_type.css' },
|
|
17
|
+
{ name: 'maude', path: 'system/maude', tokensCssRel: 'system/maude/colors_and_type.css' },
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function paramsOf(url: string): URLSearchParams {
|
|
22
|
+
const qs = url.split('?')[1] ?? '';
|
|
23
|
+
return new URLSearchParams(qs);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('canvasUrl — per-canvas design-system token resolution', () => {
|
|
27
|
+
test('explicit opts.ds resolves a non-default DS canvas to its OWN tokens', () => {
|
|
28
|
+
const p = paramsOf(canvasUrl('.design/ui/Studio.tsx', cfg, { ds: 'maude' }));
|
|
29
|
+
expect(p.get('tokens')).toBe('system/maude/colors_and_type.css');
|
|
30
|
+
expect(p.get('components')).toBe('system/maude/preview/_components.css');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('RCA interface form — relative path + opts.ds', () => {
|
|
34
|
+
const p = paramsOf(canvasUrl('ui/Studio.tsx', cfg, { ds: 'maude' }));
|
|
35
|
+
expect(p.get('tokens')).toBe('system/maude/colors_and_type.css');
|
|
36
|
+
expect(p.get('components')).toBe('system/maude/preview/_components.css');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('opts.ds = the default DS resolves to ds0 tokens', () => {
|
|
40
|
+
const p = paramsOf(canvasUrl('.design/ui/Studio.tsx', cfg, { ds: 'project' }));
|
|
41
|
+
expect(p.get('tokens')).toBe('system/project/colors_and_type.css');
|
|
42
|
+
expect(p.get('components')).toBe('system/project/preview/_components.css');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('cfg.canvasDesignSystems map drives resolution when opts.ds is absent', () => {
|
|
46
|
+
const withMap = { ...cfg, canvasDesignSystems: { '.design/ui/Studio.tsx': 'maude' } };
|
|
47
|
+
const p = paramsOf(canvasUrl('.design/ui/Studio.tsx', withMap));
|
|
48
|
+
expect(p.get('tokens')).toBe('system/maude/colors_and_type.css');
|
|
49
|
+
expect(p.get('components')).toBe('system/maude/preview/_components.css');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('opts.ds wins over the map', () => {
|
|
53
|
+
const withMap = { ...cfg, canvasDesignSystems: { '.design/ui/Studio.tsx': 'maude' } };
|
|
54
|
+
const p = paramsOf(canvasUrl('.design/ui/Studio.tsx', withMap, { ds: 'project' }));
|
|
55
|
+
expect(p.get('tokens')).toBe('system/project/colors_and_type.css');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('unknown / no DS falls back to ds0 (single-DS + legacy behavior intact)', () => {
|
|
59
|
+
const p = paramsOf(canvasUrl('.design/ui/Plain.tsx', cfg, {}));
|
|
60
|
+
expect(p.get('tokens')).toBe('system/project/colors_and_type.css');
|
|
61
|
+
expect(p.get('components')).toBe('system/project/preview/_components.css');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('an unknown DS name (not in designSystems) falls back to ds0, never undefined', () => {
|
|
65
|
+
const p = paramsOf(canvasUrl('.design/ui/Ghost.tsx', cfg, { ds: 'does-not-exist' }));
|
|
66
|
+
expect(p.get('tokens')).toBe('system/project/colors_and_type.css');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('specimen paths stay path-resolved and are unaffected by the map', () => {
|
|
70
|
+
const withWrongMap = {
|
|
71
|
+
...cfg,
|
|
72
|
+
// A bogus map entry must NOT override the specimen branch.
|
|
73
|
+
canvasDesignSystems: { '.design/system/maude/preview/Buttons.tsx': 'project' },
|
|
74
|
+
};
|
|
75
|
+
const p = paramsOf(canvasUrl('.design/system/maude/preview/Buttons.tsx', withWrongMap));
|
|
76
|
+
expect(p.get('tokens')).toBe('system/maude/colors_and_type.css');
|
|
77
|
+
expect(p.get('layout')).toBe('system/maude/preview/_layout.css');
|
|
78
|
+
expect(p.get('components')).toBe('system/maude/preview/_components.css');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('the bug it guards against: ds0 is the WRONG DS for a maude canvas', () => {
|
|
82
|
+
// Documents the failure mode — with neither opts.ds nor a map entry the
|
|
83
|
+
// resolver returns designSystems[0] (project), which is exactly what broke a
|
|
84
|
+
// maude canvas before DDR-093. The fix is that the map now supplies 'maude'.
|
|
85
|
+
const p = paramsOf(canvasUrl('.design/ui/Studio.tsx', cfg));
|
|
86
|
+
expect(p.get('tokens')).toBe('system/project/colors_and_type.css'); // ds0 — wrong for maude
|
|
87
|
+
const fixed = paramsOf(
|
|
88
|
+
canvasUrl('.design/ui/Studio.tsx', {
|
|
89
|
+
...cfg,
|
|
90
|
+
canvasDesignSystems: { '.design/ui/Studio.tsx': 'maude' },
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
expect(fixed.get('tokens')).toBe('system/maude/colors_and_type.css'); // fixed
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('non-tsx paths bypass the shell and return a direct URL', () => {
|
|
97
|
+
expect(canvasUrl('.design/ui/legacy.html', cfg)).toBe(urlOf('.design/ui/legacy.html'));
|
|
98
|
+
});
|
|
99
|
+
});
|