@1agh/maude 0.28.1 → 0.30.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.
Files changed (372) hide show
  1. package/README.md +3 -3
  2. package/{plugins/design/dev-server → apps/studio}/ai-banner.tsx +1 -2
  3. package/apps/studio/annotations-align.ts +149 -0
  4. package/apps/studio/annotations-bindings.ts +197 -0
  5. package/{plugins/design/dev-server → apps/studio}/annotations-context-toolbar.tsx +681 -151
  6. package/apps/studio/annotations-groups.ts +270 -0
  7. package/apps/studio/annotations-layer.tsx +4537 -0
  8. package/apps/studio/annotations-model.ts +2077 -0
  9. package/apps/studio/annotations-snap.ts +125 -0
  10. package/{plugins/design/dev-server → apps/studio}/api.ts +616 -70
  11. package/{plugins/design/dev-server → apps/studio}/artboard-activity-overlay.tsx +1 -1
  12. package/{plugins/design/dev-server → apps/studio}/artboard-marquee.tsx +1 -1
  13. package/apps/studio/bin/_motion-sample-playwright.mjs +129 -0
  14. package/{plugins/design/dev-server → apps/studio}/bin/_png-playwright.mjs +1 -1
  15. package/apps/studio/bin/annotate.mjs +732 -0
  16. package/apps/studio/bin/annotate.sh +17 -0
  17. package/{plugins/design/dev-server → apps/studio}/bin/canvas-edit.sh +1 -1
  18. package/{plugins/design/dev-server → apps/studio}/bin/draw-proof.sh +37 -2
  19. package/{plugins/design/dev-server → apps/studio}/bin/preflight.sh +1 -1
  20. package/apps/studio/bin/read-annotations.mjs +573 -0
  21. package/apps/studio/bin/read-annotations.sh +11 -0
  22. package/{plugins/design/dev-server → apps/studio}/bin/runtime-health.sh +1 -1
  23. package/{plugins/design/dev-server → apps/studio}/bin/screenshot.sh +62 -2
  24. package/{plugins/design/dev-server → apps/studio}/bin/server-up.sh +9 -3
  25. package/apps/studio/bin/to-lottie-verify.html +68 -0
  26. package/apps/studio/bin/to-lottie.sh +128 -0
  27. package/{plugins/design/dev-server → apps/studio}/build.ts +6 -4
  28. package/{plugins/design/dev-server → apps/studio}/canvas-arrowheads.ts +79 -10
  29. package/{plugins/design/dev-server → apps/studio}/canvas-comment-mount.tsx +1 -1
  30. package/apps/studio/canvas-create.ts +104 -0
  31. package/{plugins/design/dev-server → apps/studio}/canvas-cursors.ts +48 -12
  32. package/{plugins/design/dev-server → apps/studio}/canvas-edit.ts +257 -7
  33. package/{plugins/design/dev-server → apps/studio}/canvas-icons.tsx +165 -1
  34. package/{plugins/design/dev-server → apps/studio}/canvas-lib-inline.ts +1 -1
  35. package/{plugins/design/dev-server → apps/studio}/canvas-lib-resolver.ts +1 -1
  36. package/{plugins/design/dev-server → apps/studio}/canvas-lib.tsx +31 -12
  37. package/{plugins/design/dev-server → apps/studio}/canvas-shell.tsx +360 -48
  38. package/apps/studio/client/app.jsx +6742 -0
  39. package/apps/studio/client/canvas-url.js +81 -0
  40. package/{plugins/design/dev-server → apps/studio}/client/comments-overlay.css +41 -27
  41. package/apps/studio/client/index.html +16 -0
  42. package/apps/studio/client/styles/1-tokens-maude.css +296 -0
  43. package/apps/studio/client/styles/3-shell-maude.css +1161 -0
  44. package/{plugins/design/dev-server → apps/studio}/client/styles/3-shell.css +129 -0
  45. package/apps/studio/client/styles/4-components-maude.css +74 -0
  46. package/{plugins/design/dev-server → apps/studio}/client/styles/4-components.css +396 -34
  47. package/apps/studio/client/styles/5-maude-overrides.css +137 -0
  48. package/{plugins/design/dev-server → apps/studio}/client/styles/_index.css +9 -0
  49. package/{plugins/design/dev-server → apps/studio}/client/styles.css +1 -1
  50. package/apps/studio/client/tour/overlay.jsx +279 -0
  51. package/apps/studio/client/tour/usage-tour.js +48 -0
  52. package/apps/studio/client/whats-new-seen.js +52 -0
  53. package/apps/studio/client/whats-new.jsx +250 -0
  54. package/{plugins/design/dev-server → apps/studio}/commands/annotation-strokes-command.ts +2 -2
  55. package/{plugins/design/dev-server → apps/studio}/commands/equal-spacing-command.ts +1 -1
  56. package/{plugins/design/dev-server → apps/studio}/commands/move-artboards-command.ts +2 -2
  57. package/{plugins/design/dev-server → apps/studio}/comments-overlay.tsx +32 -11
  58. package/{plugins/design/dev-server → apps/studio}/context-menu.tsx +5 -4
  59. package/{plugins/design/dev-server → apps/studio}/contextual-toolbar.tsx +18 -7
  60. package/{plugins/design/dev-server → apps/studio}/cursors-overlay.tsx +21 -21
  61. package/apps/studio/dist/client.bundle.js +25545 -0
  62. package/apps/studio/dist/comment-mount.js +2046 -0
  63. package/apps/studio/dist/styles.css +8018 -0
  64. package/apps/studio/dom-selection.ts +342 -0
  65. package/apps/studio/draw/animate.ts +333 -0
  66. package/{plugins/design/dev-server → apps/studio}/draw/brush.ts +1 -1
  67. package/{plugins/design/dev-server → apps/studio}/draw/composition.ts +1 -1
  68. package/{plugins/design/dev-server → apps/studio}/draw/geometry.ts +1 -1
  69. package/{plugins/design/dev-server → apps/studio}/draw/index.ts +4 -1
  70. package/{plugins/design/dev-server → apps/studio}/draw/layout.ts +1 -1
  71. package/apps/studio/draw/morph.ts +204 -0
  72. package/{plugins/design/dev-server → apps/studio}/draw/optimize.ts +1 -1
  73. package/{plugins/design/dev-server → apps/studio}/draw/palette.ts +1 -1
  74. package/{plugins/design/dev-server → apps/studio}/draw/primitives.ts +1 -1
  75. package/apps/studio/draw/serialize-animate.ts +324 -0
  76. package/{plugins/design/dev-server → apps/studio}/draw/serialize.ts +9 -3
  77. package/apps/studio/draw/test/animate.test.ts +172 -0
  78. package/apps/studio/draw/test/morph.test.ts +107 -0
  79. package/apps/studio/draw/test/serialize-animate.test.ts +142 -0
  80. package/{plugins/design/dev-server → apps/studio}/equal-spacing-detector.ts +1 -1
  81. package/{plugins/design/dev-server → apps/studio}/equal-spacing-handles.tsx +7 -29
  82. package/{plugins/design/dev-server → apps/studio}/export-dialog.tsx +16 -4
  83. package/{plugins/design/dev-server → apps/studio}/history.ts +20 -3
  84. package/{plugins/design/dev-server → apps/studio}/http.ts +226 -6
  85. package/{plugins/design/dev-server → apps/studio}/input-router.tsx +15 -1
  86. package/{plugins/design/dev-server → apps/studio}/inspect.ts +50 -4
  87. package/{plugins/design/dev-server → apps/studio}/marquee-overlay.tsx +5 -2
  88. package/{plugins/design/dev-server → apps/studio}/participants-chrome.tsx +2 -8
  89. package/{plugins/design/dev-server → apps/studio}/paths.ts +15 -11
  90. package/{plugins/design/dev-server → apps/studio}/server.ts +23 -2
  91. package/{plugins/design/dev-server → apps/studio}/sync/agent.ts +231 -57
  92. package/{plugins/design/dev-server → apps/studio}/sync/codec.ts +45 -0
  93. package/apps/studio/sync/cold-start.ts +158 -0
  94. package/{plugins/design/dev-server → apps/studio}/sync/connection-state.ts +58 -2
  95. package/{plugins/design/dev-server → apps/studio}/sync/index.ts +563 -221
  96. package/apps/studio/sync/journal.ts +190 -0
  97. package/apps/studio/sync/migrate-seed.ts +321 -0
  98. package/{plugins/design/dev-server → apps/studio}/sync/projection.ts +22 -3
  99. package/{plugins/design/dev-server → apps/studio}/sync/status.ts +15 -4
  100. package/{plugins/design/dev-server → apps/studio}/test/activity.test.ts +1 -7
  101. package/apps/studio/test/annotate-write.test.ts +184 -0
  102. package/apps/studio/test/annotations-align.test.ts +88 -0
  103. package/apps/studio/test/annotations-bindings.test.ts +124 -0
  104. package/apps/studio/test/annotations-groups.test.ts +231 -0
  105. package/{plugins/design/dev-server → apps/studio}/test/annotations-layer.test.ts +63 -0
  106. package/{plugins/design/dev-server → apps/studio}/test/annotations-roundtrip.test.ts +97 -0
  107. package/apps/studio/test/annotations-snap.test.ts +79 -0
  108. package/apps/studio/test/annotations-text-format.test.ts +396 -0
  109. package/apps/studio/test/asset-api.test.ts +149 -0
  110. package/apps/studio/test/canvas-create-api.test.ts +361 -0
  111. package/{plugins/design/dev-server → apps/studio}/test/canvas-cursors.test.ts +2 -0
  112. package/apps/studio/test/canvas-edit.test.ts +319 -0
  113. package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-resolver.test.ts +1 -1
  114. package/apps/studio/test/canvas-media-drop.test.ts +158 -0
  115. package/{plugins/design/dev-server → apps/studio}/test/canvas-origin-gate.test.ts +25 -1
  116. package/apps/studio/test/canvas-url.test.ts +99 -0
  117. package/{plugins/design/dev-server → apps/studio}/test/collab-bridge.test.ts +0 -2
  118. package/{plugins/design/dev-server → apps/studio}/test/collab-room.test.ts +2 -7
  119. package/{plugins/design/dev-server → apps/studio}/test/csp-canvas-shell.test.ts +5 -0
  120. package/apps/studio/test/csrf-write-guard.test.ts +52 -0
  121. package/apps/studio/test/figjam-v3-model.test.ts +342 -0
  122. package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +1 -0
  123. package/apps/studio/test/index-data-canvas-ds.test.ts +97 -0
  124. package/apps/studio/test/motion-sample.test.ts +35 -0
  125. package/{plugins/design/dev-server → apps/studio}/test/perf-harness.ts +1 -1
  126. package/{plugins/design/dev-server → apps/studio}/test/phase-3.6-smoke.test.ts +3 -1
  127. package/apps/studio/test/read-annotations.test.ts +409 -0
  128. package/{plugins/design/dev-server → apps/studio}/test/sanitize-annotation-svg.test.ts +108 -2
  129. package/{plugins/design/dev-server → apps/studio}/test/shared-doc-convergence.test.ts +1 -1
  130. package/apps/studio/test/shared-doc-migrate.test.ts +369 -0
  131. package/apps/studio/test/sync-agent.test.ts +575 -0
  132. package/{plugins/design/dev-server → apps/studio}/test/sync-codec.test.ts +65 -0
  133. package/apps/studio/test/sync-cold-start.test.ts +244 -0
  134. package/{plugins/design/dev-server → apps/studio}/test/sync-connection-state.test.ts +68 -0
  135. package/{plugins/design/dev-server → apps/studio}/test/sync-hardening.test.ts +0 -8
  136. package/{plugins/design/dev-server → apps/studio}/test/sync-hubs-config.test.ts +0 -1
  137. package/apps/studio/test/sync-incident-replay.test.ts +211 -0
  138. package/apps/studio/test/sync-journal.test.ts +176 -0
  139. package/{plugins/design/dev-server → apps/studio}/test/sync-runtime.test.ts +360 -5
  140. package/{plugins/design/dev-server → apps/studio}/test/sync-status.test.ts +66 -0
  141. package/apps/studio/test/tour-overlay.test.tsx +56 -0
  142. package/apps/studio/test/use-annotation-resize.test.ts +402 -0
  143. package/{plugins/design/dev-server → apps/studio}/test/use-artboard-drag.test.ts +0 -1
  144. package/{plugins/design/dev-server → apps/studio}/test/use-collab.test.ts +0 -0
  145. package/{plugins/design/dev-server → apps/studio}/test/use-tool-mode.test.tsx +8 -2
  146. package/apps/studio/test/whats-new.test.ts +101 -0
  147. package/apps/studio/text-imports.d.ts +9 -0
  148. package/{plugins/design/dev-server → apps/studio}/tool-palette.tsx +15 -70
  149. package/{plugins/design/dev-server → apps/studio}/undo-hud.tsx +1 -2
  150. package/{plugins/design/dev-server → apps/studio}/undo-stack.ts +1 -1
  151. package/{plugins/design/dev-server → apps/studio}/use-agent-presence.tsx +5 -3
  152. package/apps/studio/use-annotation-resize.tsx +912 -0
  153. package/{plugins/design/dev-server → apps/studio}/use-annotation-selection.tsx +17 -5
  154. package/{plugins/design/dev-server → apps/studio}/use-annotations-visibility.tsx +1 -1
  155. package/{plugins/design/dev-server → apps/studio}/use-artboard-drag.tsx +1 -1
  156. package/{plugins/design/dev-server → apps/studio}/use-canvas-activity.tsx +3 -3
  157. package/apps/studio/use-canvas-media-drop.tsx +343 -0
  158. package/{plugins/design/dev-server → apps/studio}/use-collab.tsx +21 -16
  159. package/{plugins/design/dev-server → apps/studio}/use-cursor-modifiers.tsx +1 -1
  160. package/{plugins/design/dev-server → apps/studio}/use-keyboard-discipline.tsx +5 -2
  161. package/{plugins/design/dev-server → apps/studio}/use-selection-set.tsx +18 -1
  162. package/{plugins/design/dev-server → apps/studio}/use-snap-guides.tsx +1 -1
  163. package/{plugins/design/dev-server → apps/studio}/use-tool-mode.tsx +8 -2
  164. package/{plugins/design/dev-server → apps/studio}/use-undo-stack.tsx +1 -1
  165. package/apps/studio/whats-new.json +264 -0
  166. package/apps/studio/whats-new.schema.json +86 -0
  167. package/apps/studio/whats-new.ts +87 -0
  168. package/cli/bin/maude.mjs +10 -0
  169. package/cli/commands/design-link.test.mjs +84 -0
  170. package/cli/commands/design.mjs +16 -7
  171. package/cli/commands/design.test.mjs +40 -0
  172. package/cli/commands/help.mjs +8 -2
  173. package/cli/commands/hub.mjs +3 -3
  174. package/cli/commands/hub.test.mjs +1 -1
  175. package/cli/commands/scenario-report.mjs +2 -9
  176. package/cli/lib/design-link.mjs +51 -1
  177. package/cli/lib/flow-design-integration.test.mjs +1 -1
  178. package/cli/lib/gitignore-block.mjs +1 -0
  179. package/cli/lib/plugin-cli-reachability.test.mjs +8 -7
  180. package/cli/lib/preflight.mjs +1 -1
  181. package/package.json +15 -15
  182. package/plugins/design/templates/_shell.html +28 -4
  183. package/plugins/design/templates/brief-board.tsx.template +61 -0
  184. package/plugins/design/templates/canvas.tsx.template +1 -1
  185. package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -1
  186. package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
  187. package/plugins/flow/.claude-plugin/config.schema.json +16 -0
  188. package/plugins/design/dev-server/annotations-layer.tsx +0 -3402
  189. package/plugins/design/dev-server/client/app.jsx +0 -2985
  190. package/plugins/design/dev-server/client/index.html +0 -15
  191. package/plugins/design/dev-server/dist/client.bundle.js +0 -20692
  192. package/plugins/design/dev-server/dist/comment-mount.js +0 -1924
  193. package/plugins/design/dev-server/dist/styles.css +0 -2710
  194. package/plugins/design/dev-server/dom-selection.ts +0 -156
  195. package/plugins/design/dev-server/sync/migrate-seed.ts +0 -163
  196. package/plugins/design/dev-server/test/canvas-edit.test.ts +0 -139
  197. package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +0 -159
  198. package/plugins/design/dev-server/test/sync-agent.test.ts +0 -305
  199. package/plugins/design/dev-server/test/use-annotation-resize.test.ts +0 -200
  200. package/plugins/design/dev-server/use-annotation-resize.tsx +0 -449
  201. /package/{plugins/design/dev-server → apps/studio}/activity.ts +0 -0
  202. /package/{plugins/design/dev-server → apps/studio}/bin/_enumerate-artboards-playwright.mjs +0 -0
  203. /package/{plugins/design/dev-server → apps/studio}/bin/_html-playwright.mjs +0 -0
  204. /package/{plugins/design/dev-server → apps/studio}/bin/_pdf-playwright.mjs +0 -0
  205. /package/{plugins/design/dev-server → apps/studio}/bin/_pptx-playwright.mjs +0 -0
  206. /package/{plugins/design/dev-server → apps/studio}/bin/_pw-launch.mjs +0 -0
  207. /package/{plugins/design/dev-server → apps/studio}/bin/_screenshot-playwright.mjs +0 -0
  208. /package/{plugins/design/dev-server → apps/studio}/bin/_svg-optimize.mjs +0 -0
  209. /package/{plugins/design/dev-server → apps/studio}/bin/_svg-playwright.mjs +0 -0
  210. /package/{plugins/design/dev-server → apps/studio}/bin/asset-sweep.sh +0 -0
  211. /package/{plugins/design/dev-server → apps/studio}/bin/bootstrap-check.sh +0 -0
  212. /package/{plugins/design/dev-server → apps/studio}/bin/check-runtime-bundles.sh +0 -0
  213. /package/{plugins/design/dev-server → apps/studio}/bin/draw-build.sh +0 -0
  214. /package/{plugins/design/dev-server → apps/studio}/bin/handoff.sh +0 -0
  215. /package/{plugins/design/dev-server → apps/studio}/bin/prep.sh +0 -0
  216. /package/{plugins/design/dev-server → apps/studio}/bin/scenario-report.mjs +0 -0
  217. /package/{plugins/design/dev-server → apps/studio}/bin/slug.sh +0 -0
  218. /package/{plugins/design/dev-server → apps/studio}/bin/smoke.sh +0 -0
  219. /package/{plugins/design/dev-server → apps/studio}/bin/svg-optimize.sh +0 -0
  220. /package/{plugins/design/dev-server → apps/studio}/bin/visual-sanity.sh +0 -0
  221. /package/{plugins/design/dev-server → apps/studio}/boot-self-heal.ts +0 -0
  222. /package/{plugins/design/dev-server → apps/studio}/canvas-build.ts +0 -0
  223. /package/{plugins/design/dev-server → apps/studio}/canvas-header.ts +0 -0
  224. /package/{plugins/design/dev-server → apps/studio}/canvas-meta.schema.json +0 -0
  225. /package/{plugins/design/dev-server → apps/studio}/canvas-pipeline.ts +0 -0
  226. /package/{plugins/design/dev-server → apps/studio}/client/hmr.mjs +0 -0
  227. /package/{plugins/design/dev-server → apps/studio}/client/iframe-lazy.mjs +0 -0
  228. /package/{plugins/design/dev-server → apps/studio}/client/styles/0-reset.css +0 -0
  229. /package/{plugins/design/dev-server → apps/studio}/client/styles/1-tokens.css +0 -0
  230. /package/{plugins/design/dev-server → apps/studio}/client/styles/2-layout.css +0 -0
  231. /package/{plugins/design/dev-server → apps/studio}/client/styles/5-utilities.css +0 -0
  232. /package/{plugins/design/dev-server → apps/studio}/collab/ai-activity.ts +0 -0
  233. /package/{plugins/design/dev-server → apps/studio}/collab/awareness-bridge.ts +0 -0
  234. /package/{plugins/design/dev-server → apps/studio}/collab/git-lifecycle.ts +0 -0
  235. /package/{plugins/design/dev-server → apps/studio}/collab/index.ts +0 -0
  236. /package/{plugins/design/dev-server → apps/studio}/collab/persistence.ts +0 -0
  237. /package/{plugins/design/dev-server → apps/studio}/collab/protocol.ts +0 -0
  238. /package/{plugins/design/dev-server → apps/studio}/collab/registry.ts +0 -0
  239. /package/{plugins/design/dev-server → apps/studio}/collab/room.ts +0 -0
  240. /package/{plugins/design/dev-server → apps/studio}/config.schema.json +0 -0
  241. /package/{plugins/design/dev-server → apps/studio}/context.ts +0 -0
  242. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/.min-sizes.json +0 -0
  243. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_decoding.js +0 -0
  244. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_encoding.js +0 -0
  245. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion.js +0 -0
  246. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion_react.js +0 -0
  247. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/pixi-js.js +0 -0
  248. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom.js +0 -0
  249. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom_client.js +0 -0
  250. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react.js +0 -0
  251. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-dev-runtime.js +0 -0
  252. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-runtime.js +0 -0
  253. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_awareness.js +0 -0
  254. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_sync.js +0 -0
  255. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/yjs.js +0 -0
  256. /package/{plugins/design/dev-server → apps/studio}/draw/test/brush.test.ts +0 -0
  257. /package/{plugins/design/dev-server → apps/studio}/draw/test/composition.test.ts +0 -0
  258. /package/{plugins/design/dev-server → apps/studio}/draw/test/geometry.test.ts +0 -0
  259. /package/{plugins/design/dev-server → apps/studio}/draw/test/gradient.test.ts +0 -0
  260. /package/{plugins/design/dev-server → apps/studio}/draw/test/layout.test.ts +0 -0
  261. /package/{plugins/design/dev-server → apps/studio}/draw/test/optimize.test.ts +0 -0
  262. /package/{plugins/design/dev-server → apps/studio}/draw/test/palette.test.ts +0 -0
  263. /package/{plugins/design/dev-server → apps/studio}/draw/test/primitives.test.ts +0 -0
  264. /package/{plugins/design/dev-server → apps/studio}/draw/test/serialize.test.ts +0 -0
  265. /package/{plugins/design/dev-server → apps/studio}/examples/README.md +0 -0
  266. /package/{plugins/design/dev-server → apps/studio}/examples/perf-100-artboards.tsx +0 -0
  267. /package/{plugins/design/dev-server → apps/studio}/exporters/_browser-bundles.ts +0 -0
  268. /package/{plugins/design/dev-server → apps/studio}/exporters/canva-handoff-prompt.ts +0 -0
  269. /package/{plugins/design/dev-server → apps/studio}/exporters/canva.ts +0 -0
  270. /package/{plugins/design/dev-server → apps/studio}/exporters/html.ts +0 -0
  271. /package/{plugins/design/dev-server → apps/studio}/exporters/index.ts +0 -0
  272. /package/{plugins/design/dev-server → apps/studio}/exporters/pdf.ts +0 -0
  273. /package/{plugins/design/dev-server → apps/studio}/exporters/png.ts +0 -0
  274. /package/{plugins/design/dev-server → apps/studio}/exporters/pptx.ts +0 -0
  275. /package/{plugins/design/dev-server → apps/studio}/exporters/scope.ts +0 -0
  276. /package/{plugins/design/dev-server → apps/studio}/exporters/svg.ts +0 -0
  277. /package/{plugins/design/dev-server → apps/studio}/exporters/zip.ts +0 -0
  278. /package/{plugins/design/dev-server → apps/studio}/fs-watch.ts +0 -0
  279. /package/{plugins/design/dev-server → apps/studio}/handoff.ts +0 -0
  280. /package/{plugins/design/dev-server → apps/studio}/hmr-broadcast.ts +0 -0
  281. /package/{plugins/design/dev-server → apps/studio}/locator.ts +0 -0
  282. /package/{plugins/design/dev-server → apps/studio}/mem.ts +0 -0
  283. /package/{plugins/design/dev-server → apps/studio}/runtime-bundle.ts +0 -0
  284. /package/{plugins/design/dev-server → apps/studio}/server.mjs +0 -0
  285. /package/{plugins/design/dev-server → apps/studio}/sync/atomic-write.ts +0 -0
  286. /package/{plugins/design/dev-server → apps/studio}/sync/echo-guard.ts +0 -0
  287. /package/{plugins/design/dev-server → apps/studio}/sync/fs-mirror.ts +0 -0
  288. /package/{plugins/design/dev-server → apps/studio}/sync/hubs-config.ts +0 -0
  289. /package/{plugins/design/dev-server → apps/studio}/sync/materialize.ts +0 -0
  290. /package/{plugins/design/dev-server → apps/studio}/sync/origins.ts +0 -0
  291. /package/{plugins/design/dev-server → apps/studio}/sync/untrusted.ts +0 -0
  292. /package/{plugins/design/dev-server → apps/studio}/test/_helpers.ts +0 -0
  293. /package/{plugins/design/dev-server → apps/studio}/test/active-state.test.ts +0 -0
  294. /package/{plugins/design/dev-server → apps/studio}/test/ai-activity.test.ts +0 -0
  295. /package/{plugins/design/dev-server → apps/studio}/test/annotation-strokes-command.test.ts +0 -0
  296. /package/{plugins/design/dev-server → apps/studio}/test/annotations-api.test.ts +0 -0
  297. /package/{plugins/design/dev-server → apps/studio}/test/annotations-draw-modifiers.test.ts +0 -0
  298. /package/{plugins/design/dev-server → apps/studio}/test/artboard-activity-overlay.test.tsx +0 -0
  299. /package/{plugins/design/dev-server → apps/studio}/test/binary-smoke.test.ts +0 -0
  300. /package/{plugins/design/dev-server → apps/studio}/test/boot-self-heal.test.ts +0 -0
  301. /package/{plugins/design/dev-server → apps/studio}/test/bundle-smoke.test.ts +0 -0
  302. /package/{plugins/design/dev-server → apps/studio}/test/canvas-build.test.ts +0 -0
  303. /package/{plugins/design/dev-server → apps/studio}/test/canvas-header.test.ts +0 -0
  304. /package/{plugins/design/dev-server → apps/studio}/test/canvas-hmr-runtime.test.tsx +0 -0
  305. /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-inline.test.ts +0 -0
  306. /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-motion.test.ts +0 -0
  307. /package/{plugins/design/dev-server → apps/studio}/test/canvas-meta-api.test.ts +0 -0
  308. /package/{plugins/design/dev-server → apps/studio}/test/canvas-pipeline.test.ts +0 -0
  309. /package/{plugins/design/dev-server → apps/studio}/test/canvas-route.test.ts +0 -0
  310. /package/{plugins/design/dev-server → apps/studio}/test/collab-annotations-bridge.test.ts +0 -0
  311. /package/{plugins/design/dev-server → apps/studio}/test/collab-awareness-bridge.test.ts +0 -0
  312. /package/{plugins/design/dev-server → apps/studio}/test/collab-loopback.test.ts +0 -0
  313. /package/{plugins/design/dev-server → apps/studio}/test/collab-protocol.test.ts +0 -0
  314. /package/{plugins/design/dev-server → apps/studio}/test/collab-reseed-guard.test.ts +0 -0
  315. /package/{plugins/design/dev-server → apps/studio}/test/collab-stress.test.ts +0 -0
  316. /package/{plugins/design/dev-server → apps/studio}/test/comment-mount.test.ts +0 -0
  317. /package/{plugins/design/dev-server → apps/studio}/test/comments-api.test.ts +0 -0
  318. /package/{plugins/design/dev-server → apps/studio}/test/compile-entry.test.ts +0 -0
  319. /package/{plugins/design/dev-server → apps/studio}/test/context-resolve-tokens.test.ts +0 -0
  320. /package/{plugins/design/dev-server → apps/studio}/test/equal-spacing-detector.test.ts +0 -0
  321. /package/{plugins/design/dev-server → apps/studio}/test/exporters/canva.test.ts +0 -0
  322. /package/{plugins/design/dev-server → apps/studio}/test/exporters/endpoint.test.ts +0 -0
  323. /package/{plugins/design/dev-server → apps/studio}/test/exporters/history.test.ts +0 -0
  324. /package/{plugins/design/dev-server → apps/studio}/test/exporters/html.test.ts +0 -0
  325. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pdf.test.ts +0 -0
  326. /package/{plugins/design/dev-server → apps/studio}/test/exporters/png.test.ts +0 -0
  327. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx-deck.test.ts +0 -0
  328. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx.test.ts +0 -0
  329. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pw-launch.test.ts +0 -0
  330. /package/{plugins/design/dev-server → apps/studio}/test/exporters/scope.test.ts +0 -0
  331. /package/{plugins/design/dev-server → apps/studio}/test/exporters/svg.test.ts +0 -0
  332. /package/{plugins/design/dev-server → apps/studio}/test/exporters/zip.test.ts +0 -0
  333. /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-20-annotations.svg +0 -0
  334. /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-21-annotations.svg +0 -0
  335. /package/{plugins/design/dev-server → apps/studio}/test/fs-watch.test.ts +0 -0
  336. /package/{plugins/design/dev-server → apps/studio}/test/git-lifecycle.test.ts +0 -0
  337. /package/{plugins/design/dev-server → apps/studio}/test/handoff-static-frames.test.ts +0 -0
  338. /package/{plugins/design/dev-server → apps/studio}/test/handoff.test.ts +0 -0
  339. /package/{plugins/design/dev-server → apps/studio}/test/history-rollback.test.ts +0 -0
  340. /package/{plugins/design/dev-server → apps/studio}/test/hmr-broadcast.test.ts +0 -0
  341. /package/{plugins/design/dev-server → apps/studio}/test/hmr-classify.test.ts +0 -0
  342. /package/{plugins/design/dev-server → apps/studio}/test/input-router.test.ts +0 -0
  343. /package/{plugins/design/dev-server → apps/studio}/test/locator.test.ts +0 -0
  344. /package/{plugins/design/dev-server → apps/studio}/test/marquee-overlay.test.ts +0 -0
  345. /package/{plugins/design/dev-server → apps/studio}/test/move-artboards-command.test.ts +0 -0
  346. /package/{plugins/design/dev-server → apps/studio}/test/participants-chrome.test.ts +0 -0
  347. /package/{plugins/design/dev-server → apps/studio}/test/paths.test.ts +0 -0
  348. /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle-error-mapping.test.ts +0 -0
  349. /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle.test.ts +0 -0
  350. /package/{plugins/design/dev-server → apps/studio}/test/server-lifecycle.test.ts +0 -0
  351. /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-foundation.test.ts +0 -0
  352. /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-projection.test.ts +0 -0
  353. /package/{plugins/design/dev-server → apps/studio}/test/snap-distance-pill.test.ts +0 -0
  354. /package/{plugins/design/dev-server → apps/studio}/test/sync-atomic-write.test.ts +0 -0
  355. /package/{plugins/design/dev-server → apps/studio}/test/sync-echo-guard.test.ts +0 -0
  356. /package/{plugins/design/dev-server → apps/studio}/test/sync-fs-mirror.test.ts +0 -0
  357. /package/{plugins/design/dev-server → apps/studio}/test/sync-meta-codec.test.ts +0 -0
  358. /package/{plugins/design/dev-server → apps/studio}/test/sync-untrusted.test.ts +0 -0
  359. /package/{plugins/design/dev-server → apps/studio}/test/system-endpoint.test.ts +0 -0
  360. /package/{plugins/design/dev-server → apps/studio}/test/tool-palette.test.tsx +0 -0
  361. /package/{plugins/design/dev-server → apps/studio}/test/undo-stack.test.ts +0 -0
  362. /package/{plugins/design/dev-server → apps/studio}/test/use-agent-presence.test.tsx +0 -0
  363. /package/{plugins/design/dev-server → apps/studio}/test/use-annotation-selection.test.tsx +0 -0
  364. /package/{plugins/design/dev-server → apps/studio}/test/use-canvas-activity.test.tsx +0 -0
  365. /package/{plugins/design/dev-server → apps/studio}/test/use-cursor-modifiers.test.ts +0 -0
  366. /package/{plugins/design/dev-server → apps/studio}/test/use-keyboard-discipline.test.ts +0 -0
  367. /package/{plugins/design/dev-server → apps/studio}/test/use-selection-set.test.tsx +0 -0
  368. /package/{plugins/design/dev-server → apps/studio}/test/use-snap-guides.test.ts +0 -0
  369. /package/{plugins/design/dev-server → apps/studio}/test/use-undo-stack.test.tsx +0 -0
  370. /package/{plugins/design/dev-server → apps/studio}/test/ws-handshake.test.ts +0 -0
  371. /package/{plugins/design/dev-server → apps/studio}/tsconfig.json +0 -0
  372. /package/{plugins/design/dev-server → apps/studio}/ws.ts +0 -0
@@ -0,0 +1,149 @@
1
+ // POST /_api/asset — Phase 23 content-addressed image upload (Task 5).
2
+ //
3
+ // Two layers:
4
+ // (1) the pure magic-byte sniff (`sniffImageType`) — the ONE gate that decides
5
+ // the stored type; a header lie / SVG is caught here;
6
+ // (2) the POST endpoint round-trip (boots a real server against a sandbox) —
7
+ // happy path for all four types, header-lie, oversize, SVG-reject,
8
+ // content-addressed dedupe, method gate.
9
+
10
+ import { describe, expect, test } from 'bun:test';
11
+ import { existsSync, readdirSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+
14
+ import { sniffImageType } from '../api.ts';
15
+ import { bootServer, killProc, makeSandbox, nextPort } from './_helpers.ts';
16
+
17
+ // ── Minimal valid magic-byte headers (padded so length checks pass). ──────────
18
+ const pad = (header: number[], len = 64): Uint8Array => {
19
+ const out = new Uint8Array(len);
20
+ out.set(header, 0);
21
+ return out;
22
+ };
23
+ const PNG = pad([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
24
+ const JPEG = pad([0xff, 0xd8, 0xff, 0xe0]);
25
+ const GIF = pad([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]); // "GIF89a"
26
+ const WEBP = pad([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50]); // RIFF????WEBP
27
+ const SVG = new TextEncoder().encode('<svg xmlns="http://www.w3.org/2000/svg"><script/></svg>');
28
+
29
+ describe('asset-api / sniffImageType (the type gate)', () => {
30
+ test('recognizes each supported raster type by magic bytes', () => {
31
+ expect(sniffImageType(PNG)).toBe('png');
32
+ expect(sniffImageType(JPEG)).toBe('jpg');
33
+ expect(sniffImageType(GIF)).toBe('gif');
34
+ expect(sniffImageType(WEBP)).toBe('webp');
35
+ });
36
+
37
+ test('rejects SVG / text / empty / truncated headers', () => {
38
+ expect(sniffImageType(SVG)).toBeNull();
39
+ expect(sniffImageType(new TextEncoder().encode('not an image'))).toBeNull();
40
+ expect(sniffImageType(new Uint8Array(0))).toBeNull();
41
+ // Truncated PNG signature (only 4 of 8 bytes) must NOT match.
42
+ expect(sniffImageType(new Uint8Array([0x89, 0x50, 0x4e, 0x47]))).toBeNull();
43
+ // RIFF without the WEBP fourcc (e.g. a WAV) must NOT match.
44
+ expect(
45
+ sniffImageType(pad([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x41, 0x56, 0x45]))
46
+ ).toBeNull();
47
+ });
48
+
49
+ test('content addresses the stored ext (a .png NAME with GIF bytes sniffs gif)', () => {
50
+ // The route names the file from sniffImageType(bytes), never the upload name.
51
+ expect(sniffImageType(GIF)).toBe('gif');
52
+ });
53
+ });
54
+
55
+ describe('asset-api / POST /_api/asset (endpoint round-trip)', () => {
56
+ async function withServer(
57
+ fn: (port: number, designRoot: string) => Promise<void>,
58
+ extraEnv?: Record<string, string>
59
+ ) {
60
+ const sandbox = makeSandbox();
61
+ const port = nextPort();
62
+ const proc = await bootServer(sandbox.root, port, extraEnv);
63
+ try {
64
+ await fn(port, sandbox.designRoot);
65
+ } finally {
66
+ await killProc(proc);
67
+ }
68
+ }
69
+
70
+ const postAsset = (port: number, body: Uint8Array, contentType = 'application/octet-stream') =>
71
+ fetch(`http://localhost:${port}/_api/asset`, {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': contentType },
74
+ body,
75
+ });
76
+
77
+ test('writes a valid PNG/JPEG/WEBP/GIF and returns its assets path', async () => {
78
+ await withServer(async (port, designRoot) => {
79
+ for (const [bytes, ext] of [
80
+ [PNG, 'png'],
81
+ [JPEG, 'jpg'],
82
+ [WEBP, 'webp'],
83
+ [GIF, 'gif'],
84
+ ] as const) {
85
+ const res = await postAsset(port, bytes);
86
+ expect(res.status).toBe(201);
87
+ const json = (await res.json()) as { path: string };
88
+ expect(json.path).toMatch(new RegExp(`^assets/[0-9a-f]{8}\\.${ext}$`));
89
+ expect(existsSync(join(designRoot, json.path))).toBe(true);
90
+ }
91
+ });
92
+ });
93
+
94
+ test('a header-lie (declared image/png, GIF bytes) is stored as .gif (sniff wins)', async () => {
95
+ await withServer(async (port) => {
96
+ const res = await postAsset(port, GIF, 'image/png');
97
+ expect(res.status).toBe(201);
98
+ const json = (await res.json()) as { path: string };
99
+ expect(json.path.endsWith('.gif')).toBe(true);
100
+ });
101
+ });
102
+
103
+ test('rejects an SVG upload (no script-bearing vector) with 415', async () => {
104
+ await withServer(async (port) => {
105
+ const res = await postAsset(port, SVG, 'image/svg+xml');
106
+ expect(res.status).toBe(415);
107
+ });
108
+ });
109
+
110
+ test('rejects an oversize body (> 10 MB) with 413', async () => {
111
+ await withServer(async (port) => {
112
+ const big = new Uint8Array(10 * 1024 * 1024 + 1);
113
+ big.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], 0); // valid PNG header
114
+ const res = await postAsset(port, big);
115
+ expect(res.status).toBe(413);
116
+ });
117
+ });
118
+
119
+ test('identical bytes dedupe to ONE file (content-addressed; disk-fill guard)', async () => {
120
+ await withServer(async (port, designRoot) => {
121
+ const a = await (await postAsset(port, PNG)).json();
122
+ const b = await (await postAsset(port, PNG)).json();
123
+ expect(a.path).toBe(b.path);
124
+ const files = readdirSync(join(designRoot, 'assets'));
125
+ expect(files.length).toBe(1);
126
+ });
127
+ });
128
+
129
+ test('GET /_api/asset is method-not-allowed (write-only)', async () => {
130
+ await withServer(async (port) => {
131
+ const res = await fetch(`http://localhost:${port}/_api/asset`);
132
+ expect(res.status).toBe(405);
133
+ });
134
+ });
135
+
136
+ test('enforces the aggregate session write budget (disk-fill guard → 429)', async () => {
137
+ // Budget = 100 bytes; each padded magic-byte image is 64 bytes. The first
138
+ // DISTINCT write fits (64); a second distinct image (64) would total 128 >
139
+ // 100 → rejected. A dedupe hit (same bytes) is free and must NOT count.
140
+ await withServer(
141
+ async (port) => {
142
+ expect((await postAsset(port, PNG)).status).toBe(201); // 64 written
143
+ expect((await postAsset(port, PNG)).status).toBe(201); // dedupe — free
144
+ expect((await postAsset(port, JPEG)).status).toBe(429); // 64+64 > 100
145
+ },
146
+ { MAUDE_ASSET_SESSION_BUDGET: '100' }
147
+ );
148
+ });
149
+ });
@@ -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',