@agent-native/core 0.80.2 → 0.80.5

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 (322) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +66 -2
  3. package/corpus/core/docs/content/locales/ar-SA/real-time-collaboration.mdx +2 -2
  4. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +1 -1
  5. package/corpus/core/docs/content/locales/de-DE/real-time-collaboration.mdx +3 -3
  6. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +1 -1
  7. package/corpus/core/docs/content/locales/es-ES/real-time-collaboration.mdx +2 -2
  8. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +1 -1
  9. package/corpus/core/docs/content/locales/fr-FR/real-time-collaboration.mdx +2 -2
  10. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +1 -1
  11. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +1 -1
  12. package/corpus/core/docs/content/locales/ja-JP/real-time-collaboration.mdx +2 -2
  13. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +1 -1
  14. package/corpus/core/docs/content/locales/ko-KR/real-time-collaboration.mdx +2 -2
  15. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +1 -1
  16. package/corpus/core/docs/content/locales/pt-BR/real-time-collaboration.mdx +2 -2
  17. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +1 -1
  18. package/corpus/core/docs/content/locales/zh-CN/real-time-collaboration.mdx +2 -2
  19. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +1 -1
  20. package/corpus/core/docs/content/locales/zh-TW/real-time-collaboration.mdx +2 -2
  21. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +1 -1
  22. package/corpus/core/docs/content/real-time-collaboration.mdx +2 -2
  23. package/corpus/core/docs/content/template-design.mdx +1 -1
  24. package/corpus/core/package.json +3 -7
  25. package/corpus/core/src/action.ts +32 -0
  26. package/corpus/core/src/agent/durable-background.ts +32 -11
  27. package/corpus/core/src/agent/production-agent.ts +14 -1
  28. package/corpus/core/src/agent/run-loop-with-resume.ts +3 -1
  29. package/corpus/core/src/brand-kit/fig/index.ts +17 -37
  30. package/corpus/core/src/cli/index.ts +1 -1
  31. package/corpus/core/src/cli/mcp-config-writers.ts +187 -51
  32. package/corpus/core/src/cli/plan-local.ts +445 -108
  33. package/corpus/core/src/cli/skills.ts +32 -8
  34. package/corpus/core/src/client/AssistantChat.tsx +11 -1
  35. package/corpus/core/src/client/agent-chat.ts +12 -1
  36. package/corpus/core/src/client/composer/PromptComposer.tsx +100 -41
  37. package/corpus/core/src/client/org/OrgSwitcher.tsx +1 -1
  38. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -27
  39. package/corpus/core/src/collab/presence.ts +1 -1
  40. package/corpus/core/src/mcp/connect-route.ts +34 -5
  41. package/corpus/core/src/server/action-discovery.ts +4 -0
  42. package/corpus/core/src/server/action-routes.ts +14 -0
  43. package/corpus/core/src/server/agent-chat-plugin.ts +20 -0
  44. package/corpus/core/src/vite/client.ts +15 -1
  45. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +2 -0
  46. package/corpus/templates/analytics/actions/update-dashboard.ts +8 -0
  47. package/corpus/templates/analytics/app/components/layout/Layout.tsx +1 -1
  48. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +117 -33
  49. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -0
  50. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +7 -0
  51. package/corpus/templates/analytics/changelog/2026-06-27-dashboards-can-now-nest-under-a-parent-in-the-sidebar-via-a-.md +6 -0
  52. package/corpus/templates/analytics/changelog/2026-06-29-analytics-pages-now-sit-closer-under-the-header-with-tighter.md +6 -0
  53. package/corpus/templates/assets/actions/generate-image-batch.ts +3 -0
  54. package/corpus/templates/assets/actions/generate-image.ts +3 -0
  55. package/corpus/templates/assets/changelog/2026-06-29-longer-agent-generation-window.md +6 -0
  56. package/corpus/templates/assets/netlify.toml +3 -3
  57. package/corpus/templates/assets/server/plugins/agent-chat.ts +4 -1
  58. package/corpus/templates/clips/app/components/capture-install-options.tsx +7 -8
  59. package/corpus/templates/clips/app/components/player/timestamped-comment-button.tsx +162 -0
  60. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +1 -1
  61. package/corpus/templates/clips/app/i18n/ar-SA.ts +4 -0
  62. package/corpus/templates/clips/app/i18n/de-DE.ts +4 -0
  63. package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
  64. package/corpus/templates/clips/app/i18n/es-ES.ts +4 -0
  65. package/corpus/templates/clips/app/i18n/fr-FR.ts +4 -0
  66. package/corpus/templates/clips/app/i18n/hi-IN.ts +4 -0
  67. package/corpus/templates/clips/app/i18n/ja-JP.ts +4 -0
  68. package/corpus/templates/clips/app/i18n/ko-KR.ts +4 -0
  69. package/corpus/templates/clips/app/i18n/pt-BR.ts +4 -0
  70. package/corpus/templates/clips/app/i18n/zh-CN.ts +4 -0
  71. package/corpus/templates/clips/app/i18n/zh-TW.ts +4 -0
  72. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +42 -18
  73. package/corpus/templates/content/AGENTS.md +12 -0
  74. package/corpus/templates/content/app/blocks/BuilderDocsBlocks.tsx +10 -0
  75. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +1 -0
  76. package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +185 -0
  77. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -0
  78. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +19 -0
  79. package/corpus/templates/content/app/components/editor/extensions/ContentReferenceNode.tsx +82 -0
  80. package/corpus/templates/content/app/global.css +9 -0
  81. package/corpus/templates/content/app/i18n/zh-TW.ts +12 -0
  82. package/corpus/templates/content/app/i18n-data.ts +140 -0
  83. package/corpus/templates/content/changelog/2026-06-29-reusable-mdx-references-now-preview-linked-local-documents-i.md +6 -0
  84. package/corpus/templates/content/shared/builder-docs-blocks.ts +8 -0
  85. package/corpus/templates/content/shared/builder-mdx.ts +141 -4
  86. package/corpus/templates/content/shared/nfm.ts +62 -0
  87. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +10 -11
  88. package/corpus/templates/design/AGENTS.md +5 -0
  89. package/corpus/templates/design/_gitignore +7 -0
  90. package/corpus/templates/design/actions/apply-visual-edit.ts +22 -12
  91. package/corpus/templates/design/actions/create-design-system.ts +15 -4
  92. package/corpus/templates/design/actions/create-file.ts +20 -1
  93. package/corpus/templates/design/actions/delete-design-system.ts +2 -2
  94. package/corpus/templates/design/actions/duplicate-design.ts +35 -9
  95. package/corpus/templates/design/actions/generate-design.ts +146 -69
  96. package/corpus/templates/design/actions/generate-screens.ts +51 -19
  97. package/corpus/templates/design/actions/get-design-snapshot.ts +5 -1
  98. package/corpus/templates/design/actions/get-design.ts +2 -0
  99. package/corpus/templates/design/actions/import-design-project.ts +28 -9
  100. package/corpus/templates/design/actions/import-document.ts +22 -8
  101. package/corpus/templates/design/actions/index-design-system-with-builder.ts +6 -153
  102. package/corpus/templates/design/actions/navigate.ts +105 -54
  103. package/corpus/templates/design/actions/set-default-design-system.ts +17 -3
  104. package/corpus/templates/design/actions/show-design-questions.ts +2 -0
  105. package/corpus/templates/design/actions/update-design.ts +2 -1
  106. package/corpus/templates/design/actions/view-screen.ts +12 -6
  107. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +4 -4
  108. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +159 -41
  109. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +8 -4
  110. package/corpus/templates/design/app/components/design/DrawOverlay.tsx +37 -42
  111. package/corpus/templates/design/app/components/design/EditPanel.tsx +336 -183
  112. package/corpus/templates/design/app/components/design/LayersPanel.tsx +153 -34
  113. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +135 -33
  114. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +6 -0
  115. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +2 -1
  116. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +4 -10
  117. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +84 -43
  118. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +4 -4
  119. package/corpus/templates/design/app/components/design/inspector/{FigmaColorPicker.tsx → DesignColorPicker.tsx} +89 -62
  120. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +14 -26
  121. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +40 -12
  122. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +13 -4
  123. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +13 -13
  124. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +13 -9
  125. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +13 -4
  126. package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +6 -6
  127. package/corpus/templates/design/app/components/design/inspector/{figma-icons.tsx → design-icons.tsx} +2 -2
  128. package/corpus/templates/design/app/components/design/inspector/index.ts +12 -12
  129. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +5 -2
  130. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +3 -3
  131. package/corpus/templates/design/app/components/layout/Layout.tsx +5 -2
  132. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  133. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +167 -91
  134. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +16 -3
  135. package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +15 -0
  136. package/corpus/templates/design/app/global.css +4 -0
  137. package/corpus/templates/design/app/hooks/use-design-system.ts +4 -1
  138. package/corpus/templates/design/app/hooks/use-navigation-state.ts +38 -3
  139. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +9 -2
  140. package/corpus/templates/design/app/i18n/zh-TW.ts +22 -0
  141. package/corpus/templates/design/app/i18n-data.ts +527 -0
  142. package/corpus/templates/design/app/lib/design-systems.ts +6 -4
  143. package/corpus/templates/design/app/lib/design-templates.ts +15 -3
  144. package/corpus/templates/design/app/pages/DesignEditor.tsx +641 -158
  145. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +42 -34
  146. package/corpus/templates/design/app/pages/VisualEdit.tsx +100 -0
  147. package/corpus/templates/design/app/root.tsx +42 -9
  148. package/corpus/templates/design/app/routes/visual-edit.tsx +7 -0
  149. package/corpus/templates/design/changelog/2026-06-28-live-cursors-now-keep-names-beside-a-cleaner-pointer-marker.md +1 -1
  150. package/corpus/templates/design/changelog/2026-06-28-mode-switching-now-lives-in-a-compact-bottom-right-icon-dock.md +1 -1
  151. package/corpus/templates/design/changelog/2026-06-28-visual-code-design-mode.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-29-design-generation-prompts-now-show-connect-ai-before-sending.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +1 -1
  154. package/corpus/templates/design/changelog/2026-06-29-fixed-design-editor-exports-and-gradient-edits-so-multi-scre.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-29-longer-hosted-agent-turns.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-29-png-export-handles-modern-css-color-functions-more-reliably.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-29-png-export-now-ignores-duplicate-clicks-while-a-download-is-.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-29-public-visual-edit-and-shared-design-links-now-invite-visito.md +6 -0
  159. package/corpus/templates/design/changelog/2026-06-29-visual-edit-actions-now-reject-missing-design-targets-before.md +6 -0
  160. package/corpus/templates/design/e2e/README.md +71 -0
  161. package/corpus/templates/design/e2e/global-setup.ts +157 -0
  162. package/corpus/templates/design/e2e/helpers.ts +148 -0
  163. package/corpus/templates/design/netlify.toml +3 -4
  164. package/corpus/templates/design/package.json +5 -1
  165. package/corpus/templates/design/playwright.config.ts +51 -0
  166. package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +121 -0
  167. package/corpus/templates/design/server/handlers/uploads.ts +89 -38
  168. package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +181 -0
  169. package/corpus/templates/design/server/lib/coding-handoff.ts +33 -4
  170. package/corpus/templates/design/server/lib/design-export.ts +35 -8
  171. package/corpus/templates/design/server/plugins/agent-chat.ts +4 -0
  172. package/corpus/templates/design/server/plugins/auth.ts +10 -1
  173. package/corpus/templates/design/server/routes/api/index-design-system-with-builder.post.ts +1 -0
  174. package/corpus/templates/design/shared/api.ts +2 -0
  175. package/corpus/templates/design/shared/apply-edits.ts +12 -8
  176. package/corpus/templates/design/shared/canvas-math.ts +19 -1
  177. package/corpus/templates/design/shared/code-layer.ts +87 -28
  178. package/corpus/templates/design/shared/generation-session.ts +1 -0
  179. package/corpus/templates/design/shared/resolve-tweaks.ts +4 -3
  180. package/corpus/templates/design/shared/shader-presets.ts +10 -0
  181. package/corpus/templates/design/shared/source-mode.ts +9 -0
  182. package/corpus/templates/design/vitest.config.ts +2 -1
  183. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +1 -1
  184. package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +1 -1
  185. package/corpus/templates/plan/.agents/skills/visual-plan/references/local-files.md +14 -5
  186. package/corpus/templates/plan/.agents/skills/visual-recap/references/local-files.md +14 -5
  187. package/corpus/templates/plan/AGENTS.md +1 -1
  188. package/corpus/templates/plan/actions/create-plan-design.ts +1 -1
  189. package/corpus/templates/plan/actions/validate-local-plan-source.ts +110 -0
  190. package/corpus/templates/plan/app/pages/PlansPage.tsx +1 -0
  191. package/corpus/templates/plan/docs/commenting-ux-plan.md +2 -2
  192. package/corpus/templates/plan/server/lib/plan-connector-catalog.ts +1 -0
  193. package/corpus/templates/plan/server/lib/public-action-paths.ts +1 -0
  194. package/corpus/templates/plan/server/plan-content.ts +1 -1
  195. package/corpus/templates/plan/server/ui-plan-html.ts +2 -2
  196. package/corpus/templates/slides/actions/index-design-system-with-builder.ts +6 -153
  197. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +53 -38
  198. package/corpus/templates/slides/app/components/design-system/{fig-import-response.ts → builder-index-response.ts} +9 -6
  199. package/corpus/templates/slides/app/components/visual-editor/MultiSelectChip.tsx +1 -1
  200. package/corpus/templates/slides/changelog/2026-06-29-design-system-setup-now-indexes-uploaded-design-files-throug.md +6 -0
  201. package/corpus/templates/slides/changelog/2026-06-29-longer-hosted-agent-turns.md +6 -0
  202. package/corpus/templates/slides/netlify.toml +3 -6
  203. package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +116 -0
  204. package/corpus/templates/slides/server/lib/builder-design-system-proxy.ts +179 -0
  205. package/corpus/templates/slides/server/plugins/agent-chat.ts +4 -1
  206. package/corpus/templates/slides/server/routes/api/index-design-system-with-builder.post.ts +1 -0
  207. package/dist/action.d.ts +23 -0
  208. package/dist/action.d.ts.map +1 -1
  209. package/dist/action.js +9 -0
  210. package/dist/action.js.map +1 -1
  211. package/dist/agent/durable-background.d.ts +9 -7
  212. package/dist/agent/durable-background.d.ts.map +1 -1
  213. package/dist/agent/durable-background.js +27 -11
  214. package/dist/agent/durable-background.js.map +1 -1
  215. package/dist/agent/production-agent.d.ts +10 -0
  216. package/dist/agent/production-agent.d.ts.map +1 -1
  217. package/dist/agent/production-agent.js +4 -1
  218. package/dist/agent/production-agent.js.map +1 -1
  219. package/dist/agent/run-loop-with-resume.d.ts +2 -1
  220. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  221. package/dist/agent/run-loop-with-resume.js +2 -2
  222. package/dist/agent/run-loop-with-resume.js.map +1 -1
  223. package/dist/brand-kit/fig/index.d.ts +4 -4
  224. package/dist/brand-kit/fig/index.d.ts.map +1 -1
  225. package/dist/brand-kit/fig/index.js +14 -27
  226. package/dist/brand-kit/fig/index.js.map +1 -1
  227. package/dist/cli/index.js +1 -1
  228. package/dist/cli/index.js.map +1 -1
  229. package/dist/cli/mcp-config-writers.d.ts +15 -7
  230. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  231. package/dist/cli/mcp-config-writers.js +198 -45
  232. package/dist/cli/mcp-config-writers.js.map +1 -1
  233. package/dist/cli/plan-local.d.ts +30 -0
  234. package/dist/cli/plan-local.d.ts.map +1 -1
  235. package/dist/cli/plan-local.js +281 -56
  236. package/dist/cli/plan-local.js.map +1 -1
  237. package/dist/cli/skills.d.ts +2 -2
  238. package/dist/cli/skills.d.ts.map +1 -1
  239. package/dist/cli/skills.js +32 -8
  240. package/dist/cli/skills.js.map +1 -1
  241. package/dist/client/AssistantChat.d.ts.map +1 -1
  242. package/dist/client/AssistantChat.js +11 -1
  243. package/dist/client/AssistantChat.js.map +1 -1
  244. package/dist/client/agent-chat.d.ts.map +1 -1
  245. package/dist/client/agent-chat.js +15 -2
  246. package/dist/client/agent-chat.js.map +1 -1
  247. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  248. package/dist/client/composer/PromptComposer.js +35 -1
  249. package/dist/client/composer/PromptComposer.js.map +1 -1
  250. package/dist/client/org/OrgSwitcher.js +1 -1
  251. package/dist/client/org/OrgSwitcher.js.map +1 -1
  252. package/dist/client/sharing/ShareButton.d.ts +1 -1
  253. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  254. package/dist/client/sharing/ShareButton.js +2 -15
  255. package/dist/client/sharing/ShareButton.js.map +1 -1
  256. package/dist/collab/presence.d.ts +1 -1
  257. package/dist/collab/presence.js +1 -1
  258. package/dist/collab/presence.js.map +1 -1
  259. package/dist/collab/routes.d.ts +1 -1
  260. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  261. package/dist/mcp/connect-route.d.ts.map +1 -1
  262. package/dist/mcp/connect-route.js +34 -5
  263. package/dist/mcp/connect-route.js.map +1 -1
  264. package/dist/observability/routes.d.ts +2 -2
  265. package/dist/progress/routes.d.ts +1 -1
  266. package/dist/resources/handlers.d.ts +1 -1
  267. package/dist/server/action-discovery.d.ts.map +1 -1
  268. package/dist/server/action-discovery.js +5 -0
  269. package/dist/server/action-discovery.js.map +1 -1
  270. package/dist/server/action-routes.d.ts.map +1 -1
  271. package/dist/server/action-routes.js +14 -0
  272. package/dist/server/action-routes.js.map +1 -1
  273. package/dist/server/agent-chat-plugin.d.ts +6 -0
  274. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  275. package/dist/server/agent-chat-plugin.js +12 -3
  276. package/dist/server/agent-chat-plugin.js.map +1 -1
  277. package/dist/vite/client.d.ts.map +1 -1
  278. package/dist/vite/client.js +12 -1
  279. package/dist/vite/client.js.map +1 -1
  280. package/docs/content/locales/ar-SA/real-time-collaboration.mdx +2 -2
  281. package/docs/content/locales/ar-SA/template-design.mdx +1 -1
  282. package/docs/content/locales/de-DE/real-time-collaboration.mdx +3 -3
  283. package/docs/content/locales/de-DE/template-design.mdx +1 -1
  284. package/docs/content/locales/es-ES/real-time-collaboration.mdx +2 -2
  285. package/docs/content/locales/es-ES/template-design.mdx +1 -1
  286. package/docs/content/locales/fr-FR/real-time-collaboration.mdx +2 -2
  287. package/docs/content/locales/fr-FR/template-design.mdx +1 -1
  288. package/docs/content/locales/hi-IN/template-design.mdx +1 -1
  289. package/docs/content/locales/ja-JP/real-time-collaboration.mdx +2 -2
  290. package/docs/content/locales/ja-JP/template-design.mdx +1 -1
  291. package/docs/content/locales/ko-KR/real-time-collaboration.mdx +2 -2
  292. package/docs/content/locales/ko-KR/template-design.mdx +1 -1
  293. package/docs/content/locales/pt-BR/real-time-collaboration.mdx +2 -2
  294. package/docs/content/locales/pt-BR/template-design.mdx +1 -1
  295. package/docs/content/locales/zh-CN/real-time-collaboration.mdx +2 -2
  296. package/docs/content/locales/zh-CN/template-design.mdx +1 -1
  297. package/docs/content/locales/zh-TW/real-time-collaboration.mdx +2 -2
  298. package/docs/content/locales/zh-TW/template-design.mdx +1 -1
  299. package/docs/content/real-time-collaboration.mdx +2 -2
  300. package/docs/content/template-design.mdx +1 -1
  301. package/package.json +3 -7
  302. package/corpus/core/src/brand-kit/fig/decode.ts +0 -410
  303. package/corpus/core/src/brand-kit/fig/extract-design-system.ts +0 -947
  304. package/corpus/core/src/brand-kit/fig/fig-to-html.ts +0 -1906
  305. package/corpus/templates/design/actions/import-figma.ts +0 -55
  306. package/corpus/templates/design/changelog/2026-06-28-figma-style-code-design-mode.md +0 -6
  307. package/corpus/templates/design/server/handlers/import-figma-system.ts +0 -86
  308. package/corpus/templates/design/server/routes/api/import-figma-system.post.ts +0 -1
  309. package/corpus/templates/slides/server/handlers/import-figma-system.ts +0 -101
  310. package/corpus/templates/slides/server/routes/api/import-figma-system.post.ts +0 -1
  311. package/dist/brand-kit/fig/decode.d.ts +0 -33
  312. package/dist/brand-kit/fig/decode.d.ts.map +0 -1
  313. package/dist/brand-kit/fig/decode.js +0 -358
  314. package/dist/brand-kit/fig/decode.js.map +0 -1
  315. package/dist/brand-kit/fig/extract-design-system.d.ts +0 -44
  316. package/dist/brand-kit/fig/extract-design-system.d.ts.map +0 -1
  317. package/dist/brand-kit/fig/extract-design-system.js +0 -752
  318. package/dist/brand-kit/fig/extract-design-system.js.map +0 -1
  319. package/dist/brand-kit/fig/fig-to-html.d.ts +0 -246
  320. package/dist/brand-kit/fig/fig-to-html.d.ts.map +0 -1
  321. package/dist/brand-kit/fig/fig-to-html.js +0 -1506
  322. package/dist/brand-kit/fig/fig-to-html.js.map +0 -1
@@ -0,0 +1,157 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { chromium, type FullConfig } from "@playwright/test";
5
+
6
+ /**
7
+ * Global setup: authenticate a test user (email/password; there is no dev auth
8
+ * bypass) and seed one design with a known fixture HTML so specs run against
9
+ * deterministic content. Writes:
10
+ * e2e/.auth/state.json - signed session storageState
11
+ * e2e/.auth/seed.json - { designId } of the seeded design
12
+ */
13
+
14
+ export const E2E_EMAIL = "e2e@local.test";
15
+ export const E2E_PASSWORD = "password-e2e-1234";
16
+ export const SEED_TITLE = "E2E Seed Design";
17
+
18
+ const AUTH_DIR = path.join(import.meta.dirname, ".auth");
19
+ const STATE_PATH = path.join(AUTH_DIR, "state.json");
20
+ const SEED_PATH = path.join(AUTH_DIR, "seed.json");
21
+
22
+ /**
23
+ * Fixture HTML with distinct, text-identifiable elements. Plain inline styles
24
+ * (no CDN) so the layout is deterministic and offline. The flex row of two
25
+ * buttons exercises reorder/move; headings and paragraphs exercise select.
26
+ */
27
+ export const FIXTURE_HTML = `<!doctype html>
28
+ <html lang="en">
29
+ <head>
30
+ <meta charset="utf-8" />
31
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
32
+ <title>E2E Fixture</title>
33
+ </head>
34
+ <body style="margin:0;font-family:system-ui,sans-serif;background:#0f1115;color:#f4f4f5">
35
+ <main style="max-width:720px;margin:0 auto;padding:48px 32px;display:flex;flex-direction:column;gap:24px">
36
+ <h1 style="font-size:40px;font-weight:800;margin:0">E2E Hero Heading</h1>
37
+ <p style="font-size:18px;line-height:1.6;margin:0;color:#a1a1aa">First fixture paragraph for selection tests.</p>
38
+ <p style="font-size:18px;line-height:1.6;margin:0;color:#a1a1aa">Second fixture paragraph for selection tests.</p>
39
+ <div style="display:flex;flex-direction:row;gap:16px">
40
+ <button style="padding:14px 28px;border-radius:10px;border:0;background:#6366f1;color:#fff;font-size:16px">Alpha Button</button>
41
+ <button style="padding:14px 28px;border-radius:10px;border:0;background:#22c55e;color:#06240f;font-size:16px">Beta Button</button>
42
+ </div>
43
+ <section style="margin-top:16px;padding:24px;border-radius:14px;background:#1a1d24">
44
+ <h2 style="font-size:24px;margin:0 0 8px">Fixture Card Title</h2>
45
+ <p style="margin:0;color:#a1a1aa">Card body text inside a nested container.</p>
46
+ </section>
47
+ </main>
48
+ </body>
49
+ </html>`;
50
+
51
+ async function postAction(
52
+ request: import("@playwright/test").APIRequestContext,
53
+ baseURL: string,
54
+ name: string,
55
+ input: Record<string, unknown>,
56
+ ): Promise<any> {
57
+ const res = await request.post(`${baseURL}/_agent-native/actions/${name}`, {
58
+ data: input,
59
+ headers: { "Content-Type": "application/json" },
60
+ });
61
+ if (!res.ok()) {
62
+ throw new Error(
63
+ `action ${name} failed: ${res.status()} ${await res.text()}`,
64
+ );
65
+ }
66
+ return res.json();
67
+ }
68
+
69
+ export default async function globalSetup(config: FullConfig) {
70
+ const baseURL =
71
+ (config.projects[0]?.use?.baseURL as string | undefined) ??
72
+ "http://127.0.0.1:9333";
73
+ await mkdir(AUTH_DIR, { recursive: true });
74
+
75
+ const browser = await chromium.launch();
76
+ const context = await browser.newContext();
77
+ const page = await context.newPage();
78
+
79
+ try {
80
+ await page.goto(`${baseURL}/`, { waitUntil: "domcontentloaded" });
81
+
82
+ const isSignIn = async () => /sign in/i.test(await page.title());
83
+
84
+ if (await isSignIn()) {
85
+ // Try to create the account; if it already exists, fall back to sign in.
86
+ await page.getByRole("textbox", { name: "Email" }).fill(E2E_EMAIL);
87
+ await page
88
+ .getByRole("textbox", { name: "Password", exact: true })
89
+ .fill(E2E_PASSWORD);
90
+ const confirm = page.getByRole("textbox", { name: "Confirm password" });
91
+ if (await confirm.count()) await confirm.fill(E2E_PASSWORD);
92
+ await page
93
+ .locator("#signup-form")
94
+ .getByRole("button", { name: "Create account" })
95
+ .click()
96
+ .catch(() => {});
97
+ await page.waitForTimeout(2500);
98
+
99
+ if (await isSignIn()) {
100
+ // Account exists; switch to the Sign in tab and log in.
101
+ await page
102
+ .getByRole("button", { name: "Sign in", exact: true })
103
+ .first()
104
+ .click()
105
+ .catch(() => {});
106
+ await page.getByRole("textbox", { name: "Email" }).fill(E2E_EMAIL);
107
+ await page
108
+ .getByRole("textbox", { name: "Password", exact: true })
109
+ .fill(E2E_PASSWORD);
110
+ await page
111
+ .getByRole("button", { name: /sign in/i })
112
+ .last()
113
+ .click()
114
+ .catch(() => {});
115
+ await page.waitForTimeout(2500);
116
+ }
117
+ }
118
+
119
+ await page
120
+ .waitForFunction(() => !/sign in/i.test(document.title), null, {
121
+ timeout: 20_000,
122
+ })
123
+ .catch(() => {});
124
+
125
+ await context.storageState({ path: STATE_PATH });
126
+
127
+ // Seed a design + fixture file via the authenticated action surface.
128
+ const created = await postAction(
129
+ context.request,
130
+ baseURL,
131
+ "create-design",
132
+ {
133
+ title: SEED_TITLE,
134
+ projectType: "prototype",
135
+ },
136
+ );
137
+ const designId: string =
138
+ created?.id ?? created?.data?.id ?? created?.design?.id;
139
+ if (!designId) {
140
+ throw new Error(
141
+ `create-design did not return an id: ${JSON.stringify(created)}`,
142
+ );
143
+ }
144
+ await postAction(context.request, baseURL, "create-file", {
145
+ designId,
146
+ filename: "index.html",
147
+ content: FIXTURE_HTML,
148
+ fileType: "html",
149
+ });
150
+
151
+ await writeFile(SEED_PATH, JSON.stringify({ designId }, null, 2));
152
+ // eslint-disable-next-line no-console
153
+ console.log(`[e2e] seeded design ${designId} for ${E2E_EMAIL}`);
154
+ } finally {
155
+ await browser.close();
156
+ }
157
+ }
@@ -0,0 +1,148 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { expect, type Page, type FrameLocator } from "@playwright/test";
5
+
6
+ /**
7
+ * Helpers for driving the Design visual editor in real Chrome.
8
+ *
9
+ * Hard-won facts for this editor:
10
+ * - The design renders inside an iframe, so tests should use a frame locator
11
+ * (`page.frameLocator('iframe')`) instead of parent-page CSS selectors.
12
+ * - A pointer-capturing shield `<div data-agent-native-edit-overlay="shield">`
13
+ * sits on top inside the iframe, so clicks need `{ force: true }`.
14
+ * - Selection/edits are reported to the parent via postMessage
15
+ * (`element-select`, `element-hover`, `visual-style-change`,
16
+ * `visual-structure-change`). Assert on those + the parent inspector DOM.
17
+ * - `page.screenshot()` HANGS (the page never idles). Use `cdpScreenshot()`.
18
+ */
19
+
20
+ export async function readSeedDesignId(): Promise<string> {
21
+ const seedPath = path.join(import.meta.dirname, ".auth", "seed.json");
22
+ const raw = await readFile(seedPath, "utf8");
23
+ const { designId } = JSON.parse(raw) as { designId: string };
24
+ if (!designId) throw new Error("no seeded designId - global-setup failed");
25
+ return designId;
26
+ }
27
+
28
+ export function designFrame(page: Page): FrameLocator {
29
+ return page.frameLocator("iframe");
30
+ }
31
+
32
+ /** Open the editor for a design and wait for the toolbar + iframe to be ready. */
33
+ export async function gotoEditor(page: Page, designId: string): Promise<void> {
34
+ await page.goto(`/design/${designId}`, { waitUntil: "domcontentloaded" });
35
+ await expect(
36
+ page.getByRole("button", { name: "Move", exact: true }),
37
+ ).toBeVisible({ timeout: 30_000 });
38
+ await expect(page.locator("iframe").first()).toBeVisible();
39
+ // Wait for the iframe bridge to stamp at least one selectable node.
40
+ await expect
41
+ .poll(
42
+ async () =>
43
+ designFrame(page)
44
+ .locator("[data-agent-native-node-id], h1, h2, p, button")
45
+ .count(),
46
+ { timeout: 20_000 },
47
+ )
48
+ .toBeGreaterThan(0);
49
+ }
50
+
51
+ /** Start capturing bridge postMessages on the parent window. */
52
+ export async function installBridge(page: Page): Promise<void> {
53
+ await page.evaluate(() => {
54
+ (window as any).__bridge = [];
55
+ window.addEventListener("message", (e: MessageEvent) => {
56
+ const t = (e.data as any)?.type;
57
+ if (typeof t === "string" && /^(element-|visual-)/.test(t)) {
58
+ (window as any).__bridge.push(e.data);
59
+ }
60
+ });
61
+ });
62
+ }
63
+
64
+ export async function bridgeMessages(page: Page): Promise<any[]> {
65
+ return page.evaluate(() => (window as any).__bridge ?? []);
66
+ }
67
+
68
+ /** Wait until a bridge message of `type` arrives, returning its payload. */
69
+ export async function waitForBridge(
70
+ page: Page,
71
+ type: string,
72
+ timeout = 8_000,
73
+ ): Promise<any> {
74
+ const handle = await page.waitForFunction(
75
+ (t) =>
76
+ ((window as any).__bridge ?? []).find((m: any) => m.type === t) ?? null,
77
+ type,
78
+ { timeout },
79
+ );
80
+ return handle.jsonValue();
81
+ }
82
+
83
+ /**
84
+ * Click an element inside the design iframe by its visible text and return the
85
+ * resulting `element-select` payload. Uses force:true to punch through the
86
+ * shield overlay (which is what actually drives selection).
87
+ */
88
+ export async function selectByText(page: Page, text: string): Promise<any> {
89
+ await installBridge(page);
90
+ await page.evaluate(() => ((window as any).__bridge = []));
91
+ await designFrame(page)
92
+ .getByText(text, { exact: false })
93
+ .first()
94
+ .click({ force: true, timeout: 8_000 });
95
+ const sel = await waitForBridge(page, "element-select");
96
+ return sel?.payload ?? sel;
97
+ }
98
+
99
+ /** Number of inputs in the right-hand inspector (proxy for "inspector populated"). */
100
+ export async function inspectorInputCount(page: Page): Promise<number> {
101
+ return page.locator("input").count();
102
+ }
103
+
104
+ /**
105
+ * Drag an element inside the iframe by `(dx, dy)` parent-page pixels using real
106
+ * pointer events (the canvas bridge handles mousedown/mousemove/mouseup).
107
+ * Selects it first so the move/reorder interaction is armed.
108
+ */
109
+ export async function dragCanvasByText(
110
+ page: Page,
111
+ text: string,
112
+ dx: number,
113
+ dy: number,
114
+ ): Promise<string[]> {
115
+ await selectByText(page, text);
116
+ await page.evaluate(() => ((window as any).__bridge = []));
117
+ const box = await designFrame(page)
118
+ .getByText(text, { exact: false })
119
+ .first()
120
+ .boundingBox();
121
+ if (!box) throw new Error(`no bounding box for "${text}"`);
122
+ const cx = box.x + box.width / 2;
123
+ const cy = box.y + box.height / 2;
124
+ await page.mouse.move(cx, cy);
125
+ await page.mouse.down();
126
+ const steps = 10;
127
+ for (let i = 1; i <= steps; i++) {
128
+ await page.mouse.move(cx + (dx * i) / steps, cy + (dy * i) / steps);
129
+ }
130
+ await page.waitForTimeout(150);
131
+ await page.mouse.up();
132
+ await page.waitForTimeout(400);
133
+ const msgs = await bridgeMessages(page);
134
+ return [...new Set(msgs.map((m) => m.type))];
135
+ }
136
+
137
+ /** Screenshot via CDP; bypasses Playwright's stability wait, which never settles. */
138
+ export async function cdpScreenshot(
139
+ page: Page,
140
+ filePath: string,
141
+ ): Promise<void> {
142
+ const client = await page.context().newCDPSession(page);
143
+ const { data } = await client.send("Page.captureScreenshot", {
144
+ format: "png",
145
+ });
146
+ const { writeFile } = await import("node:fs/promises");
147
+ await writeFile(filePath, Buffer.from(data, "base64"));
148
+ }
@@ -8,10 +8,9 @@ functions = "templates/design/.netlify/functions-internal"
8
8
  GA_MEASUREMENT_ID = "G-ESF7FYXGN9"
9
9
  NITRO_PRESET = "netlify"
10
10
  NPM_CONFIG_PRODUCTION = "false"
11
+ AGENT_CHAT_DURABLE_BACKGROUND = "true"
11
12
 
12
- # A2A delegation calls run a full agent loop (LLM + tool calls + DB queries).
13
- # The framework does not impose a default agent-run timeout; if a template sets
14
- # AGENT_RUN_SOFT_TIMEOUT_MS, keep it below this function timeout so the hidden
15
- # auto-continuation path can hand off before Netlify kills the function.
13
+ # Emit the Netlify background function used by Design's durable agent-chat opt-in.
14
+ # The normal server timeout still protects non-background HTTP handlers.
16
15
  [functions."*"]
17
16
  timeout = 75
@@ -10,7 +10,10 @@
10
10
  "test": "vitest --run",
11
11
  "typecheck": "agent-native typecheck",
12
12
  "action": "agent-native action",
13
- "script": "agent-native script"
13
+ "script": "agent-native script",
14
+ "e2e": "playwright test",
15
+ "e2e:headed": "playwright test --headed",
16
+ "e2e:ui": "playwright test --ui"
14
17
  },
15
18
  "dependencies": {
16
19
  "@agent-native/core": "workspace:*",
@@ -31,6 +34,7 @@
31
34
  "zod": "^4.3.6"
32
35
  },
33
36
  "devDependencies": {
37
+ "@playwright/test": "^1.61.1",
34
38
  "@radix-ui/react-accordion": "^1.2.12",
35
39
  "@radix-ui/react-alert-dialog": "^1.1.15",
36
40
  "@radix-ui/react-aspect-ratio": "^1.1.8",
@@ -0,0 +1,51 @@
1
+ import path from "node:path";
2
+
3
+ import { defineConfig, devices } from "@playwright/test";
4
+
5
+ /**
6
+ * E2E config for the Design visual editor.
7
+ *
8
+ * Runs real Chromium against a dev server backed by a throwaway local SQLite
9
+ * database (`data/e2e.db`). `global-setup.ts` signs up a test user, saves the
10
+ * signed session as `storageState`, and seeds one design with a known fixture.
11
+ *
12
+ * Run: `pnpm e2e` (headless), `pnpm e2e:headed`, `pnpm e2e:ui`.
13
+ * Override the target with `E2E_BASE_URL` (e.g. point at an already-running
14
+ * server on :9300); then `webServer.reuseExistingServer` keeps it.
15
+ */
16
+ const PORT = Number(process.env.E2E_PORT ?? 9333);
17
+ const BASE_URL = process.env.E2E_BASE_URL ?? `http://127.0.0.1:${PORT}`;
18
+
19
+ export default defineConfig({
20
+ testDir: "./e2e",
21
+ // The editor is heavy (iframe bridge + polling); give generous budgets.
22
+ timeout: 90_000,
23
+ expect: { timeout: 15_000 },
24
+ fullyParallel: false,
25
+ workers: 1,
26
+ retries: process.env.CI ? 1 : 0,
27
+ reporter: process.env.CI ? "github" : "list",
28
+ globalSetup: path.join(import.meta.dirname, "e2e", "global-setup.ts"),
29
+ use: {
30
+ baseURL: BASE_URL,
31
+ storageState: path.join(import.meta.dirname, "e2e", ".auth", "state.json"),
32
+ trace: "on-first-retry",
33
+ actionTimeout: 15_000,
34
+ navigationTimeout: 30_000,
35
+ },
36
+ projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
37
+ webServer: process.env.E2E_BASE_URL
38
+ ? undefined
39
+ : {
40
+ // APP_NAME + the app-prefixed DESIGN_DATABASE_URL is checked BEFORE the
41
+ // generic DATABASE_URL, so a `.env` Postgres URL can never override this
42
+ // throwaway local SQLite db.
43
+ command: `APP_NAME=design DESIGN_DATABASE_URL="file:./data/e2e.db" PORT=${PORT} pnpm dev`,
44
+ url: BASE_URL,
45
+ reuseExistingServer: !process.env.CI,
46
+ // Cold Vite dep-optimization on first boot can exceed two minutes.
47
+ timeout: 300_000,
48
+ stdout: "ignore",
49
+ stderr: "pipe",
50
+ },
51
+ });
@@ -0,0 +1,121 @@
1
+ import {
2
+ FeatureNotConfiguredError,
3
+ getSession,
4
+ startBuilderDesignSystemIndex,
5
+ } from "@agent-native/core/server";
6
+ import {
7
+ defineEventHandler,
8
+ getRequestHeader,
9
+ readMultipartFormData,
10
+ setResponseStatus,
11
+ } from "h3";
12
+
13
+ import { upsertBuilderProxyDesignSystem } from "../lib/builder-design-system-proxy.js";
14
+
15
+ const MAX_FIG_BYTES = 200 * 1024 * 1024;
16
+ const MULTIPART_OVERHEAD_BYTES = 1024 * 1024;
17
+
18
+ function requestContentLength(event: Parameters<typeof getRequestHeader>[0]) {
19
+ const raw = getRequestHeader(event, "content-length");
20
+ if (!raw) return null;
21
+ const parsed = Number(raw);
22
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
23
+ }
24
+
25
+ /**
26
+ * Builder-indexing endpoint: accepts a `.fig` upload (multipart field `file`)
27
+ * and starts Builder's design-system indexing pipeline. The app does not decode
28
+ * Figma files locally; Builder owns extraction, generated docs, and the
29
+ * asynchronous indexing job.
30
+ */
31
+ export const indexDesignSystemWithBuilder = defineEventHandler(
32
+ async (event) => {
33
+ const session = await getSession(event).catch(() => null);
34
+ if (!session?.email) {
35
+ setResponseStatus(event, 401);
36
+ return { error: "Unauthorized" };
37
+ }
38
+
39
+ const contentLength = requestContentLength(event);
40
+ if (
41
+ contentLength !== null &&
42
+ contentLength > MAX_FIG_BYTES + MULTIPART_OVERHEAD_BYTES
43
+ ) {
44
+ setResponseStatus(event, 413);
45
+ return {
46
+ error: `File too large (max ${Math.round(MAX_FIG_BYTES / 1024 / 1024)} MB).`,
47
+ };
48
+ }
49
+
50
+ let parts;
51
+ try {
52
+ parts = await readMultipartFormData(event);
53
+ } catch {
54
+ setResponseStatus(event, 413);
55
+ return { error: "Upload too large or malformed." };
56
+ }
57
+ const part = parts?.find(
58
+ (p) => (p.name === "file" || p.name === "fig") && p.data,
59
+ );
60
+ if (!part) {
61
+ setResponseStatus(event, 400);
62
+ return {
63
+ error: "No .fig file uploaded (expected multipart field 'file').",
64
+ };
65
+ }
66
+ if (part.data.length > MAX_FIG_BYTES) {
67
+ setResponseStatus(event, 413);
68
+ return {
69
+ error: `File too large (max ${Math.round(MAX_FIG_BYTES / 1024 / 1024)} MB).`,
70
+ };
71
+ }
72
+
73
+ const suggestedTitle =
74
+ (part.filename || "Imported brand")
75
+ .replace(/\.fig$/i, "")
76
+ .replace(/[-_]+/g, " ")
77
+ .trim() || "Imported brand";
78
+
79
+ try {
80
+ const result = await startBuilderDesignSystemIndex({
81
+ projectName: suggestedTitle,
82
+ files: [
83
+ {
84
+ name: part.filename || "brand.fig",
85
+ data: part.data,
86
+ mimeType: "application/octet-stream",
87
+ },
88
+ ],
89
+ });
90
+ const proxy = await upsertBuilderProxyDesignSystem({
91
+ result,
92
+ ownerEmail: session.email,
93
+ orgId: session.orgId ?? null,
94
+ projectName: suggestedTitle,
95
+ });
96
+ return {
97
+ ...result,
98
+ ...proxy,
99
+ uploadedFileCount: 1,
100
+ };
101
+ } catch (err) {
102
+ if (err instanceof FeatureNotConfiguredError) {
103
+ setResponseStatus(event, 412);
104
+ return {
105
+ error:
106
+ err.message ||
107
+ "Connect Builder.io before indexing a design system from Figma.",
108
+ builderConnectUrl:
109
+ err.builderConnectUrl ?? "/_agent-native/builder/connect",
110
+ };
111
+ }
112
+ setResponseStatus(event, 502);
113
+ return {
114
+ error:
115
+ err instanceof Error
116
+ ? err.message
117
+ : "Builder design-system indexing failed.",
118
+ };
119
+ }
120
+ },
121
+ );
@@ -5,6 +5,7 @@ import path from "path";
5
5
  import { getSession } from "@agent-native/core/server";
6
6
  import {
7
7
  defineEventHandler,
8
+ getRequestHeader,
8
9
  readMultipartFormData,
9
10
  setResponseStatus,
10
11
  } from "h3";
@@ -132,6 +133,20 @@ async function extractUploadText(
132
133
  return {};
133
134
  }
134
135
 
136
+ type UploadedFileResult = {
137
+ path: string;
138
+ originalName: string;
139
+ filename: string;
140
+ type: string;
141
+ size: number;
142
+ textContent?: string;
143
+ textTruncated?: boolean;
144
+ };
145
+
146
+ type InternalUploadedFileResult = UploadedFileResult & {
147
+ _destPath: string;
148
+ };
149
+
135
150
  export const uploadFiles = defineEventHandler(async (event) => {
136
151
  const session = await getSession(event).catch(() => null);
137
152
  if (!session?.email) {
@@ -139,6 +154,21 @@ export const uploadFiles = defineEventHandler(async (event) => {
139
154
  return { error: "Unauthorized" };
140
155
  }
141
156
 
157
+ const MAX_FILES = 20;
158
+ const MAX_FILE_SIZE = 50 * 1024 * 1024;
159
+ // Total-body pre-flight: reject before buffering if Content-Length is clearly
160
+ // over the theoretical max (MAX_FILES × MAX_FILE_SIZE each, plus overhead).
161
+ // Content-Length can be spoofed but cheaply eliminates accidental oversized
162
+ // uploads without allocating any memory for the body first.
163
+ const TOTAL_BODY_LIMIT = MAX_FILES * MAX_FILE_SIZE;
164
+ const contentLength = Number(
165
+ getRequestHeader(event, "content-length") ?? "0",
166
+ );
167
+ if (contentLength > TOTAL_BODY_LIMIT) {
168
+ setResponseStatus(event, 413);
169
+ return { error: "Request body too large" };
170
+ }
171
+
142
172
  const parts = await readMultipartFormData(event);
143
173
  const fileParts = parts?.filter((p) => p.name === "files" && p.data) ?? [];
144
174
 
@@ -147,9 +177,6 @@ export const uploadFiles = defineEventHandler(async (event) => {
147
177
  return { error: "No files uploaded" };
148
178
  }
149
179
 
150
- const MAX_FILES = 20;
151
- const MAX_FILE_SIZE = 50 * 1024 * 1024;
152
-
153
180
  if (fileParts.length > MAX_FILES) {
154
181
  setResponseStatus(event, 413);
155
182
  return { error: `Too many files (max ${MAX_FILES})` };
@@ -161,41 +188,65 @@ export const uploadFiles = defineEventHandler(async (event) => {
161
188
  return { error: "File too large (max 50 MB per file)" };
162
189
  }
163
190
 
164
- try {
165
- return await Promise.all(
166
- fileParts.map(async (part) => {
167
- const originalName = part.filename || "upload";
168
- const filename = safeFilename(originalName);
169
- if (!filename) {
170
- throw new Error(
171
- "Unsupported file type. Allowed: code, docs, text, JSON, CSV, and raster images.",
172
- );
173
- }
174
- const ext = path.extname(filename).toLowerCase();
175
- if (!hasExpectedSignature(ext, part.data)) {
176
- throw new Error(`File contents do not match ${ext} upload type`);
177
- }
178
- const uploadDir = tenantUploadDir(session.email);
179
- await fs.promises.mkdir(uploadDir, { recursive: true });
180
- const destPath = path.join(uploadDir, filename);
181
- await fs.promises.writeFile(destPath, part.data);
182
- const extracted = await extractUploadText(ext, part.data);
183
-
184
- return {
185
- path: path
186
- .relative(process.cwd(), destPath)
187
- .split(path.sep)
188
- .join("/"),
189
- originalName,
190
- filename,
191
- type: part.type || "application/octet-stream",
192
- size: part.data.length,
193
- ...extracted,
194
- };
195
- }),
196
- );
197
- } catch (err) {
191
+ const results = await Promise.allSettled<InternalUploadedFileResult>(
192
+ fileParts.map(async (part) => {
193
+ const originalName = part.filename || "upload";
194
+ const filename = safeFilename(originalName);
195
+ if (!filename) {
196
+ throw new Error(
197
+ "Unsupported file type. Allowed: code, docs, text, JSON, CSV, and raster images.",
198
+ );
199
+ }
200
+ const ext = path.extname(filename).toLowerCase();
201
+ if (!hasExpectedSignature(ext, part.data)) {
202
+ throw new Error(`File contents do not match ${ext} upload type`);
203
+ }
204
+ const uploadDir = tenantUploadDir(session.email);
205
+ await fs.promises.mkdir(uploadDir, { recursive: true });
206
+ const destPath = path.join(uploadDir, filename);
207
+ await fs.promises.writeFile(destPath, part.data);
208
+ const extracted = await extractUploadText(ext, part.data);
209
+
210
+ return {
211
+ // Return the filename (nanoid + ext) as the opaque path token rather
212
+ // than the internal filesystem path so we don't expose the server
213
+ // directory layout or per-tenant hash to the client.
214
+ path: filename,
215
+ originalName,
216
+ filename,
217
+ type: part.type || "application/octet-stream",
218
+ size: part.data.length,
219
+ ...extracted,
220
+ _destPath: destPath,
221
+ };
222
+ }),
223
+ );
224
+
225
+ const failures = results.filter((r) => r.status === "rejected");
226
+ if (failures.length > 0) {
227
+ // Clean up any files that were successfully written before the failure.
228
+ const writtenPaths = results
229
+ .filter((r) => r.status === "fulfilled")
230
+ .map(
231
+ (r) =>
232
+ (r as PromiseFulfilledResult<InternalUploadedFileResult>).value
233
+ ._destPath,
234
+ );
235
+ await Promise.allSettled(writtenPaths.map((p) => fs.promises.unlink(p)));
236
+
237
+ const firstError = (failures[0] as PromiseRejectedResult).reason;
198
238
  setResponseStatus(event, 400);
199
- return { error: err instanceof Error ? err.message : "Invalid upload" };
239
+ return {
240
+ error:
241
+ firstError instanceof Error ? firstError.message : "Invalid upload",
242
+ };
200
243
  }
244
+
245
+ // Strip the internal _destPath field before returning to the client.
246
+ return results.map((r) => {
247
+ const { _destPath: _unused, ...rest } = (
248
+ r as PromiseFulfilledResult<InternalUploadedFileResult>
249
+ ).value;
250
+ return rest;
251
+ });
201
252
  });