@agent-native/core 0.92.7 → 0.92.8

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 (289) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/docs/content/a2a-protocol.mdx +12 -0
  4. package/corpus/core/docs/content/locales/ar-SA/a2a-protocol.mdx +12 -0
  5. package/corpus/core/docs/content/locales/ar-SA/pr-visual-recap.mdx +3 -3
  6. package/corpus/core/docs/content/locales/ar-SA/pure-agent-apps.mdx +4 -0
  7. package/corpus/core/docs/content/locales/de-DE/a2a-protocol.mdx +12 -0
  8. package/corpus/core/docs/content/locales/de-DE/pr-visual-recap.mdx +3 -3
  9. package/corpus/core/docs/content/locales/de-DE/pure-agent-apps.mdx +4 -0
  10. package/corpus/core/docs/content/locales/es-ES/a2a-protocol.mdx +12 -0
  11. package/corpus/core/docs/content/locales/es-ES/pr-visual-recap.mdx +3 -3
  12. package/corpus/core/docs/content/locales/es-ES/pure-agent-apps.mdx +4 -0
  13. package/corpus/core/docs/content/locales/fr-FR/a2a-protocol.mdx +12 -0
  14. package/corpus/core/docs/content/locales/fr-FR/pr-visual-recap.mdx +3 -3
  15. package/corpus/core/docs/content/locales/fr-FR/pure-agent-apps.mdx +4 -0
  16. package/corpus/core/docs/content/locales/hi-IN/a2a-protocol.mdx +12 -0
  17. package/corpus/core/docs/content/locales/hi-IN/pr-visual-recap.mdx +3 -3
  18. package/corpus/core/docs/content/locales/hi-IN/pure-agent-apps.mdx +4 -0
  19. package/corpus/core/docs/content/locales/ja-JP/a2a-protocol.mdx +12 -0
  20. package/corpus/core/docs/content/locales/ja-JP/pr-visual-recap.mdx +3 -3
  21. package/corpus/core/docs/content/locales/ja-JP/pure-agent-apps.mdx +4 -0
  22. package/corpus/core/docs/content/locales/ko-KR/a2a-protocol.mdx +12 -0
  23. package/corpus/core/docs/content/locales/ko-KR/pr-visual-recap.mdx +3 -3
  24. package/corpus/core/docs/content/locales/ko-KR/pure-agent-apps.mdx +4 -0
  25. package/corpus/core/docs/content/locales/pt-BR/a2a-protocol.mdx +12 -0
  26. package/corpus/core/docs/content/locales/pt-BR/pr-visual-recap.mdx +3 -3
  27. package/corpus/core/docs/content/locales/pt-BR/pure-agent-apps.mdx +4 -0
  28. package/corpus/core/docs/content/locales/zh-CN/a2a-protocol.mdx +12 -0
  29. package/corpus/core/docs/content/locales/zh-CN/pr-visual-recap.mdx +3 -3
  30. package/corpus/core/docs/content/locales/zh-CN/pure-agent-apps.mdx +4 -0
  31. package/corpus/core/docs/content/locales/zh-TW/a2a-protocol.mdx +12 -0
  32. package/corpus/core/docs/content/locales/zh-TW/pr-visual-recap.mdx +3 -3
  33. package/corpus/core/docs/content/locales/zh-TW/pure-agent-apps.mdx +4 -0
  34. package/corpus/core/docs/content/pr-visual-recap.mdx +3 -3
  35. package/corpus/core/docs/content/pure-agent-apps.mdx +22 -0
  36. package/corpus/core/package.json +1 -1
  37. package/corpus/core/src/agent/engine/builtin.ts +1 -1
  38. package/corpus/core/src/agent/model-config.ts +30 -30
  39. package/corpus/core/src/agent/production-agent.ts +129 -7
  40. package/corpus/core/src/cli/design-connect.ts +438 -83
  41. package/corpus/core/src/cli/recap.ts +2 -2
  42. package/corpus/core/src/client/AgentPanel.tsx +8 -1
  43. package/corpus/core/src/client/agent-chat-adapter.ts +12 -7
  44. package/corpus/core/src/client/composer/TiptapComposer.tsx +1 -1
  45. package/corpus/core/src/client/index.ts +4 -0
  46. package/corpus/core/src/client/org/TeamPage.tsx +41 -26
  47. package/corpus/core/src/client/use-action.ts +113 -1
  48. package/corpus/core/src/db/client.ts +12 -2
  49. package/corpus/core/src/db/create-get-db.ts +4 -1
  50. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +1 -1
  51. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +1 -1
  52. package/corpus/core/src/server/transcribe-voice.ts +1 -1
  53. package/corpus/core/src/usage/store.ts +13 -4
  54. package/corpus/core/src/vite/client.ts +38 -1
  55. package/corpus/templates/analytics/AGENTS.md +3 -0
  56. package/corpus/templates/analytics/actions/get-data-program.ts +78 -0
  57. package/corpus/templates/analytics/actions/list-data-programs.ts +49 -0
  58. package/corpus/templates/analytics/app/global.css +11 -0
  59. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +163 -17
  60. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  61. package/corpus/templates/analytics/app/i18n-data.ts +20 -0
  62. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +96 -3
  63. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -1
  64. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +61 -4
  65. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +40 -2
  66. package/corpus/templates/analytics/changelog/2026-07-09-analytics-no-longer-forgets-successful-data-queries-when-a-l.md +6 -0
  67. package/corpus/templates/analytics/changelog/2026-07-09-dashboard-charts-keep-consistent-spacing-when-they-stack-ver.md +6 -0
  68. package/corpus/templates/analytics/changelog/2026-07-09-dashboard-charts-tables-and-extensions-can-be-selected-and-d.md +6 -0
  69. package/corpus/templates/analytics/changelog/2026-07-09-long-running-analytics-requests-now-recover-automatically-wh.md +6 -0
  70. package/corpus/templates/analytics/server/db/index.ts +6 -0
  71. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +1 -7
  72. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +102 -15
  73. package/corpus/templates/content/app/hooks/use-content-database.ts +52 -0
  74. package/corpus/templates/content/app/hooks/use-document-properties.ts +50 -6
  75. package/corpus/templates/content/app/i18n/zh-TW.ts +2 -0
  76. package/corpus/templates/content/app/i18n-data.ts +28 -0
  77. package/corpus/templates/content/changelog/2026-07-09-notion-conflict-warnings-no-longer-flash-during-normal-synce.md +6 -0
  78. package/corpus/templates/content/changelog/2026-07-09-property-menus-and-delete-confirmations-now-open-visibly-and.md +6 -0
  79. package/corpus/templates/content/server/lib/notion-sync.ts +130 -28
  80. package/corpus/templates/design/actions/add-breakpoint.ts +48 -68
  81. package/corpus/templates/design/actions/add-localhost-screens.ts +496 -61
  82. package/corpus/templates/design/actions/apply-component-prop-edit.ts +2 -18
  83. package/corpus/templates/design/actions/apply-design-token-edit.ts +51 -82
  84. package/corpus/templates/design/actions/apply-tweaks.ts +63 -41
  85. package/corpus/templates/design/actions/capture-design-state.ts +2 -2
  86. package/corpus/templates/design/actions/connect-localhost.ts +58 -12
  87. package/corpus/templates/design/actions/create-component.ts +2 -18
  88. package/corpus/templates/design/actions/create-design-branch.ts +38 -18
  89. package/corpus/templates/design/actions/create-fusion-app.ts +15 -11
  90. package/corpus/templates/design/actions/delete-file.ts +19 -34
  91. package/corpus/templates/design/actions/deploy-design-preview.ts +59 -27
  92. package/corpus/templates/design/actions/deploy-fusion-app.ts +22 -15
  93. package/corpus/templates/design/actions/export-pdf.ts +2 -1
  94. package/corpus/templates/design/actions/export-zip.ts +9 -3
  95. package/corpus/templates/design/actions/generate-design.ts +107 -70
  96. package/corpus/templates/design/actions/get-component-details.ts +2 -18
  97. package/corpus/templates/design/actions/get-design-branch-diff.ts +2 -3
  98. package/corpus/templates/design/actions/get-design-surface-index.ts +2 -18
  99. package/corpus/templates/design/actions/get-design.ts +2 -1
  100. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +7 -1
  101. package/corpus/templates/design/actions/import-design-tokens.ts +114 -76
  102. package/corpus/templates/design/actions/index-components.ts +2 -18
  103. package/corpus/templates/design/actions/list-design-source-capabilities.ts +23 -28
  104. package/corpus/templates/design/actions/list-local-files.ts +9 -2
  105. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +82 -98
  106. package/corpus/templates/design/actions/open-component-source.ts +2 -18
  107. package/corpus/templates/design/actions/open-visual-edit.ts +19 -1
  108. package/corpus/templates/design/actions/present-design-variants.ts +70 -70
  109. package/corpus/templates/design/actions/preview-component-prop-edit.ts +4 -14
  110. package/corpus/templates/design/actions/read-local-file.ts +9 -2
  111. package/corpus/templates/design/actions/remove-breakpoint.ts +40 -61
  112. package/corpus/templates/design/actions/rename-screen.ts +425 -0
  113. package/corpus/templates/design/actions/request-localhost-write-consent.ts +12 -2
  114. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +9 -2
  115. package/corpus/templates/design/actions/sync-fusion-app.ts +56 -39
  116. package/corpus/templates/design/actions/update-design.ts +376 -61
  117. package/corpus/templates/design/actions/update-file.ts +455 -188
  118. package/corpus/templates/design/actions/write-local-file.ts +10 -2
  119. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +685 -608
  120. package/corpus/templates/design/app/components/design/LayersPanel.tsx +43 -15
  121. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +576 -2015
  122. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +62 -14
  123. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +263 -25
  124. package/corpus/templates/design/app/components/design/design-canvas/annotation-submit.ts +34 -0
  125. package/corpus/templates/design/app/components/design/design-canvas/coordinate-transforms.ts +40 -0
  126. package/corpus/templates/design/app/components/design/design-canvas/creation.ts +47 -0
  127. package/corpus/templates/design/app/components/design/design-canvas/element-payload.ts +27 -0
  128. package/corpus/templates/design/app/components/design/design-canvas/embedded-frame.ts +62 -0
  129. package/corpus/templates/design/app/components/design/design-canvas/external-preview.ts +137 -0
  130. package/corpus/templates/design/app/components/design/design-canvas/file-drop.ts +10 -0
  131. package/corpus/templates/design/app/components/design/design-canvas/hit-test.ts +23 -0
  132. package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +23 -0
  133. package/corpus/templates/design/app/components/design/design-canvas/iframe-pan.ts +177 -0
  134. package/corpus/templates/design/app/components/design/design-canvas/motion-types.ts +7 -0
  135. package/corpus/templates/design/app/components/design/design-canvas/pending-text-edit.ts +62 -0
  136. package/corpus/templates/design/app/components/design/index.ts +2 -2
  137. package/corpus/templates/design/app/components/design/multi-screen/canvas-tools.ts +100 -0
  138. package/corpus/templates/design/app/components/design/multi-screen/coordinate-transforms.ts +81 -0
  139. package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +23 -13
  140. package/corpus/templates/design/app/components/design/multi-screen/culling.ts +238 -19
  141. package/corpus/templates/design/app/components/design/multi-screen/draft-primitives.ts +424 -0
  142. package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +341 -0
  143. package/corpus/templates/design/app/components/design/multi-screen/iframe-targeting.ts +52 -0
  144. package/corpus/templates/design/app/components/design/multi-screen/overview-layout.ts +62 -0
  145. package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +283 -0
  146. package/corpus/templates/design/app/components/design/multi-screen/screen-content-cache.ts +272 -0
  147. package/corpus/templates/design/app/components/design/multi-screen/types.ts +5 -1
  148. package/corpus/templates/design/app/components/design/multi-screen/wheel-gesture-state.ts +12 -0
  149. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +325 -84
  150. package/corpus/templates/design/app/hooks/use-navigation-state.ts +15 -1
  151. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +119 -47
  152. package/corpus/templates/design/app/i18n/zh-TW.ts +9 -0
  153. package/corpus/templates/design/app/i18n-data.ts +223 -0
  154. package/corpus/templates/design/app/lib/design-save-outbox.ts +379 -0
  155. package/corpus/templates/design/app/lib/png-clipboard.ts +113 -0
  156. package/corpus/templates/design/app/pages/design-editor/canvas-primitives.ts +229 -0
  157. package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +918 -0
  158. package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +43 -0
  159. package/corpus/templates/design/app/pages/design-editor/cross-screen-text-color.ts +336 -0
  160. package/corpus/templates/design/app/pages/design-editor/data-operations.ts +270 -0
  161. package/corpus/templates/design/app/pages/design-editor/dom-guards.ts +24 -0
  162. package/corpus/templates/design/app/pages/design-editor/dom-utils.ts +11 -0
  163. package/corpus/templates/design/app/pages/design-editor/editor-session.ts +20 -0
  164. package/corpus/templates/design/app/pages/design-editor/editor-state.ts +408 -0
  165. package/corpus/templates/design/app/pages/design-editor/export-capture.ts +115 -0
  166. package/corpus/templates/design/app/pages/design-editor/geometry-persistence.ts +93 -0
  167. package/corpus/templates/design/app/pages/design-editor/history.ts +169 -0
  168. package/corpus/templates/design/app/pages/design-editor/layout-operations.ts +185 -0
  169. package/corpus/templates/design/app/pages/design-editor/motion-state.ts +309 -0
  170. package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +438 -0
  171. package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +510 -0
  172. package/corpus/templates/design/app/pages/design-editor/portable-style.ts +115 -0
  173. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +337 -0
  174. package/corpus/templates/design/app/pages/design-editor/style-utils.ts +5 -0
  175. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +92 -0
  176. package/corpus/templates/design/app/pages/design-editor/tweak-save.ts +87 -0
  177. package/corpus/templates/design/app/pages/design-editor/types.ts +36 -0
  178. package/corpus/templates/design/app/routes/design.$id.tsx +11 -1
  179. package/corpus/templates/design/changelog/2026-07-09-design-edits-canvas-positions-tools-and-tab-context-now-pers.md +6 -0
  180. package/corpus/templates/design/changelog/2026-07-09-figma-style-shortcuts-drawing-annotations-and-cross-screen-d.md +6 -0
  181. package/corpus/templates/design/changelog/2026-07-09-hand-and-scale-tools-now-keep-their-active-toolbar-identity-.md +6 -0
  182. package/corpus/templates/design/changelog/2026-07-09-large-multi-screen-designs-now-stay-responsive-during-long-c.md +6 -0
  183. package/corpus/templates/design/changelog/2026-07-09-layer-drags-now-land-exactly-where-the-insertion-indicator-s.md +6 -0
  184. package/corpus/templates/design/server/db/index.ts +2 -52
  185. package/corpus/templates/design/server/db/schema.ts +16 -0
  186. package/corpus/templates/design/server/lib/design-data-access.ts +70 -0
  187. package/corpus/templates/design/server/lib/design-data-mutation.ts +249 -0
  188. package/corpus/templates/design/server/lib/fusion-screens.ts +89 -76
  189. package/corpus/templates/design/server/lib/import-design-files.ts +76 -35
  190. package/corpus/templates/design/server/lib/verify-write-grant.ts +7 -2
  191. package/corpus/templates/design/server/plugins/db.ts +19 -0
  192. package/corpus/templates/design/server/source-workspace.ts +12 -13
  193. package/corpus/templates/design/shared/resolve-tweaks.ts +15 -0
  194. package/corpus/templates/design/shared/screen-rename.ts +42 -0
  195. package/corpus/templates/design/shared/source-mode.ts +84 -4
  196. package/dist/agent/engine/builder-engine.d.ts +1 -1
  197. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  198. package/dist/agent/engine/builtin.js +1 -1
  199. package/dist/agent/engine/builtin.js.map +1 -1
  200. package/dist/agent/model-config.d.ts +9 -9
  201. package/dist/agent/model-config.d.ts.map +1 -1
  202. package/dist/agent/model-config.js +30 -30
  203. package/dist/agent/model-config.js.map +1 -1
  204. package/dist/agent/production-agent.d.ts +60 -1
  205. package/dist/agent/production-agent.d.ts.map +1 -1
  206. package/dist/agent/production-agent.js +106 -7
  207. package/dist/agent/production-agent.js.map +1 -1
  208. package/dist/cli/design-connect.d.ts +30 -1
  209. package/dist/cli/design-connect.d.ts.map +1 -1
  210. package/dist/cli/design-connect.js +359 -75
  211. package/dist/cli/design-connect.js.map +1 -1
  212. package/dist/cli/recap.js +2 -2
  213. package/dist/cli/recap.js.map +1 -1
  214. package/dist/client/AgentPanel.d.ts.map +1 -1
  215. package/dist/client/AgentPanel.js +6 -1
  216. package/dist/client/AgentPanel.js.map +1 -1
  217. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  218. package/dist/client/agent-chat-adapter.js +10 -5
  219. package/dist/client/agent-chat-adapter.js.map +1 -1
  220. package/dist/client/composer/TiptapComposer.js +1 -1
  221. package/dist/client/composer/TiptapComposer.js.map +1 -1
  222. package/dist/client/index.d.ts +1 -1
  223. package/dist/client/index.d.ts.map +1 -1
  224. package/dist/client/index.js +1 -1
  225. package/dist/client/index.js.map +1 -1
  226. package/dist/client/org/TeamPage.d.ts.map +1 -1
  227. package/dist/client/org/TeamPage.js +6 -5
  228. package/dist/client/org/TeamPage.js.map +1 -1
  229. package/dist/client/use-action.d.ts +32 -0
  230. package/dist/client/use-action.d.ts.map +1 -1
  231. package/dist/client/use-action.js +68 -1
  232. package/dist/client/use-action.js.map +1 -1
  233. package/dist/collab/struct-routes.d.ts +1 -1
  234. package/dist/db/client.d.ts +3 -0
  235. package/dist/db/client.d.ts.map +1 -1
  236. package/dist/db/client.js +7 -2
  237. package/dist/db/client.js.map +1 -1
  238. package/dist/db/create-get-db.d.ts.map +1 -1
  239. package/dist/db/create-get-db.js +2 -2
  240. package/dist/db/create-get-db.js.map +1 -1
  241. package/dist/notifications/routes.d.ts +3 -3
  242. package/dist/resources/handlers.d.ts +2 -2
  243. package/dist/scripts/agent-engines/manage-agent-engine.js +1 -1
  244. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  245. package/dist/scripts/agent-engines/set-agent-engine.js +1 -1
  246. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  247. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  248. package/dist/server/transcribe-voice.js +1 -1
  249. package/dist/server/transcribe-voice.js.map +1 -1
  250. package/dist/usage/store.d.ts.map +1 -1
  251. package/dist/usage/store.js +13 -4
  252. package/dist/usage/store.js.map +1 -1
  253. package/dist/vite/client.d.ts.map +1 -1
  254. package/dist/vite/client.js +36 -1
  255. package/dist/vite/client.js.map +1 -1
  256. package/docs/content/a2a-protocol.mdx +12 -0
  257. package/docs/content/locales/ar-SA/a2a-protocol.mdx +12 -0
  258. package/docs/content/locales/ar-SA/pr-visual-recap.mdx +3 -3
  259. package/docs/content/locales/ar-SA/pure-agent-apps.mdx +4 -0
  260. package/docs/content/locales/de-DE/a2a-protocol.mdx +12 -0
  261. package/docs/content/locales/de-DE/pr-visual-recap.mdx +3 -3
  262. package/docs/content/locales/de-DE/pure-agent-apps.mdx +4 -0
  263. package/docs/content/locales/es-ES/a2a-protocol.mdx +12 -0
  264. package/docs/content/locales/es-ES/pr-visual-recap.mdx +3 -3
  265. package/docs/content/locales/es-ES/pure-agent-apps.mdx +4 -0
  266. package/docs/content/locales/fr-FR/a2a-protocol.mdx +12 -0
  267. package/docs/content/locales/fr-FR/pr-visual-recap.mdx +3 -3
  268. package/docs/content/locales/fr-FR/pure-agent-apps.mdx +4 -0
  269. package/docs/content/locales/hi-IN/a2a-protocol.mdx +12 -0
  270. package/docs/content/locales/hi-IN/pr-visual-recap.mdx +3 -3
  271. package/docs/content/locales/hi-IN/pure-agent-apps.mdx +4 -0
  272. package/docs/content/locales/ja-JP/a2a-protocol.mdx +12 -0
  273. package/docs/content/locales/ja-JP/pr-visual-recap.mdx +3 -3
  274. package/docs/content/locales/ja-JP/pure-agent-apps.mdx +4 -0
  275. package/docs/content/locales/ko-KR/a2a-protocol.mdx +12 -0
  276. package/docs/content/locales/ko-KR/pr-visual-recap.mdx +3 -3
  277. package/docs/content/locales/ko-KR/pure-agent-apps.mdx +4 -0
  278. package/docs/content/locales/pt-BR/a2a-protocol.mdx +12 -0
  279. package/docs/content/locales/pt-BR/pr-visual-recap.mdx +3 -3
  280. package/docs/content/locales/pt-BR/pure-agent-apps.mdx +4 -0
  281. package/docs/content/locales/zh-CN/a2a-protocol.mdx +12 -0
  282. package/docs/content/locales/zh-CN/pr-visual-recap.mdx +3 -3
  283. package/docs/content/locales/zh-CN/pure-agent-apps.mdx +4 -0
  284. package/docs/content/locales/zh-TW/a2a-protocol.mdx +12 -0
  285. package/docs/content/locales/zh-TW/pr-visual-recap.mdx +3 -3
  286. package/docs/content/locales/zh-TW/pure-agent-apps.mdx +4 -0
  287. package/docs/content/pr-visual-recap.mdx +3 -3
  288. package/docs/content/pure-agent-apps.mdx +22 -0
  289. package/package.json +2 -2
@@ -64,6 +64,11 @@ export interface DesignConnectArgs {
64
64
  * matches the token already stored on the user's connection row (no
65
65
  * self-registration). Also read from AGENT_NATIVE_BRIDGE_TOKEN. */
66
66
  bridgeToken?: string;
67
+ /** Read-only token used by Design browser previews. This is deliberately
68
+ * distinct from `bridgeToken`, which unlocks local filesystem reads/writes.
69
+ * When omitted it is derived one-way from bridgeToken for compatibility
70
+ * with existing /visual-edit launch commands. */
71
+ previewToken?: string;
67
72
  json: boolean;
68
73
  once: boolean;
69
74
  dryRun: boolean;
@@ -77,6 +82,8 @@ export interface DesignConnectRoute {
77
82
  title: string;
78
83
  sourceFile?: string;
79
84
  sourceKind: "react-router" | "html" | "manual";
85
+ screenshotUrl?: string;
86
+ metadata?: Record<string, unknown>;
80
87
  }
81
88
 
82
89
  export interface DesignConnectManifest {
@@ -112,6 +119,34 @@ export interface DesignConnectBridge {
112
119
  * the manifest JSON so it is not exposed over the network via GET /manifest.
113
120
  * The server-side grant action reads it from the running bridge instance. */
114
121
  bridgeToken: string;
122
+ /** Read-only token accepted by manifest, route, snapshot, proxy, and editor
123
+ * bridge-registration endpoints. Safe to hand to the Design browser, but
124
+ * never accepted by filesystem endpoints. */
125
+ previewToken: string;
126
+ }
127
+
128
+ export interface DesignConnectBridgeOptions {
129
+ bridgeToken?: string;
130
+ previewToken?: string;
131
+ /** Extra exact browser origins allowed to make CORS requests to the bridge.
132
+ * The production Design origin and loopback development origins are always
133
+ * recognized; custom deployments should pass their app origin here. */
134
+ allowedOrigins?: string[];
135
+ }
136
+
137
+ const PREVIEW_TOKEN_DOMAIN = "agent-native-design-preview-v1\0";
138
+
139
+ /**
140
+ * Derive a read-only preview credential from the stronger filesystem token.
141
+ * The one-way hash keeps old `--bridge-token` launch commands compatible while
142
+ * ensuring a leaked preview credential cannot be promoted into write access.
143
+ */
144
+ export function deriveDesignPreviewToken(bridgeToken: string): string {
145
+ return crypto
146
+ .createHash("sha256")
147
+ .update(PREVIEW_TOKEN_DOMAIN)
148
+ .update(bridgeToken)
149
+ .digest("hex");
115
150
  }
116
151
 
117
152
  function stringFlagValue(argv: string[], index: number, flag: string) {
@@ -183,6 +218,11 @@ export function parseDesignConnectArgs(argv: string[]): DesignConnectArgs {
183
218
  index += 1;
184
219
  } else if (arg.startsWith("--bridge-token=")) {
185
220
  parsed.bridgeToken = arg.slice("--bridge-token=".length);
221
+ } else if (arg === "--preview-token") {
222
+ parsed.previewToken = stringFlagValue(args, index, arg);
223
+ index += 1;
224
+ } else if (arg.startsWith("--preview-token=")) {
225
+ parsed.previewToken = arg.slice("--preview-token=".length);
186
226
  } else if (arg === "--json") {
187
227
  parsed.json = true;
188
228
  parsed.once = true;
@@ -214,13 +254,36 @@ export function parseDesignConnectArgs(argv: string[]): DesignConnectArgs {
214
254
  }
215
255
 
216
256
  function routeId(routePath: string): string {
217
- if (routePath === "/") return "route-root";
218
- const slug = routePath
257
+ const normalized = routePath.trim() || "/";
258
+ const slug = normalized
219
259
  .replace(/^\/+/, "")
260
+ .replace(/\*/g, "w")
261
+ .replace(/:/g, "p")
262
+ .replace(/[[\]]/g, "")
220
263
  .replace(/[^a-zA-Z0-9]+/g, "-")
221
264
  .replace(/^-+|-+$/g, "")
222
265
  .toLowerCase();
223
- return slug ? `route-${slug}` : "route-wildcard";
266
+ // Slugs are intentionally readable but necessarily lossy: `/foo/bar`,
267
+ // `/foo-bar`, and `/foo_bar` all collapse to the same text, while `/` and
268
+ // `/*` can collide with literal `/root` and `/wildcard` paths. Suffix every
269
+ // route with a stable hash of its normalized path so URL/query states and
270
+ // router patterns can never silently replace one another in the manifest.
271
+ const readable =
272
+ normalized === "/"
273
+ ? "root"
274
+ : /^\/\*+$/.test(normalized) || !slug
275
+ ? "wildcard"
276
+ : slug;
277
+ return `route-${readable}-${stableRoutePathHash(normalized)}`;
278
+ }
279
+
280
+ function stableRoutePathHash(value: string): string {
281
+ let hash = 0xcbf29ce484222325n;
282
+ for (const character of value) {
283
+ hash ^= BigInt(character.codePointAt(0)!);
284
+ hash = BigInt.asUintN(64, hash * 0x100000001b3n);
285
+ }
286
+ return hash.toString(36);
224
287
  }
225
288
 
226
289
  function titleFromRoutePath(routePath: string): string {
@@ -384,8 +447,12 @@ function isDesignConnectManifest(
384
447
 
385
448
  async function fetchRunningBridgeManifest(
386
449
  bridgeUrl: string,
450
+ previewToken?: string,
387
451
  ): Promise<DesignConnectManifest | null> {
388
- const manifestUrl = new URL("/manifest.json", bridgeUrl).toString();
452
+ const manifestUrl = new URL("/manifest.json", bridgeUrl);
453
+ if (previewToken) {
454
+ manifestUrl.searchParams.set("previewToken", previewToken);
455
+ }
389
456
  const controller = new AbortController();
390
457
  const timeout = setTimeout(() => controller.abort(), 800);
391
458
  try {
@@ -421,6 +488,48 @@ export function designConnectManifestsTargetSameApp(
421
488
  );
422
489
  }
423
490
 
491
+ /** Non-sensitive stable identifier used by /health so daemon reruns can detect
492
+ * an already-running bridge for the same app without exposing its root path or
493
+ * route manifest. */
494
+ export function designConnectAppFingerprint(
495
+ manifest: Pick<DesignConnectManifest, "devServerUrl" | "rootPath">,
496
+ ): string {
497
+ let devServerUrl = manifest.devServerUrl;
498
+ try {
499
+ devServerUrl = normalizeHttpUrl(devServerUrl);
500
+ } catch {
501
+ // Hash the original string for malformed legacy manifests.
502
+ }
503
+ return crypto
504
+ .createHash("sha256")
505
+ .update(`${devServerUrl}\n${path.resolve(manifest.rootPath)}`)
506
+ .digest("base64url")
507
+ .slice(0, 24);
508
+ }
509
+
510
+ async function fetchRunningBridgeFingerprint(
511
+ bridgeUrl: string,
512
+ ): Promise<string | null> {
513
+ const healthUrl = new URL("/health", bridgeUrl).toString();
514
+ const controller = new AbortController();
515
+ const timeout = setTimeout(() => controller.abort(), 800);
516
+ try {
517
+ const response = await fetch(healthUrl, {
518
+ method: "GET",
519
+ signal: controller.signal,
520
+ });
521
+ if (!response.ok) return null;
522
+ const body = (await response.json()) as Record<string, unknown>;
523
+ return typeof body["appFingerprint"] === "string"
524
+ ? body["appFingerprint"]
525
+ : null;
526
+ } catch {
527
+ return null;
528
+ } finally {
529
+ clearTimeout(timeout);
530
+ }
531
+ }
532
+
424
533
  async function resolveDevServerUrl(url?: string): Promise<string> {
425
534
  if (url) return normalizeHttpUrl(url);
426
535
  for (const candidate of DEFAULT_DEV_SERVER_CANDIDATES) {
@@ -435,12 +544,76 @@ async function ensureRouteManifest(options: {
435
544
  routes: DesignConnectRoute[];
436
545
  devServerUrl: string;
437
546
  dryRun: boolean;
438
- }): Promise<{ path: string; created: boolean }> {
547
+ }): Promise<{ path: string; created: boolean; routes: DesignConnectRoute[] }> {
439
548
  const manifestPath = path.isAbsolute(options.routeManifestPath)
440
549
  ? options.routeManifestPath
441
550
  : path.join(options.root, options.routeManifestPath);
442
551
  if (fsSync.existsSync(manifestPath)) {
443
- return { path: manifestPath, created: false };
552
+ try {
553
+ const parsed = JSON.parse(
554
+ await fs.readFile(manifestPath, "utf8"),
555
+ ) as Record<string, unknown>;
556
+ const savedRoutes = Array.isArray(parsed.routes)
557
+ ? parsed.routes.flatMap((value): DesignConnectRoute[] => {
558
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
559
+ return [];
560
+ }
561
+ const route = value as Record<string, unknown>;
562
+ if (typeof route.path !== "string" || !route.path.trim()) return [];
563
+ const sourceKind =
564
+ route.sourceKind === "react-router" ||
565
+ route.sourceKind === "html" ||
566
+ route.sourceKind === "manual"
567
+ ? route.sourceKind
568
+ : "manual";
569
+ return [
570
+ {
571
+ id:
572
+ typeof route.id === "string" && route.id.trim()
573
+ ? route.id
574
+ : routeId(route.path),
575
+ path: route.path,
576
+ title:
577
+ typeof route.title === "string" && route.title.trim()
578
+ ? route.title
579
+ : titleFromRoutePath(route.path),
580
+ sourceFile:
581
+ typeof route.sourceFile === "string"
582
+ ? route.sourceFile
583
+ : undefined,
584
+ sourceKind,
585
+ screenshotUrl:
586
+ typeof route.screenshotUrl === "string"
587
+ ? route.screenshotUrl
588
+ : undefined,
589
+ metadata:
590
+ route.metadata &&
591
+ typeof route.metadata === "object" &&
592
+ !Array.isArray(route.metadata)
593
+ ? (route.metadata as Record<string, unknown>)
594
+ : undefined,
595
+ },
596
+ ];
597
+ })
598
+ : [];
599
+ if (savedRoutes.length > 0) {
600
+ const savedPaths = new Set(savedRoutes.map((route) => route.path));
601
+ return {
602
+ path: manifestPath,
603
+ created: false,
604
+ // Keep manual/custom route order and metadata while still surfacing
605
+ // newly discovered routes on subsequent bridge starts.
606
+ routes: [
607
+ ...savedRoutes,
608
+ ...options.routes.filter((route) => !savedPaths.has(route.path)),
609
+ ],
610
+ };
611
+ }
612
+ } catch {
613
+ // Never overwrite a malformed/custom file. Route discovery remains a
614
+ // safe runtime fallback and the user can repair the manifest in place.
615
+ }
616
+ return { path: manifestPath, created: false, routes: options.routes };
444
617
  }
445
618
  if (!options.dryRun) {
446
619
  await fs.mkdir(path.dirname(manifestPath), { recursive: true });
@@ -461,7 +634,11 @@ async function ensureRouteManifest(options: {
461
634
  "utf8",
462
635
  );
463
636
  }
464
- return { path: manifestPath, created: !options.dryRun };
637
+ return {
638
+ path: manifestPath,
639
+ created: !options.dryRun,
640
+ routes: options.routes,
641
+ };
465
642
  }
466
643
 
467
644
  export async function prepareDesignConnectManifest(
@@ -471,15 +648,16 @@ export async function prepareDesignConnectManifest(
471
648
  const port = options.port ?? DEFAULT_BRIDGE_PORT;
472
649
  const devServerUrl = await resolveDevServerUrl(options.url);
473
650
  const bridgeUrl = `http://127.0.0.1:${port}`;
474
- const routes = discoverDesignRoutes(root);
651
+ const discoveredRoutes = discoverDesignRoutes(root);
475
652
  const routeManifest = await ensureRouteManifest({
476
653
  root,
477
654
  routeManifestPath: options.routeManifest ?? ROUTE_MANIFEST_FILE,
478
- routes,
655
+ routes: discoveredRoutes,
479
656
  devServerUrl,
480
657
  dryRun: Boolean(options.dryRun),
481
658
  });
482
659
  const generatedAt = new Date().toISOString();
660
+ const routes = routeManifest.routes;
483
661
 
484
662
  return {
485
663
  version: 1,
@@ -511,6 +689,73 @@ export async function prepareDesignConnectManifest(
511
689
  };
512
690
  }
513
691
 
692
+ const BRIDGE_CORS_HEADERS = Symbol("agent-native-design-bridge-cors");
693
+
694
+ type CorsAwareResponse = ServerResponse & {
695
+ [BRIDGE_CORS_HEADERS]?: Record<string, string>;
696
+ };
697
+
698
+ function isLoopbackOrigin(parsed: URL): boolean {
699
+ const hostname = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
700
+ return (
701
+ hostname === "localhost" ||
702
+ hostname === "::1" ||
703
+ /^127(?:\.\d{1,3}){3}$/.test(hostname)
704
+ );
705
+ }
706
+
707
+ function isApprovedDesignOrigin(
708
+ rawOrigin: string,
709
+ configuredOrigins: ReadonlySet<string>,
710
+ ): boolean {
711
+ let parsed: URL;
712
+ try {
713
+ parsed = new URL(rawOrigin);
714
+ } catch {
715
+ return false;
716
+ }
717
+ if (parsed.origin !== rawOrigin) return false;
718
+ if (configuredOrigins.has(parsed.origin)) return true;
719
+ if (
720
+ (parsed.protocol === "http:" || parsed.protocol === "https:") &&
721
+ isLoopbackOrigin(parsed)
722
+ ) {
723
+ return true;
724
+ }
725
+ return (
726
+ parsed.protocol === "https:" &&
727
+ (parsed.hostname === "design.agent-native.com" ||
728
+ parsed.hostname.endsWith(".design.agent-native.com"))
729
+ );
730
+ }
731
+
732
+ function configureBridgeCors(
733
+ req: IncomingMessage,
734
+ res: ServerResponse,
735
+ configuredOrigins: ReadonlySet<string>,
736
+ ): boolean {
737
+ const origin =
738
+ typeof req.headers.origin === "string" ? req.headers.origin : "";
739
+ const approved = origin
740
+ ? isApprovedDesignOrigin(origin, configuredOrigins)
741
+ : false;
742
+ (res as CorsAwareResponse)[BRIDGE_CORS_HEADERS] = approved
743
+ ? {
744
+ "access-control-allow-origin": origin,
745
+ "access-control-allow-methods": "GET, HEAD, POST, OPTIONS",
746
+ "access-control-allow-headers":
747
+ "content-type, x-bridge-token, x-design-preview-token",
748
+ "access-control-allow-private-network": "true",
749
+ vary: "Origin",
750
+ }
751
+ : {};
752
+ return approved;
753
+ }
754
+
755
+ function bridgeCorsHeaders(res: ServerResponse): Record<string, string> {
756
+ return (res as CorsAwareResponse)[BRIDGE_CORS_HEADERS] ?? {};
757
+ }
758
+
514
759
  function sendJson(
515
760
  res: ServerResponse,
516
761
  statusCode: number,
@@ -518,10 +763,7 @@ function sendJson(
518
763
  ) {
519
764
  res.writeHead(statusCode, {
520
765
  "content-type": "application/json; charset=utf-8",
521
- "access-control-allow-origin": "*",
522
- "access-control-allow-methods": "GET, POST, OPTIONS",
523
- "access-control-allow-headers": "content-type, x-bridge-token",
524
- "access-control-allow-private-network": "true",
766
+ ...bridgeCorsHeaders(res),
525
767
  });
526
768
  res.end(`${JSON.stringify(body, null, 2)}\n`);
527
769
  }
@@ -534,10 +776,7 @@ function sendText(
534
776
  ) {
535
777
  res.writeHead(statusCode, {
536
778
  "content-type": contentType,
537
- "access-control-allow-origin": "*",
538
- "access-control-allow-methods": "GET, POST, OPTIONS",
539
- "access-control-allow-headers": "content-type, x-bridge-token",
540
- "access-control-allow-private-network": "true",
779
+ ...bridgeCorsHeaders(res),
541
780
  });
542
781
  res.end(body);
543
782
  }
@@ -549,10 +788,7 @@ function sendBytes(
549
788
  headers: Headers,
550
789
  ) {
551
790
  const responseHeaders: Record<string, string> = {
552
- "access-control-allow-origin": "*",
553
- "access-control-allow-methods": "GET, HEAD, POST, OPTIONS",
554
- "access-control-allow-headers": "content-type, x-bridge-token",
555
- "access-control-allow-private-network": "true",
791
+ ...bridgeCorsHeaders(res),
556
792
  };
557
793
  for (const name of [
558
794
  "content-type",
@@ -575,6 +811,28 @@ function sameOrigin(a: string, b: string): boolean {
575
811
  }
576
812
  }
577
813
 
814
+ function constantTimeTokenMatches(
815
+ providedToken: string,
816
+ expectedToken: string,
817
+ ): boolean {
818
+ try {
819
+ return (
820
+ providedToken.length === expectedToken.length &&
821
+ crypto.timingSafeEqual(
822
+ Buffer.from(providedToken, "utf8"),
823
+ Buffer.from(expectedToken, "utf8"),
824
+ )
825
+ );
826
+ } catch {
827
+ return false;
828
+ }
829
+ }
830
+
831
+ function readHeader(req: IncomingMessage, name: string): string {
832
+ const value = req.headers[name];
833
+ return typeof value === "string" ? value : "";
834
+ }
835
+
578
836
  function resolvePreviewSnapshotUrl(
579
837
  devServerUrl: string,
580
838
  rawUrl: string | null,
@@ -1224,34 +1482,83 @@ async function walkBridgeFiles(rootPath: string): Promise<ListFilesResult> {
1224
1482
 
1225
1483
  export async function startDesignConnectBridge(
1226
1484
  manifest: DesignConnectManifest,
1227
- seedToken?: string,
1485
+ seedOrOptions?: string | DesignConnectBridgeOptions,
1228
1486
  ): Promise<DesignConnectBridge> {
1229
1487
  // Shared secret the browser sends (x-bridge-token) to unlock live-edit/read/
1230
1488
  // write. Bridge and the user's connection row must agree on it. Adopt a
1231
1489
  // server-minted seed when given (MCP flow); otherwise mint one and rely on
1232
1490
  // --app-url self-registration to push it up. Kept in-process, never served.
1491
+ const options: DesignConnectBridgeOptions =
1492
+ typeof seedOrOptions === "string"
1493
+ ? { bridgeToken: seedOrOptions }
1494
+ : (seedOrOptions ?? {});
1233
1495
  const bridgeToken =
1234
- seedToken ||
1496
+ options.bridgeToken ||
1235
1497
  process.env["AGENT_NATIVE_BRIDGE_TOKEN"] ||
1236
1498
  crypto.randomBytes(32).toString("hex");
1499
+ const previewToken =
1500
+ options.previewToken ||
1501
+ process.env["AGENT_NATIVE_PREVIEW_TOKEN"] ||
1502
+ deriveDesignPreviewToken(bridgeToken);
1503
+ const configuredOrigins = new Set(
1504
+ (options.allowedOrigins ?? []).flatMap((raw): string[] => {
1505
+ try {
1506
+ return [new URL(raw).origin];
1507
+ } catch {
1508
+ return [];
1509
+ }
1510
+ }),
1511
+ );
1237
1512
  let liveEditBridgeScript = "";
1513
+ // One bridge process serves every URL-backed screen in an overview. The
1514
+ // editor script carries screen-specific state (notably screenId), so a
1515
+ // single global slot lets parallel iframe registrations overwrite each
1516
+ // other and boot a frame with another frame's identity. Keep keyed scripts
1517
+ // for modern clients while retaining the unkeyed slot for older clients.
1518
+ const liveEditBridgeScripts = new Map<string, string>();
1238
1519
 
1239
1520
  const server = http.createServer(
1240
1521
  (req: IncomingMessage, res: ServerResponse) => {
1522
+ const corsApproved = configureBridgeCors(req, res, configuredOrigins);
1241
1523
  if (req.method === "OPTIONS") {
1242
- sendJson(res, 204, {});
1524
+ sendJson(
1525
+ res,
1526
+ corsApproved ? 204 : 403,
1527
+ corsApproved
1528
+ ? {}
1529
+ : { ok: false, error: "origin is not allowed by this bridge" },
1530
+ );
1243
1531
  return;
1244
1532
  }
1245
1533
 
1246
- const pathname = new URL(req.url ?? "/", manifest.bridgeUrl).pathname;
1534
+ const requestUrl = new URL(req.url ?? "/", manifest.bridgeUrl);
1535
+ const pathname = requestUrl.pathname;
1536
+ const providedPreviewToken =
1537
+ readHeader(req, "x-design-preview-token") ||
1538
+ requestUrl.searchParams.get("previewToken") ||
1539
+ "";
1540
+ const previewTokenValid = constantTimeTokenMatches(
1541
+ providedPreviewToken,
1542
+ previewToken,
1543
+ );
1544
+ const rejectInvalidPreviewToken = (): boolean => {
1545
+ if (previewTokenValid) return false;
1546
+ sendJson(res, 401, {
1547
+ ok: false,
1548
+ error: "invalid or missing preview token",
1549
+ });
1550
+ return true;
1551
+ };
1247
1552
 
1248
- // ── Public read-only routes (no token required) ──────────────────────
1553
+ // ── Read-only preview routes (preview token required) ────────────────
1249
1554
 
1250
1555
  if (pathname === "/" || pathname === "/manifest.json") {
1556
+ if (rejectInvalidPreviewToken()) return;
1251
1557
  sendJson(res, 200, manifest as unknown as Record<string, unknown>);
1252
1558
  return;
1253
1559
  }
1254
1560
  if (pathname === "/routes.json") {
1561
+ if (rejectInvalidPreviewToken()) return;
1255
1562
  sendJson(res, 200, {
1256
1563
  version: 1,
1257
1564
  sourceType: "localhost",
@@ -1263,7 +1570,11 @@ export async function startDesignConnectBridge(
1263
1570
  return;
1264
1571
  }
1265
1572
  if (pathname === "/health") {
1266
- sendJson(res, 200, { ok: true, source: manifest.source });
1573
+ sendJson(res, 200, {
1574
+ ok: true,
1575
+ source: manifest.source,
1576
+ appFingerprint: designConnectAppFingerprint(manifest),
1577
+ });
1267
1578
  return;
1268
1579
  }
1269
1580
  if (pathname === "/live-edit-bridge") {
@@ -1271,42 +1582,54 @@ export async function startDesignConnectBridge(
1271
1582
  sendJson(res, 405, { ok: false, error: "method not allowed" });
1272
1583
  return;
1273
1584
  }
1274
- const tokenHeader = req.headers["x-bridge-token"];
1275
- const providedToken =
1276
- typeof tokenHeader === "string" ? tokenHeader : "";
1277
- let tokenValid = false;
1278
- try {
1279
- tokenValid =
1280
- providedToken.length === bridgeToken.length &&
1281
- crypto.timingSafeEqual(
1282
- Buffer.from(providedToken, "utf8"),
1283
- Buffer.from(bridgeToken, "utf8"),
1284
- );
1285
- } catch {
1286
- tokenValid = false;
1287
- }
1288
- if (!tokenValid) {
1289
- sendJson(res, 401, {
1290
- ok: false,
1291
- error: "invalid or missing bridge token",
1292
- });
1293
- return;
1294
- }
1585
+ if (rejectInvalidPreviewToken()) return;
1295
1586
  void (async () => {
1296
1587
  try {
1297
1588
  const raw = await readRequestBody(req);
1298
1589
  const body = JSON.parse(raw) as Record<string, unknown>;
1299
1590
  const script =
1300
1591
  typeof body["script"] === "string" ? body["script"] : "";
1301
- if (!script.includes("agent-native:editor-chrome-ready")) {
1592
+ const bridgeKey =
1593
+ typeof body["bridgeKey"] === "string"
1594
+ ? body["bridgeKey"].trim()
1595
+ : "";
1596
+ const installsSupportedDesignBridge =
1597
+ script.includes("agent-native:editor-chrome-ready") ||
1598
+ script.includes("embedded-canvas-pan");
1599
+ if (!installsSupportedDesignBridge) {
1600
+ sendJson(res, 400, {
1601
+ ok: false,
1602
+ error:
1603
+ "script must install an approved Agent Native editor or canvas-pan bridge",
1604
+ });
1605
+ return;
1606
+ }
1607
+ if (
1608
+ bridgeKey &&
1609
+ (!/^[a-zA-Z0-9:._-]+$/.test(bridgeKey) || bridgeKey.length > 128)
1610
+ ) {
1302
1611
  sendJson(res, 400, {
1303
1612
  ok: false,
1304
- error: "script must install the Agent Native editor bridge",
1613
+ error: "bridgeKey must be 1-128 safe identifier characters",
1305
1614
  });
1306
1615
  return;
1307
1616
  }
1308
1617
  liveEditBridgeScript = script;
1309
- sendJson(res, 200, { ok: true });
1618
+ if (bridgeKey) {
1619
+ liveEditBridgeScripts.delete(bridgeKey);
1620
+ liveEditBridgeScripts.set(bridgeKey, script);
1621
+ // Bound the in-memory cache. Normal editor usage has one key per
1622
+ // visible screen; 128 also leaves ample room for mode changes.
1623
+ while (liveEditBridgeScripts.size > 128) {
1624
+ const oldest = liveEditBridgeScripts.keys().next().value;
1625
+ if (typeof oldest !== "string") break;
1626
+ liveEditBridgeScripts.delete(oldest);
1627
+ }
1628
+ }
1629
+ sendJson(res, 200, {
1630
+ ok: true,
1631
+ ...(bridgeKey ? { bridgeKey } : {}),
1632
+ });
1310
1633
  } catch (err: unknown) {
1311
1634
  sendJson(res, 400, {
1312
1635
  ok: false,
@@ -1321,9 +1644,9 @@ export async function startDesignConnectBridge(
1321
1644
  sendJson(res, 405, { ok: false, error: "method not allowed" });
1322
1645
  return;
1323
1646
  }
1647
+ if (rejectInvalidPreviewToken()) return;
1324
1648
  void (async () => {
1325
1649
  try {
1326
- const requestUrl = new URL(req.url ?? "/", manifest.bridgeUrl);
1327
1650
  const targetUrl = resolvePreviewSnapshotUrl(
1328
1651
  manifest.devServerUrl,
1329
1652
  requestUrl.searchParams.get("url") ??
@@ -1335,6 +1658,23 @@ export async function startDesignConnectBridge(
1335
1658
  );
1336
1659
  const includeEditorBridge =
1337
1660
  requestUrl.searchParams.get("bridge") !== "0";
1661
+ const requestedBridgeKey =
1662
+ requestUrl.searchParams.get("bridgeKey")?.trim() ?? "";
1663
+ const editorBridgeScript = requestedBridgeKey
1664
+ ? (liveEditBridgeScripts.get(requestedBridgeKey) ?? "")
1665
+ : liveEditBridgeScript;
1666
+ if (
1667
+ includeEditorBridge &&
1668
+ requestedBridgeKey &&
1669
+ !editorBridgeScript
1670
+ ) {
1671
+ sendJson(res, 409, {
1672
+ ok: false,
1673
+ error:
1674
+ "The requested live-edit bridge script is not registered. Reload the Design frame to register it again.",
1675
+ });
1676
+ return;
1677
+ }
1338
1678
  // The dev server route the SPA must boot on (e.g. "/todo"), taken
1339
1679
  // from the resolved snapshot target rather than the bridge's own
1340
1680
  // "/live-edit" request path.
@@ -1344,7 +1684,7 @@ export async function startDesignConnectBridge(
1344
1684
  const html = injectLiveEditBridge(
1345
1685
  snapshot.html,
1346
1686
  new URL("/", manifest.bridgeUrl).toString(),
1347
- includeEditorBridge ? liveEditBridgeScript : "",
1687
+ includeEditorBridge ? editorBridgeScript : "",
1348
1688
  targetPath,
1349
1689
  );
1350
1690
  sendText(
@@ -1369,9 +1709,9 @@ export async function startDesignConnectBridge(
1369
1709
  sendJson(res, 405, { ok: false, error: "method not allowed" });
1370
1710
  return;
1371
1711
  }
1712
+ if (rejectInvalidPreviewToken()) return;
1372
1713
  void (async () => {
1373
1714
  try {
1374
- const requestUrl = new URL(req.url ?? "/", manifest.bridgeUrl);
1375
1715
  const targetUrl = resolvePreviewSnapshotUrl(
1376
1716
  manifest.devServerUrl,
1377
1717
  requestUrl.searchParams.get("url") ??
@@ -1413,20 +1753,8 @@ export async function startDesignConnectBridge(
1413
1753
  }
1414
1754
 
1415
1755
  // Authenticate with constant-time comparison to prevent timing attacks.
1416
- const tokenHeader = req.headers["x-bridge-token"];
1417
- const providedToken =
1418
- typeof tokenHeader === "string" ? tokenHeader : "";
1419
- let tokenValid = false;
1420
- try {
1421
- tokenValid =
1422
- providedToken.length === bridgeToken.length &&
1423
- crypto.timingSafeEqual(
1424
- Buffer.from(providedToken, "utf8"),
1425
- Buffer.from(bridgeToken, "utf8"),
1426
- );
1427
- } catch {
1428
- tokenValid = false;
1429
- }
1756
+ const providedToken = readHeader(req, "x-bridge-token");
1757
+ const tokenValid = constantTimeTokenMatches(providedToken, bridgeToken);
1430
1758
  if (!tokenValid) {
1431
1759
  sendJson(res, 401, {
1432
1760
  ok: false,
@@ -1644,6 +1972,15 @@ export async function startDesignConnectBridge(
1644
1972
  }
1645
1973
 
1646
1974
  if (req.method === "GET" || req.method === "HEAD") {
1975
+ const sameOriginPreviewSubresource =
1976
+ readHeader(req, "sec-fetch-site") === "same-origin";
1977
+ if (!previewTokenValid && !sameOriginPreviewSubresource) {
1978
+ sendJson(res, 401, {
1979
+ ok: false,
1980
+ error: "invalid or missing preview token",
1981
+ });
1982
+ return;
1983
+ }
1647
1984
  void (async () => {
1648
1985
  try {
1649
1986
  const targetUrl = resolvePreviewProxyUrl(
@@ -1682,7 +2019,7 @@ export async function startDesignConnectBridge(
1682
2019
  });
1683
2020
  });
1684
2021
 
1685
- return { server, manifest, bridgeToken };
2022
+ return { server, manifest, bridgeToken, previewToken };
1686
2023
  }
1687
2024
 
1688
2025
  /**
@@ -1741,7 +2078,7 @@ export async function registerConnectionWithServer(
1741
2078
  authToken?: string,
1742
2079
  ): Promise<void> {
1743
2080
  const endpoint = `${appUrl}/_agent-native/actions/connect-localhost`;
1744
- const { manifest, bridgeToken } = bridge;
2081
+ const { manifest, bridgeToken, previewToken } = bridge;
1745
2082
  const payload = {
1746
2083
  devServerUrl: manifest.devServerUrl,
1747
2084
  bridgeUrl: manifest.bridgeUrl,
@@ -1761,6 +2098,7 @@ export async function registerConnectionWithServer(
1761
2098
  // row. grant-localhost-write-consent then reads it from the row instead of
1762
2099
  // minting its own unrelated token, which would always produce a 401.
1763
2100
  bridgeToken,
2101
+ previewToken,
1764
2102
  status: "connected" as const,
1765
2103
  };
1766
2104
 
@@ -1814,6 +2152,10 @@ Options:
1814
2152
  /visual-edit flow so the bridge and the user's stored
1815
2153
  connection token match with no self-registration.
1816
2154
  (also reads AGENT_NATIVE_BRIDGE_TOKEN env)
2155
+ --preview-token <token> Adopt the paired read-only browser preview token.
2156
+ Optional when --bridge-token is present: compatible
2157
+ clients derive the same one-way token automatically.
2158
+ (also reads AGENT_NATIVE_PREVIEW_TOKEN env)
1817
2159
  --daemon Start the bridge detached, wait for /health, then exit
1818
2160
  --json Print the manifest JSON and exit
1819
2161
  --once Prepare/scaffold the manifest and exit
@@ -1869,19 +2211,24 @@ function resolveCurrentCliInvocation(argv: string[]): {
1869
2211
  async function startDetachedDesignBridge(
1870
2212
  argv: string[],
1871
2213
  manifest: DesignConnectManifest,
2214
+ previewToken?: string,
1872
2215
  ): Promise<number> {
1873
2216
  if (await waitForBridgeHealth(manifest.bridgeUrl, 800)) {
1874
- const runningManifest = await fetchRunningBridgeManifest(
1875
- manifest.bridgeUrl,
1876
- );
2217
+ const [runningManifest, runningFingerprint] = await Promise.all([
2218
+ fetchRunningBridgeManifest(manifest.bridgeUrl, previewToken),
2219
+ fetchRunningBridgeFingerprint(manifest.bridgeUrl),
2220
+ ]);
2221
+ const fingerprintMatches =
2222
+ runningFingerprint === designConnectAppFingerprint(manifest);
1877
2223
  if (
1878
- runningManifest &&
1879
- designConnectManifestsTargetSameApp(runningManifest, manifest)
2224
+ (runningManifest &&
2225
+ designConnectManifestsTargetSameApp(runningManifest, manifest)) ||
2226
+ fingerprintMatches
1880
2227
  ) {
1881
2228
  console.error(
1882
2229
  `Design localhost bridge already running at ${manifest.bridgeUrl}`,
1883
2230
  );
1884
- console.log(JSON.stringify(runningManifest, null, 2));
2231
+ console.log(JSON.stringify(runningManifest ?? manifest, null, 2));
1885
2232
  return 0;
1886
2233
  }
1887
2234
 
@@ -1942,24 +2289,33 @@ export async function runDesign(argv: string[]) {
1942
2289
  }
1943
2290
 
1944
2291
  const manifest = await prepareDesignConnectManifest(parsed);
2292
+ const seedBridgeToken =
2293
+ parsed.bridgeToken || process.env["AGENT_NATIVE_BRIDGE_TOKEN"] || undefined;
2294
+ const seedPreviewToken =
2295
+ parsed.previewToken ||
2296
+ process.env["AGENT_NATIVE_PREVIEW_TOKEN"] ||
2297
+ (seedBridgeToken ? deriveDesignPreviewToken(seedBridgeToken) : undefined);
2298
+ const appUrl = resolveAppUrl(parsed.appUrl);
1945
2299
  if (parsed.daemon) {
1946
- return startDetachedDesignBridge(argv, manifest);
2300
+ return startDetachedDesignBridge(argv, manifest, seedPreviewToken);
1947
2301
  }
1948
2302
  if (parsed.json || parsed.once || parsed.dryRun) {
1949
2303
  console.log(JSON.stringify(manifest, null, 2));
1950
2304
  return 0;
1951
2305
  }
1952
2306
 
1953
- const seedToken =
1954
- parsed.bridgeToken || process.env["AGENT_NATIVE_BRIDGE_TOKEN"] || undefined;
1955
- const bridge = await startDesignConnectBridge(manifest, seedToken);
2307
+ const bridge = await startDesignConnectBridge(manifest, {
2308
+ bridgeToken: seedBridgeToken,
2309
+ previewToken: seedPreviewToken,
2310
+ allowedOrigins: appUrl ? [appUrl] : [],
2311
+ });
1956
2312
  console.error("Design localhost bridge running");
1957
2313
  console.error(`Bridge: ${manifest.bridgeUrl}`);
1958
2314
  console.error(`Manifest: ${manifest.bridgeUrl}/manifest.json`);
1959
2315
  console.error(`Routes: ${manifest.routeCount}`);
1960
2316
  console.error(`Dev URL: ${manifest.devServerUrl}`);
1961
2317
 
1962
- if (seedToken) {
2318
+ if (seedBridgeToken) {
1963
2319
  // Server already stored this token on the row; bridge matches it, so no
1964
2320
  // self-registration needed. Zero-config path for the remote-MCP flow.
1965
2321
  console.error(
@@ -1968,13 +2324,12 @@ export async function runDesign(argv: string[]) {
1968
2324
  } else {
1969
2325
  // No seed: fall back to self-registration — POST the minted token to
1970
2326
  // connect-localhost. Needs an auth token in env or it 401s (the old gap).
1971
- const appUrl = resolveAppUrl(parsed.appUrl);
1972
2327
  if (appUrl) {
1973
2328
  void registerConnectionWithServer(appUrl, bridge, resolveAuthToken());
1974
2329
  } else {
1975
2330
  // No token source at all — warn rather than 401 silently at edit time.
1976
2331
  console.error(
1977
- "[design connect] No bridge token or app URL resolved (pass --bridge-token, or --app-url / AGENT_NATIVE_URL); skipping self-registration — live-edit will fail to authorize.",
2332
+ "[design connect] No bridge token or app URL resolved (pass --bridge-token, or --app-url / AGENT_NATIVE_URL); skipping self-registration — browser preview and live-edit will fail to authorize.",
1978
2333
  );
1979
2334
  }
1980
2335
  }