@agent-native/core 0.80.11 → 0.81.1

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 (230) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +53 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/design-connect.ts +419 -19
  5. package/corpus/core/src/client/AgentPanel.tsx +33 -1
  6. package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
  7. package/corpus/core/src/client/chat/index.ts +1 -0
  8. package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
  9. package/corpus/core/src/client/chat-view-transition.ts +33 -2
  10. package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
  11. package/corpus/core/src/client/index.ts +1 -0
  12. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
  13. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
  14. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
  15. package/corpus/core/src/db/client.ts +29 -2
  16. package/corpus/core/src/provider-api/index.ts +29 -0
  17. package/corpus/core/src/server/core-routes-plugin.ts +58 -20
  18. package/corpus/core/src/styles/agent-native.css +54 -0
  19. package/corpus/core/src/usage/store.ts +2 -2
  20. package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
  21. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
  22. package/corpus/templates/analytics/app/global.css +8 -0
  23. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
  24. package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
  25. package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
  26. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
  27. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
  28. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  29. package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
  34. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
  35. package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
  36. package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
  37. package/corpus/templates/assets/app/global.css +15 -0
  38. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
  39. package/corpus/templates/assets/app/routes/library.tsx +90 -16
  40. package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
  41. package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
  42. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
  43. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
  44. package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
  46. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  47. package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
  48. package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
  49. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
  50. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
  51. package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
  52. package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
  53. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
  54. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
  55. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
  56. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
  57. package/corpus/templates/design/AGENTS.md +27 -0
  58. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
  59. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
  60. package/corpus/templates/design/actions/apply-component-prop-edit.ts +63 -20
  61. package/corpus/templates/design/actions/apply-motion-edit.ts +67 -69
  62. package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
  63. package/corpus/templates/design/actions/capture-design-state.ts +27 -4
  64. package/corpus/templates/design/actions/connect-localhost.ts +59 -22
  65. package/corpus/templates/design/actions/export-zip.ts +23 -8
  66. package/corpus/templates/design/actions/generate-design.ts +53 -1
  67. package/corpus/templates/design/actions/get-component-details.ts +5 -19
  68. package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
  69. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
  70. package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
  71. package/corpus/templates/design/actions/index-design-tokens.ts +10 -1
  72. package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
  73. package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
  74. package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
  75. package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
  76. package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
  77. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
  78. package/corpus/templates/design/actions/navigate.ts +18 -3
  79. package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
  80. package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
  81. package/corpus/templates/design/actions/provider-api-request.ts +3 -3
  82. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
  83. package/corpus/templates/design/actions/run-design-audit.ts +53 -4
  84. package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
  85. package/corpus/templates/design/actions/view-screen.ts +1 -1
  86. package/corpus/templates/design/actions/write-local-file.ts +213 -0
  87. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +230 -3308
  88. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
  89. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +776 -287
  90. package/corpus/templates/design/app/components/design/EditPanel.tsx +212 -208
  91. package/corpus/templates/design/app/components/design/LayersPanel.tsx +14 -2
  92. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
  93. package/corpus/templates/design/app/components/design/MotionDock.tsx +235 -266
  94. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +490 -55
  95. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +43 -62
  96. package/corpus/templates/design/app/components/design/StatesPanel.tsx +34 -61
  97. package/corpus/templates/design/app/components/design/TokensPanel.tsx +251 -0
  98. package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
  99. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
  100. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
  101. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
  102. package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
  103. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
  104. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
  105. package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
  106. package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
  107. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
  108. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
  109. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
  110. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
  111. package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
  112. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  113. package/corpus/templates/design/app/global.css +7 -0
  114. package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
  115. package/corpus/templates/design/app/i18n/zh-TW.ts +21 -0
  116. package/corpus/templates/design/app/i18n-data.ts +358 -0
  117. package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
  118. package/corpus/templates/design/app/pages/DesignEditor.tsx +2402 -499
  119. package/corpus/templates/design/app/pages/Index.tsx +5 -1
  120. package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
  121. package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
  122. package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
  123. package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
  124. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
  125. package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
  126. package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
  127. package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
  128. package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
  129. package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
  130. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
  131. package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
  132. package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
  133. package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
  134. package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
  135. package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
  136. package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
  137. package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
  138. package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
  139. package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
  140. package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
  141. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
  142. package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
  143. package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
  144. package/corpus/templates/design/e2e/helpers.ts +168 -65
  145. package/corpus/templates/design/package.json +3 -1
  146. package/corpus/templates/design/server/db/schema.ts +23 -0
  147. package/corpus/templates/design/server/handlers/uploads.ts +22 -1
  148. package/corpus/templates/design/server/lib/provider-api.ts +7 -4
  149. package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
  150. package/corpus/templates/design/server/plugins/db.ts +39 -0
  151. package/corpus/templates/design/server/register-secrets.ts +38 -0
  152. package/corpus/templates/design/shared/board-file.ts +228 -0
  153. package/corpus/templates/design/shared/board-objects.ts +288 -0
  154. package/corpus/templates/design/shared/code-layer.ts +52 -0
  155. package/corpus/templates/design/shared/motion-compiler.ts +58 -7
  156. package/corpus/templates/design/shared/motion-timeline.ts +2 -2
  157. package/corpus/templates/design/tsconfig.json +2 -1
  158. package/corpus/templates/slides/AGENTS.md +9 -0
  159. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
  160. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
  161. package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
  162. package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
  163. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
  164. package/corpus/templates/slides/app/vite-env.d.ts +18 -0
  165. package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
  166. package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
  167. package/corpus/templates/slides/package.json +1 -0
  168. package/corpus/templates/slides/vite.config.ts +1 -0
  169. package/dist/cli/design-connect.d.ts +25 -0
  170. package/dist/cli/design-connect.d.ts.map +1 -1
  171. package/dist/cli/design-connect.js +357 -18
  172. package/dist/cli/design-connect.js.map +1 -1
  173. package/dist/client/AgentPanel.d.ts +1 -1
  174. package/dist/client/AgentPanel.d.ts.map +1 -1
  175. package/dist/client/AgentPanel.js +18 -3
  176. package/dist/client/AgentPanel.js.map +1 -1
  177. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  178. package/dist/client/ConnectBuilderCard.js +1 -0
  179. package/dist/client/ConnectBuilderCard.js.map +1 -1
  180. package/dist/client/chat/index.d.ts +1 -1
  181. package/dist/client/chat/index.d.ts.map +1 -1
  182. package/dist/client/chat/index.js +1 -1
  183. package/dist/client/chat/index.js.map +1 -1
  184. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  185. package/dist/client/chat/run-recovery.js +5 -5
  186. package/dist/client/chat/run-recovery.js.map +1 -1
  187. package/dist/client/chat-view-transition.d.ts +6 -0
  188. package/dist/client/chat-view-transition.d.ts.map +1 -1
  189. package/dist/client/chat-view-transition.js +23 -2
  190. package/dist/client/chat-view-transition.js.map +1 -1
  191. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  192. package/dist/client/composer/PromptComposer.js +3 -2
  193. package/dist/client/composer/PromptComposer.js.map +1 -1
  194. package/dist/client/index.d.ts +1 -1
  195. package/dist/client/index.d.ts.map +1 -1
  196. package/dist/client/index.js +1 -1
  197. package/dist/client/index.js.map +1 -1
  198. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  199. package/dist/client/org/OrgSwitcher.js +11 -4
  200. package/dist/client/org/OrgSwitcher.js.map +1 -1
  201. package/dist/client/sharing/ShareButton.js +3 -3
  202. package/dist/client/sharing/ShareButton.js.map +1 -1
  203. package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
  204. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  205. package/dist/client/use-agent-chat-home-handoff.js +20 -6
  206. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  207. package/dist/collab/routes.d.ts +1 -1
  208. package/dist/db/client.d.ts +1 -0
  209. package/dist/db/client.d.ts.map +1 -1
  210. package/dist/db/client.js +28 -2
  211. package/dist/db/client.js.map +1 -1
  212. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  213. package/dist/notifications/routes.d.ts +2 -2
  214. package/dist/observability/routes.d.ts +8 -8
  215. package/dist/progress/routes.d.ts +1 -1
  216. package/dist/provider-api/index.d.ts +5 -4
  217. package/dist/provider-api/index.d.ts.map +1 -1
  218. package/dist/provider-api/index.js +28 -0
  219. package/dist/provider-api/index.js.map +1 -1
  220. package/dist/resources/handlers.d.ts +3 -3
  221. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  222. package/dist/server/core-routes-plugin.d.ts +25 -0
  223. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  224. package/dist/server/core-routes-plugin.js +43 -18
  225. package/dist/server/core-routes-plugin.js.map +1 -1
  226. package/dist/server/transcribe-voice.d.ts +1 -1
  227. package/dist/styles/agent-native.css +54 -0
  228. package/dist/usage/store.js +2 -2
  229. package/dist/usage/store.js.map +1 -1
  230. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import crypto from "node:crypto";
1
2
  import fsSync from "node:fs";
2
3
  import fs from "node:fs/promises";
3
4
  import http from "node:http";
@@ -84,6 +85,13 @@ export function parseDesignConnectArgs(argv) {
84
85
  else if (arg.startsWith("--route-manifest=")) {
85
86
  parsed.routeManifest = arg.slice("--route-manifest=".length);
86
87
  }
88
+ else if (arg === "--app-url") {
89
+ parsed.appUrl = stringFlagValue(args, index, arg);
90
+ index += 1;
91
+ }
92
+ else if (arg.startsWith("--app-url=")) {
93
+ parsed.appUrl = arg.slice("--app-url=".length);
94
+ }
87
95
  else if (arg === "--json") {
88
96
  parsed.json = true;
89
97
  parsed.once = true;
@@ -298,21 +306,15 @@ export async function prepareDesignConnectManifest(options = {}) {
298
306
  generatedAt,
299
307
  capabilities: BRIDGE_OPERATIONS.map((operation) => ({
300
308
  operation,
301
- status: operation === "readFile" ||
302
- operation === "applyEdit" ||
303
- operation === "writeFile"
304
- ? "planned"
305
- : "available",
306
- reason: operation === "writeFile"
307
- ? "The bridge advertises the contract before enabling local file writes."
308
- : operation === "resolveNodeToFile"
309
- ? // resolveNodeToFile maps a runtime DOM node id (from the editor's
310
- // 'select' payload) to { file, line, component } provenance. The
311
- // bridge endpoint exists; per-element provenance data must be
312
- // emitted by the connected app at build time — see the provenance
313
- // note in the help text below.
314
- "Requires the connected app to emit data-source-file / data-source-line / data-component-name attributes (e.g. via @vitejs/plugin-react jsxDEV or a Babel source plugin)."
315
- : undefined,
309
+ status: "available",
310
+ reason: operation === "resolveNodeToFile"
311
+ ? // resolveNodeToFile maps a runtime DOM node id (from the editor's
312
+ // 'select' payload) to { file, line, component } provenance. The
313
+ // bridge endpoint exists; per-element provenance data must be
314
+ // emitted by the connected app at build time — see the provenance
315
+ // note in the help text below.
316
+ "Requires the connected app to emit data-source-file / data-source-line / data-component-name attributes (e.g. via @vitejs/plugin-react jsxDEV or a Babel source plugin)."
317
+ : undefined,
316
318
  })),
317
319
  };
318
320
  }
@@ -320,19 +322,88 @@ function sendJson(res, statusCode, body) {
320
322
  res.writeHead(statusCode, {
321
323
  "content-type": "application/json; charset=utf-8",
322
324
  "access-control-allow-origin": "*",
323
- "access-control-allow-methods": "GET, OPTIONS",
324
- "access-control-allow-headers": "content-type",
325
+ "access-control-allow-methods": "GET, POST, OPTIONS",
326
+ "access-control-allow-headers": "content-type, x-bridge-token",
325
327
  "access-control-allow-private-network": "true",
326
328
  });
327
329
  res.end(`${JSON.stringify(body, null, 2)}\n`);
328
330
  }
331
+ /** Read the full request body as a UTF-8 string. */
332
+ async function readRequestBody(req) {
333
+ return new Promise((resolve, reject) => {
334
+ const chunks = [];
335
+ req.on("data", (chunk) => chunks.push(chunk));
336
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
337
+ req.on("error", reject);
338
+ });
339
+ }
340
+ /**
341
+ * Resolve `targetDir` under `rootPath` with realpath so that symlinks and
342
+ * traversal sequences (../../etc) cannot escape the root. Throws if the
343
+ * resolved path does not start with the resolved root.
344
+ */
345
+ async function assertPathInside(rootPath, targetPath) {
346
+ const resolvedRoot = await fs.realpath(rootPath).catch(() => {
347
+ throw new Error(`Bridge root path does not exist: ${rootPath}`);
348
+ });
349
+ // Resolve the parent directory (the file itself may not exist yet for writes).
350
+ const targetParent = path.dirname(path.resolve(rootPath, targetPath));
351
+ const resolvedParent = await fs.realpath(targetParent).catch(async () => {
352
+ // Parent may not exist yet; walk up until we find a real ancestor.
353
+ let candidate = targetParent;
354
+ for (let i = 0; i < 32; i++) {
355
+ const up = path.dirname(candidate);
356
+ if (up === candidate)
357
+ break;
358
+ candidate = up;
359
+ try {
360
+ return await fs.realpath(candidate);
361
+ }
362
+ catch {
363
+ // keep walking
364
+ }
365
+ }
366
+ throw new Error(`Cannot resolve parent directory: ${targetParent}`);
367
+ });
368
+ if (!resolvedParent.startsWith(resolvedRoot + path.sep) &&
369
+ resolvedParent !== resolvedRoot) {
370
+ throw new Error(`Path traversal detected: resolved target is outside root`);
371
+ }
372
+ }
373
+ /** Allowed file extensions for write/apply-edit operations. */
374
+ const ALLOWED_WRITE_EXTENSIONS = new Set([".html", ".htm", ".css"]);
375
+ function assertAllowedExtension(relPath) {
376
+ const ext = path.extname(relPath).toLowerCase();
377
+ if (!ALLOWED_WRITE_EXTENSIONS.has(ext)) {
378
+ throw new Error(`Write rejected: only .html, .htm, and .css files may be written via the bridge (got ${ext || "(no extension)"})`);
379
+ }
380
+ }
381
+ function countOccurrences(haystack, needle) {
382
+ if (!needle)
383
+ return 0;
384
+ let count = 0;
385
+ let index = haystack.indexOf(needle);
386
+ while (index !== -1) {
387
+ count += 1;
388
+ if (count > 1)
389
+ return count;
390
+ index = haystack.indexOf(needle, index + 1);
391
+ }
392
+ return count;
393
+ }
329
394
  export async function startDesignConnectBridge(manifest) {
395
+ // Mint a cryptographically random per-rootPath bridge token. This token is
396
+ // kept in-process only and is never emitted via the public GET routes so that
397
+ // an unauthenticated caller cannot read it. The server-side grant action
398
+ // obtains it out-of-band (via the exported bridge reference).
399
+ const bridgeToken = crypto.randomBytes(32).toString("hex");
330
400
  const server = http.createServer((req, res) => {
331
401
  if (req.method === "OPTIONS") {
332
402
  sendJson(res, 204, {});
333
403
  return;
334
404
  }
335
405
  const pathname = new URL(req.url ?? "/", manifest.bridgeUrl).pathname;
406
+ // ── Public read-only routes (no token required) ──────────────────────
336
407
  if (pathname === "/" || pathname === "/manifest.json") {
337
408
  sendJson(res, 200, manifest);
338
409
  return;
@@ -352,6 +423,164 @@ export async function startDesignConnectBridge(manifest) {
352
423
  sendJson(res, 200, { ok: true, source: manifest.source });
353
424
  return;
354
425
  }
426
+ // ── Token-gated write endpoints (POST only) ───────────────────────────
427
+ if (pathname === "/read-file" ||
428
+ pathname === "/write-file" ||
429
+ pathname === "/apply-edit") {
430
+ if (req.method !== "POST") {
431
+ sendJson(res, 405, { ok: false, error: "method not allowed" });
432
+ return;
433
+ }
434
+ // Authenticate with constant-time comparison to prevent timing attacks.
435
+ const tokenHeader = req.headers["x-bridge-token"];
436
+ const providedToken = typeof tokenHeader === "string" ? tokenHeader : "";
437
+ let tokenValid = false;
438
+ try {
439
+ tokenValid =
440
+ providedToken.length === bridgeToken.length &&
441
+ crypto.timingSafeEqual(Buffer.from(providedToken, "utf8"), Buffer.from(bridgeToken, "utf8"));
442
+ }
443
+ catch {
444
+ tokenValid = false;
445
+ }
446
+ if (!tokenValid) {
447
+ sendJson(res, 401, {
448
+ ok: false,
449
+ error: "invalid or missing bridge token",
450
+ });
451
+ return;
452
+ }
453
+ // Handle asynchronously so we can use await.
454
+ void (async () => {
455
+ try {
456
+ const raw = await readRequestBody(req);
457
+ const body = JSON.parse(raw);
458
+ const relPath = typeof body["relPath"] === "string" ? body["relPath"] : undefined;
459
+ if (!relPath) {
460
+ sendJson(res, 400, { ok: false, error: "relPath is required" });
461
+ return;
462
+ }
463
+ await assertPathInside(manifest.rootPath, relPath);
464
+ const absolutePath = path.resolve(manifest.rootPath, relPath);
465
+ if (pathname === "/read-file") {
466
+ // Read-file: no extension restriction — agents need to read any file.
467
+ let content;
468
+ try {
469
+ content = await fs.readFile(absolutePath, "utf8");
470
+ }
471
+ catch (err) {
472
+ const code = err instanceof Error &&
473
+ "code" in err &&
474
+ err.code;
475
+ if (code === "ENOENT") {
476
+ sendJson(res, 404, { ok: false, error: "file not found" });
477
+ }
478
+ else {
479
+ sendJson(res, 500, {
480
+ ok: false,
481
+ error: `read failed: ${err instanceof Error ? err.message : String(err)}`,
482
+ });
483
+ }
484
+ return;
485
+ }
486
+ sendJson(res, 200, { ok: true, content });
487
+ return;
488
+ }
489
+ // write-file and apply-edit only allow .html/.htm/.css.
490
+ assertAllowedExtension(relPath);
491
+ if (pathname === "/write-file") {
492
+ const content = typeof body["content"] === "string"
493
+ ? body["content"]
494
+ : undefined;
495
+ if (content === undefined) {
496
+ sendJson(res, 400, {
497
+ ok: false,
498
+ error: "content is required for write-file",
499
+ });
500
+ return;
501
+ }
502
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
503
+ await fs.writeFile(absolutePath, content, "utf8");
504
+ sendJson(res, 200, { ok: true, relPath });
505
+ return;
506
+ }
507
+ // /apply-edit: supports either full replace ({content}) or
508
+ // search-and-replace ({search, replace}).
509
+ if (typeof body["content"] === "string") {
510
+ // Full-file replace via apply-edit — same as write-file but keeps
511
+ // the endpoint semantically separate for callers that want to
512
+ // distinguish intent.
513
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
514
+ await fs.writeFile(absolutePath, body["content"], "utf8");
515
+ sendJson(res, 200, { ok: true, relPath, method: "replace" });
516
+ return;
517
+ }
518
+ const search = typeof body["search"] === "string" ? body["search"] : undefined;
519
+ const replace = typeof body["replace"] === "string" ? body["replace"] : undefined;
520
+ if (search === undefined || replace === undefined) {
521
+ sendJson(res, 400, {
522
+ ok: false,
523
+ error: "apply-edit requires either {content} for a full replace, or {search, replace} for a patch",
524
+ });
525
+ return;
526
+ }
527
+ let existing;
528
+ try {
529
+ existing = await fs.readFile(absolutePath, "utf8");
530
+ }
531
+ catch (err) {
532
+ const code = err instanceof Error &&
533
+ "code" in err &&
534
+ err.code;
535
+ if (code === "ENOENT") {
536
+ sendJson(res, 404, {
537
+ ok: false,
538
+ error: "file not found — use write-file to create new files",
539
+ });
540
+ }
541
+ else {
542
+ sendJson(res, 500, {
543
+ ok: false,
544
+ error: `read failed: ${err instanceof Error ? err.message : String(err)}`,
545
+ });
546
+ }
547
+ return;
548
+ }
549
+ if (search.length === 0) {
550
+ sendJson(res, 400, {
551
+ ok: false,
552
+ error: "search string must not be empty",
553
+ });
554
+ return;
555
+ }
556
+ const occurrenceCount = countOccurrences(existing, search);
557
+ if (occurrenceCount === 0) {
558
+ sendJson(res, 422, {
559
+ ok: false,
560
+ error: "search string not found in file",
561
+ });
562
+ return;
563
+ }
564
+ if (occurrenceCount > 1) {
565
+ sendJson(res, 422, {
566
+ ok: false,
567
+ error: "search string is ambiguous; it appears more than once in the file",
568
+ });
569
+ return;
570
+ }
571
+ const updated = existing.replace(search, replace);
572
+ await fs.writeFile(absolutePath, updated, "utf8");
573
+ sendJson(res, 200, { ok: true, relPath, method: "patch" });
574
+ }
575
+ catch (err) {
576
+ sendJson(res, 500, {
577
+ ok: false,
578
+ error: err instanceof Error ? err.message : String(err),
579
+ });
580
+ }
581
+ })();
582
+ return;
583
+ }
355
584
  sendJson(res, 404, { ok: false, error: "not found" });
356
585
  });
357
586
  await new Promise((resolve, reject) => {
@@ -361,7 +590,105 @@ export async function startDesignConnectBridge(manifest) {
361
590
  resolve();
362
591
  });
363
592
  });
364
- return { server, manifest };
593
+ return { server, manifest, bridgeToken };
594
+ }
595
+ /**
596
+ * Resolve the design app URL from an explicit value or environment variables.
597
+ * Returns undefined when no URL is configured (registration is optional).
598
+ */
599
+ export function resolveAppUrl(explicit) {
600
+ const raw = explicit ||
601
+ process.env["AGENT_NATIVE_URL"] ||
602
+ process.env["DESIGN_APP_URL"] ||
603
+ process.env["APP_URL"] ||
604
+ process.env["VITE_APP_URL"] ||
605
+ process.env["BETTER_AUTH_URL"] ||
606
+ process.env["VITE_BETTER_AUTH_URL"];
607
+ if (!raw)
608
+ return undefined;
609
+ try {
610
+ const parsed = new URL(raw.trim());
611
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
612
+ return undefined;
613
+ }
614
+ return parsed.toString().replace(/\/+$/, "");
615
+ }
616
+ catch {
617
+ return undefined;
618
+ }
619
+ }
620
+ /**
621
+ * Resolve a bearer token from environment variables for authenticating the
622
+ * self-registration POST. The CLI does not perform a device-code flow — it
623
+ * relies on a pre-minted token supplied via env var (e.g. the same token
624
+ * already written into the agent's MCP config).
625
+ */
626
+ function resolveAuthToken() {
627
+ return (process.env["AGENT_NATIVE_TOKEN"] ||
628
+ process.env["DESIGN_ACCESS_TOKEN"] ||
629
+ process.env["ACCESS_TOKEN"] ||
630
+ undefined);
631
+ }
632
+ /**
633
+ * POST to the design app's `connect-localhost` action endpoint to register
634
+ * (or refresh) the bridge connection and persist the real bridge token on the
635
+ * server row. This is a best-effort call: failures are logged but do not
636
+ * abort the bridge process.
637
+ *
638
+ * @param appUrl - Deployed design app base URL (e.g. https://design.agent-native.com)
639
+ * @param bridge - The running bridge returned by startDesignConnectBridge
640
+ * @param authToken - Optional bearer token for the authenticated action route
641
+ */
642
+ export async function registerConnectionWithServer(appUrl, bridge, authToken) {
643
+ const endpoint = `${appUrl}/_agent-native/actions/connect-localhost`;
644
+ const { manifest, bridgeToken } = bridge;
645
+ const payload = {
646
+ devServerUrl: manifest.devServerUrl,
647
+ bridgeUrl: manifest.bridgeUrl,
648
+ rootPath: manifest.rootPath,
649
+ capabilities: manifest.capabilities,
650
+ routeManifest: {
651
+ version: 1,
652
+ sourceType: "localhost",
653
+ devServerUrl: manifest.devServerUrl,
654
+ rootPath: manifest.rootPath,
655
+ routes: manifest.routes,
656
+ generatedAt: manifest.generatedAt,
657
+ },
658
+ // Include the real bridge token so the server stores it on the connection
659
+ // row. grant-localhost-write-consent then reads it from the row instead of
660
+ // minting its own unrelated token, which would always produce a 401.
661
+ bridgeToken,
662
+ status: "connected",
663
+ };
664
+ const headers = {
665
+ "content-type": "application/json",
666
+ };
667
+ if (authToken) {
668
+ headers["authorization"] = `Bearer ${authToken}`;
669
+ }
670
+ const controller = new AbortController();
671
+ const timeout = setTimeout(() => controller.abort(), 15_000);
672
+ try {
673
+ const res = await fetch(endpoint, {
674
+ method: "POST",
675
+ headers,
676
+ body: JSON.stringify(payload),
677
+ signal: controller.signal,
678
+ });
679
+ if (!res.ok) {
680
+ const message = await res.text().catch(() => res.statusText);
681
+ throw new Error(`${res.status} ${message || res.statusText}`);
682
+ }
683
+ }
684
+ catch (error) {
685
+ // Best-effort: network errors or auth issues are non-fatal, but make the
686
+ // problem discoverable before the user tries "Apply to source".
687
+ console.error(`[design connect] Could not register bridge with ${endpoint}: ${error instanceof Error ? error.message : String(error)}`);
688
+ }
689
+ finally {
690
+ clearTimeout(timeout);
691
+ }
365
692
  }
366
693
  function printHelp() {
367
694
  console.log(`Usage:
@@ -372,6 +699,8 @@ Options:
372
699
  --port <number> Local bridge port (default ${DEFAULT_BRIDGE_PORT})
373
700
  --root <path> App/repo root for route discovery (default cwd)
374
701
  --route-manifest <path> Non-destructive route manifest output path
702
+ --app-url <url> Deployed design app URL for self-registration
703
+ (also reads AGENT_NATIVE_URL / DESIGN_APP_URL env)
375
704
  --json Print the manifest JSON and exit
376
705
  --once Prepare/scaffold the manifest and exit
377
706
  --dry-run Print what would be exposed without writing files
@@ -423,6 +752,16 @@ export async function runDesign(argv) {
423
752
  console.error(`Manifest: ${manifest.bridgeUrl}/manifest.json`);
424
753
  console.error(`Routes: ${manifest.routeCount}`);
425
754
  console.error(`Dev URL: ${manifest.devServerUrl}`);
755
+ // Self-register with the design app server (best-effort). When an app URL
756
+ // is available (via --app-url or env var) the CLI POSTs the bridge token to
757
+ // connect-localhost so the server row stores the real token. Without this
758
+ // step grant-localhost-write-consent would mint a different token, causing
759
+ // every bridge write to return 401.
760
+ const appUrl = resolveAppUrl(parsed.appUrl);
761
+ if (appUrl) {
762
+ const authToken = resolveAuthToken();
763
+ void registerConnectionWithServer(appUrl, bridge, authToken);
764
+ }
426
765
  return await new Promise((resolve) => {
427
766
  const stop = () => {
428
767
  bridge.server.close(() => resolve(0));