@agent-native/core 0.79.17 → 0.79.19

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 (355) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/docs/content/file-uploads.md +11 -4
  4. package/corpus/core/docs/content/locales/ar-SA/multi-app-workspace.md +3 -3
  5. package/corpus/core/docs/content/locales/de-DE/multi-app-workspace.md +3 -3
  6. package/corpus/core/docs/content/locales/es-ES/multi-app-workspace.md +3 -3
  7. package/corpus/core/docs/content/locales/fr-FR/multi-app-workspace.md +3 -3
  8. package/corpus/core/docs/content/locales/hi-IN/multi-app-workspace.md +3 -3
  9. package/corpus/core/docs/content/locales/ja-JP/multi-app-workspace.md +3 -3
  10. package/corpus/core/docs/content/locales/ko-KR/multi-app-workspace.md +3 -3
  11. package/corpus/core/docs/content/locales/pt-BR/multi-app-workspace.md +3 -3
  12. package/corpus/core/docs/content/locales/zh-CN/multi-app-workspace.md +3 -3
  13. package/corpus/core/docs/content/locales/zh-TW/multi-app-workspace.md +3 -3
  14. package/corpus/core/docs/content/multi-app-workspace.md +3 -3
  15. package/corpus/core/docs/content/template-clips.md +14 -5
  16. package/corpus/core/package.json +1 -1
  17. package/corpus/core/src/agent/engine/builder-engine.ts +8 -3
  18. package/corpus/core/src/cli/mcp.ts +11 -6
  19. package/corpus/core/src/client/AssistantChat.tsx +6 -2
  20. package/corpus/core/src/client/chat/markdown-renderer.tsx +1 -0
  21. package/corpus/core/src/client/i18n.tsx +77 -65
  22. package/corpus/core/src/client/index.ts +7 -1
  23. package/corpus/core/src/client/org/OrgSwitcher.tsx +5 -5
  24. package/corpus/core/src/client/org/TeamPage.tsx +11 -3
  25. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +216 -0
  26. package/corpus/core/src/client/settings/index.ts +5 -0
  27. package/corpus/core/src/file-upload/index.ts +1 -0
  28. package/corpus/core/src/file-upload/registry.ts +22 -6
  29. package/corpus/core/src/file-upload/types.ts +6 -1
  30. package/corpus/core/src/integrations/adapters/email.ts +26 -18
  31. package/corpus/core/src/integrations/adapters/slack.ts +8 -7
  32. package/corpus/core/src/integrations/adapters/telegram.ts +9 -11
  33. package/corpus/core/src/integrations/adapters/whatsapp.ts +12 -9
  34. package/corpus/core/src/integrations/plugin.ts +79 -13
  35. package/corpus/core/src/onboarding/default-steps.ts +6 -3
  36. package/corpus/core/src/onboarding/types.ts +1 -1
  37. package/corpus/core/src/org/handlers.ts +1 -1
  38. package/corpus/core/src/server/agent-chat-plugin.ts +4 -7
  39. package/corpus/core/src/server/better-auth-instance.ts +19 -62
  40. package/corpus/core/src/server/core-routes-plugin.ts +93 -180
  41. package/corpus/core/src/server/create-server.ts +55 -116
  42. package/corpus/core/src/server/email-actions.ts +3 -6
  43. package/corpus/core/src/server/email.ts +52 -17
  44. package/corpus/core/src/server/index.ts +2 -1
  45. package/corpus/core/src/server/scoped-key-storage.ts +223 -0
  46. package/corpus/core/src/shared/agent-env.ts +3 -3
  47. package/corpus/core/src/sharing/actions/share-resource.ts +1 -1
  48. package/corpus/core/src/styles/agent-native.css +47 -3
  49. package/corpus/core/src/templates/workspace-root/AGENTS.md +3 -2
  50. package/corpus/core/src/templates/workspace-root/README.md +1 -1
  51. package/corpus/core/src/templates/workspace-root/scripts/repair-workspace-org.ts +29 -90
  52. package/corpus/core/src/terminal/pty-server.ts +3 -38
  53. package/corpus/core/src/triggers/dispatcher.ts +1 -2
  54. package/corpus/templates/.retired/issues/package.json +7 -0
  55. package/corpus/templates/.retired/meeting-notes/package.json +7 -0
  56. package/corpus/templates/.retired/recruiting/package.json +7 -0
  57. package/corpus/templates/.retired/scheduling/package.json +7 -0
  58. package/corpus/templates/.retired/voice/package.json +7 -0
  59. package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
  60. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -18
  61. package/corpus/templates/analytics/app/global.css +15 -0
  62. package/corpus/templates/analytics/app/i18n/zh-TW.ts +7 -1
  63. package/corpus/templates/analytics/app/i18n-data.ts +71 -2
  64. package/corpus/templates/analytics/app/pages/Ask.tsx +1 -1
  65. package/corpus/templates/analytics/app/pages/Settings.tsx +120 -73
  66. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +0 -11
  67. package/corpus/templates/analytics/app/routes/catalog.tsx +33 -176
  68. package/corpus/templates/analytics/app/routes/team.tsx +3 -2
  69. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-templates-are-tucked-into-settings-and-show-simple.md +6 -0
  70. package/corpus/templates/analytics/changelog/2026-06-26-the-ask-tab-uses-a-softer-dark-gray-canvas-while-sidebar-cha.md +6 -0
  71. package/corpus/templates/analytics/changelog/2026-06-26-the-sessions-page-shows-a-single-page-title-instead-of-repea.md +6 -0
  72. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +37 -28
  73. package/corpus/templates/assets/app/routes/settings.tsx +65 -43
  74. package/corpus/templates/assets/app/routes/team.tsx +2 -10
  75. package/corpus/templates/brain/app/routes/settings.tsx +390 -358
  76. package/corpus/templates/brain/app/routes/team.tsx +2 -8
  77. package/corpus/templates/calendar/actions/db-connect.ts +5 -29
  78. package/corpus/templates/calendar/app/components/CloudUpgrade.tsx +8 -51
  79. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +2 -1
  80. package/corpus/templates/calendar/app/components/calendar/GoogleSetupWizard.tsx +4 -2
  81. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +0 -2
  82. package/corpus/templates/calendar/app/pages/Settings.tsx +304 -271
  83. package/corpus/templates/calendar/app/routes/_app.team.tsx +2 -17
  84. package/corpus/templates/calendar/server/handlers/google-auth.ts +22 -9
  85. package/corpus/templates/calendar/server/lib/booking-emails.ts +1 -1
  86. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +1 -1
  87. package/corpus/templates/calendar/server/lib/google-calendar.ts +48 -28
  88. package/corpus/templates/chat/app/components/layout/Header.tsx +0 -1
  89. package/corpus/templates/chat/app/hooks/use-navigation-state.ts +4 -2
  90. package/corpus/templates/chat/app/routes/settings.tsx +54 -54
  91. package/corpus/templates/chat/app/routes/team.tsx +2 -10
  92. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +7 -2
  93. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +1 -1
  94. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +7 -0
  95. package/corpus/templates/clips/AGENTS.md +8 -0
  96. package/corpus/templates/clips/actions/invite-member.ts +1 -1
  97. package/corpus/templates/clips/actions/request-transcript.ts +1 -1
  98. package/corpus/templates/clips/app/components/library/library-layout.tsx +325 -333
  99. package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +80 -40
  100. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +2 -0
  101. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +629 -643
  102. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +263 -158
  103. package/corpus/templates/clips/app/routes/share.$shareId.tsx +10 -2
  104. package/corpus/templates/clips/changelog/2026-06-26-long-desktop-recordings-are-checkpointed-so-macos-finalizati.md +6 -0
  105. package/corpus/templates/clips/changelog/2026-06-26-non-mac-desktop-recordings-wait-for-upload-transcription.md +6 -0
  106. package/corpus/templates/clips/changelog/2026-06-26-recording-details-button-stays-at-the-far-right-on-small-screen.md +6 -0
  107. package/corpus/templates/clips/changelog/2026-06-26-s3-compatible-storage-keys-now-save-securely-from-clips-sett.md +6 -0
  108. package/corpus/templates/clips/changelog/2026-06-26-shared-clip-links-guide-agents-through-processing-transcripts.md +6 -0
  109. package/corpus/templates/clips/changelog/2026-06-26-the-agent-sidebar-now-slides-in-smoothly-without-duplicate-e.md +6 -0
  110. package/corpus/templates/clips/changelog/2026-06-26-the-clip-details-sidebar-now-collapses-into-a-slide-out-pane.md +6 -0
  111. package/corpus/templates/clips/desktop/src/lib/recorder.ts +55 -3
  112. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +217 -5
  113. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  114. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +176 -5
  115. package/corpus/templates/clips/server/lib/public-agent-context.ts +31 -0
  116. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +60 -14
  117. package/corpus/templates/clips/server/lib/video-storage.ts +37 -9
  118. package/corpus/templates/clips/server/routes/api/agent-transcript.json.get.ts +5 -0
  119. package/corpus/templates/content/app/components/layout/Header.tsx +0 -1
  120. package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +1 -0
  121. package/corpus/templates/content/app/routes/_app.settings.tsx +54 -55
  122. package/corpus/templates/content/app/routes/_app.team.tsx +2 -28
  123. package/corpus/templates/content/server/lib/notion.ts +43 -11
  124. package/corpus/templates/content/server/routes/api/notion/auth-url.get.ts +2 -2
  125. package/corpus/templates/content/server/routes/api/notion/status.get.ts +3 -3
  126. package/corpus/templates/design/app/routes/settings.tsx +60 -38
  127. package/corpus/templates/design/app/routes/team.tsx +2 -10
  128. package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +2 -2
  129. package/corpus/templates/dispatch/app/routes/settings.tsx +70 -55
  130. package/corpus/templates/dispatch/app/routes/team.tsx +5 -1
  131. package/corpus/templates/forms/actions/db-connect.ts +7 -36
  132. package/corpus/templates/forms/actions/navigate.ts +2 -2
  133. package/corpus/templates/forms/app/components/CloudUpgrade.tsx +8 -47
  134. package/corpus/templates/forms/app/components/layout/Header.tsx +0 -1
  135. package/corpus/templates/forms/app/components/layout/Layout.tsx +42 -25
  136. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +35 -76
  137. package/corpus/templates/forms/app/global.css +9 -25
  138. package/corpus/templates/forms/app/hooks/use-navigation-state.ts +4 -4
  139. package/corpus/templates/forms/app/i18n/ar-SA.ts +1 -0
  140. package/corpus/templates/forms/app/i18n/de-DE.ts +1 -0
  141. package/corpus/templates/forms/app/i18n/en-US.ts +1 -0
  142. package/corpus/templates/forms/app/i18n/es-ES.ts +1 -0
  143. package/corpus/templates/forms/app/i18n/fr-FR.ts +1 -0
  144. package/corpus/templates/forms/app/i18n/hi-IN.ts +1 -0
  145. package/corpus/templates/forms/app/i18n/ja-JP.ts +1 -0
  146. package/corpus/templates/forms/app/i18n/ko-KR.ts +1 -0
  147. package/corpus/templates/forms/app/i18n/pt-BR.ts +1 -0
  148. package/corpus/templates/forms/app/i18n/zh-CN.ts +1 -0
  149. package/corpus/templates/forms/app/i18n/zh-TW.ts +1 -0
  150. package/corpus/templates/forms/app/lib/route-prewarm.ts +9 -8
  151. package/corpus/templates/forms/app/pages/AskPage.tsx +70 -0
  152. package/corpus/templates/forms/app/pages/FormsListPage.tsx +74 -81
  153. package/corpus/templates/forms/app/root.tsx +1 -1
  154. package/corpus/templates/forms/app/routes/_app.ask.tsx +14 -0
  155. package/corpus/templates/forms/app/routes/_app.settings.tsx +54 -54
  156. package/corpus/templates/forms/app/routes/_app.team.tsx +2 -9
  157. package/corpus/templates/forms/app/routes/_index.tsx +21 -130
  158. package/corpus/templates/forms/changelog/2026-06-26-ask-forms-opens-as-a-standard-app-tab-with-a-simpler-sidebar.md +6 -0
  159. package/corpus/templates/forms/changelog/2026-06-26-forms-are-shown-in-a-tighter-row-list-with-a-denser-sidebar.md +6 -0
  160. package/corpus/templates/forms/shared/navigation.ts +4 -2
  161. package/corpus/templates/macros/app/routes/settings.tsx +54 -54
  162. package/corpus/templates/macros/app/routes/team.tsx +2 -10
  163. package/corpus/templates/mail/actions/helpers.ts +2 -2
  164. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +2 -1
  165. package/corpus/templates/mail/app/pages/SettingsPage.tsx +5 -5
  166. package/corpus/templates/mail/app/routes/team.tsx +3 -2
  167. package/corpus/templates/mail/server/handlers/emails.ts +3 -2
  168. package/corpus/templates/mail/server/handlers/google-auth.ts +19 -12
  169. package/corpus/templates/mail/server/lib/automation-engine.ts +3 -2
  170. package/corpus/templates/mail/server/lib/email-state.ts +2 -3
  171. package/corpus/templates/mail/server/lib/google-auth.ts +28 -11
  172. package/corpus/templates/mail/server/lib/jobs.ts +3 -2
  173. package/corpus/templates/mail/server/onboarding.ts +9 -6
  174. package/corpus/templates/mail/server/routes/api/attachments.get.ts +3 -3
  175. package/corpus/templates/plan/actions/request-plan-access.ts +1 -1
  176. package/corpus/templates/plan/app/components/layout/Header.tsx +0 -1
  177. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +56 -4
  178. package/corpus/templates/plan/app/global.css +8 -3
  179. package/corpus/templates/plan/app/hooks/use-navigation-state.ts +4 -2
  180. package/corpus/templates/plan/app/lib/route-prewarm.ts +0 -4
  181. package/corpus/templates/plan/app/pages/PlansPage.tsx +1 -1
  182. package/corpus/templates/plan/app/routes/settings.tsx +75 -73
  183. package/corpus/templates/plan/app/routes/team.tsx +2 -8
  184. package/corpus/templates/plan/changelog/2026-06-26-canvas-grid-panning-now-stays-smooth-and-fills-the-workspace.md +6 -0
  185. package/corpus/templates/plan/server/lib/comment-notifications.ts +1 -1
  186. package/corpus/templates/slides/actions/check-image-gen.ts +2 -1
  187. package/corpus/templates/slides/actions/db-connect.ts +7 -45
  188. package/corpus/templates/slides/actions/edit-image.ts +6 -3
  189. package/corpus/templates/slides/actions/generate-image-api.ts +1 -1
  190. package/corpus/templates/slides/actions/generate-image.ts +2 -2
  191. package/corpus/templates/slides/actions/generate-slides-ai.ts +5 -3
  192. package/corpus/templates/slides/actions/image-gen-status.ts +4 -3
  193. package/corpus/templates/slides/actions/image-search.ts +6 -3
  194. package/corpus/templates/slides/actions/search-images.ts +6 -3
  195. package/corpus/templates/slides/app/components/CloudUpgrade.tsx +8 -51
  196. package/corpus/templates/slides/app/components/layout/Header.tsx +0 -1
  197. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +0 -2
  198. package/corpus/templates/slides/app/routes/settings.tsx +54 -54
  199. package/corpus/templates/slides/app/routes/team.tsx +2 -12
  200. package/corpus/templates/slides/server/handlers/google-docs-auth.ts +9 -9
  201. package/corpus/templates/slides/server/handlers/image-gen.ts +5 -1
  202. package/corpus/templates/slides/server/handlers/image-providers/gemini.ts +9 -1
  203. package/corpus/templates/slides/server/handlers/image-providers/index.ts +18 -8
  204. package/corpus/templates/slides/server/handlers/image-providers/openai.ts +10 -4
  205. package/corpus/templates/slides/server/handlers/image-providers/types.ts +1 -0
  206. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +59 -34
  207. package/corpus/templates/slides/server/register-secrets.ts +60 -0
  208. package/corpus/templates/videos/actions/db-connect.ts +7 -45
  209. package/corpus/templates/videos/app/components/CloudUpgrade.tsx +8 -51
  210. package/corpus/templates/videos/app/components/Sidebar.tsx +1 -9
  211. package/corpus/templates/videos/app/components/layout/Header.tsx +0 -1
  212. package/corpus/templates/videos/app/components/layout/NavSidebar.tsx +0 -3
  213. package/corpus/templates/videos/app/routes/settings.tsx +54 -54
  214. package/corpus/templates/videos/app/routes/team.tsx +2 -8
  215. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  216. package/dist/agent/engine/builder-engine.js +8 -3
  217. package/dist/agent/engine/builder-engine.js.map +1 -1
  218. package/dist/cli/mcp.d.ts.map +1 -1
  219. package/dist/cli/mcp.js +3 -4
  220. package/dist/cli/mcp.js.map +1 -1
  221. package/dist/client/AssistantChat.d.ts.map +1 -1
  222. package/dist/client/AssistantChat.js +6 -2
  223. package/dist/client/AssistantChat.js.map +1 -1
  224. package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
  225. package/dist/client/chat/markdown-renderer.js +1 -0
  226. package/dist/client/chat/markdown-renderer.js.map +1 -1
  227. package/dist/client/i18n.d.ts.map +1 -1
  228. package/dist/client/i18n.js +20 -9
  229. package/dist/client/i18n.js.map +1 -1
  230. package/dist/client/index.d.ts +1 -1
  231. package/dist/client/index.d.ts.map +1 -1
  232. package/dist/client/index.js +1 -1
  233. package/dist/client/index.js.map +1 -1
  234. package/dist/client/org/OrgSwitcher.d.ts +3 -3
  235. package/dist/client/org/OrgSwitcher.js +2 -2
  236. package/dist/client/org/OrgSwitcher.js.map +1 -1
  237. package/dist/client/org/TeamPage.d.ts +6 -1
  238. package/dist/client/org/TeamPage.d.ts.map +1 -1
  239. package/dist/client/org/TeamPage.js +2 -2
  240. package/dist/client/org/TeamPage.js.map +1 -1
  241. package/dist/client/settings/SettingsTabsPage.d.ts +27 -0
  242. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -0
  243. package/dist/client/settings/SettingsTabsPage.js +108 -0
  244. package/dist/client/settings/SettingsTabsPage.js.map +1 -0
  245. package/dist/client/settings/index.d.ts +1 -0
  246. package/dist/client/settings/index.d.ts.map +1 -1
  247. package/dist/client/settings/index.js +1 -0
  248. package/dist/client/settings/index.js.map +1 -1
  249. package/dist/file-upload/index.d.ts +1 -1
  250. package/dist/file-upload/index.d.ts.map +1 -1
  251. package/dist/file-upload/index.js +1 -1
  252. package/dist/file-upload/index.js.map +1 -1
  253. package/dist/file-upload/registry.d.ts +1 -0
  254. package/dist/file-upload/registry.d.ts.map +1 -1
  255. package/dist/file-upload/registry.js +24 -6
  256. package/dist/file-upload/registry.js.map +1 -1
  257. package/dist/file-upload/types.d.ts +6 -1
  258. package/dist/file-upload/types.d.ts.map +1 -1
  259. package/dist/file-upload/types.js.map +1 -1
  260. package/dist/integrations/adapters/email.d.ts.map +1 -1
  261. package/dist/integrations/adapters/email.js +18 -18
  262. package/dist/integrations/adapters/email.js.map +1 -1
  263. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  264. package/dist/integrations/adapters/slack.js +8 -7
  265. package/dist/integrations/adapters/slack.js.map +1 -1
  266. package/dist/integrations/adapters/telegram.d.ts.map +1 -1
  267. package/dist/integrations/adapters/telegram.js +9 -9
  268. package/dist/integrations/adapters/telegram.js.map +1 -1
  269. package/dist/integrations/adapters/whatsapp.d.ts.map +1 -1
  270. package/dist/integrations/adapters/whatsapp.js +10 -9
  271. package/dist/integrations/adapters/whatsapp.js.map +1 -1
  272. package/dist/integrations/plugin.d.ts.map +1 -1
  273. package/dist/integrations/plugin.js +47 -13
  274. package/dist/integrations/plugin.js.map +1 -1
  275. package/dist/notifications/routes.d.ts +3 -3
  276. package/dist/observability/routes.d.ts +6 -6
  277. package/dist/onboarding/default-steps.d.ts.map +1 -1
  278. package/dist/onboarding/default-steps.js +6 -5
  279. package/dist/onboarding/default-steps.js.map +1 -1
  280. package/dist/onboarding/types.d.ts +1 -1
  281. package/dist/onboarding/types.d.ts.map +1 -1
  282. package/dist/onboarding/types.js.map +1 -1
  283. package/dist/org/handlers.js +1 -1
  284. package/dist/org/handlers.js.map +1 -1
  285. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  286. package/dist/server/agent-chat-plugin.js +4 -6
  287. package/dist/server/agent-chat-plugin.js.map +1 -1
  288. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  289. package/dist/server/better-auth-instance.d.ts.map +1 -1
  290. package/dist/server/better-auth-instance.js +17 -56
  291. package/dist/server/better-auth-instance.js.map +1 -1
  292. package/dist/server/core-routes-plugin.d.ts +1 -1
  293. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  294. package/dist/server/core-routes-plugin.js +67 -153
  295. package/dist/server/core-routes-plugin.js.map +1 -1
  296. package/dist/server/create-server.d.ts +3 -9
  297. package/dist/server/create-server.d.ts.map +1 -1
  298. package/dist/server/create-server.js +37 -91
  299. package/dist/server/create-server.js.map +1 -1
  300. package/dist/server/email-actions.d.ts +2 -3
  301. package/dist/server/email-actions.d.ts.map +1 -1
  302. package/dist/server/email-actions.js +3 -6
  303. package/dist/server/email-actions.js.map +1 -1
  304. package/dist/server/email.d.ts +3 -3
  305. package/dist/server/email.d.ts.map +1 -1
  306. package/dist/server/email.js +40 -19
  307. package/dist/server/email.js.map +1 -1
  308. package/dist/server/index.d.ts +3 -3
  309. package/dist/server/index.d.ts.map +1 -1
  310. package/dist/server/index.js +3 -3
  311. package/dist/server/index.js.map +1 -1
  312. package/dist/server/scoped-key-storage.d.ts +22 -0
  313. package/dist/server/scoped-key-storage.d.ts.map +1 -0
  314. package/dist/server/scoped-key-storage.js +152 -0
  315. package/dist/server/scoped-key-storage.js.map +1 -0
  316. package/dist/shared/agent-env.d.ts +3 -3
  317. package/dist/shared/agent-env.d.ts.map +1 -1
  318. package/dist/shared/agent-env.js +3 -3
  319. package/dist/shared/agent-env.js.map +1 -1
  320. package/dist/sharing/actions/share-resource.js +1 -1
  321. package/dist/sharing/actions/share-resource.js.map +1 -1
  322. package/dist/styles/agent-native.css +47 -3
  323. package/dist/templates/workspace-root/AGENTS.md +3 -2
  324. package/dist/templates/workspace-root/README.md +1 -1
  325. package/dist/templates/workspace-root/scripts/repair-workspace-org.ts +29 -90
  326. package/dist/terminal/pty-server.d.ts.map +1 -1
  327. package/dist/terminal/pty-server.js +3 -36
  328. package/dist/terminal/pty-server.js.map +1 -1
  329. package/dist/triggers/dispatcher.js +1 -1
  330. package/dist/triggers/dispatcher.js.map +1 -1
  331. package/docs/content/file-uploads.md +11 -4
  332. package/docs/content/locales/ar-SA/multi-app-workspace.md +3 -3
  333. package/docs/content/locales/de-DE/multi-app-workspace.md +3 -3
  334. package/docs/content/locales/es-ES/multi-app-workspace.md +3 -3
  335. package/docs/content/locales/fr-FR/multi-app-workspace.md +3 -3
  336. package/docs/content/locales/hi-IN/multi-app-workspace.md +3 -3
  337. package/docs/content/locales/ja-JP/multi-app-workspace.md +3 -3
  338. package/docs/content/locales/ko-KR/multi-app-workspace.md +3 -3
  339. package/docs/content/locales/pt-BR/multi-app-workspace.md +3 -3
  340. package/docs/content/locales/zh-CN/multi-app-workspace.md +3 -3
  341. package/docs/content/locales/zh-TW/multi-app-workspace.md +3 -3
  342. package/docs/content/multi-app-workspace.md +3 -3
  343. package/docs/content/template-clips.md +14 -5
  344. package/package.json +1 -1
  345. package/src/templates/workspace-root/AGENTS.md +3 -2
  346. package/src/templates/workspace-root/README.md +1 -1
  347. package/src/templates/workspace-root/scripts/repair-workspace-org.ts +29 -90
  348. package/corpus/core/src/server/env-var-writes.ts +0 -16
  349. package/corpus/templates/analytics/app/pages/Team.tsx +0 -9
  350. package/corpus/templates/calendar/app/pages/Team.tsx +0 -12
  351. package/corpus/templates/mail/app/pages/Team.tsx +0 -17
  352. package/dist/server/env-var-writes.d.ts +0 -10
  353. package/dist/server/env-var-writes.d.ts.map +0 -1
  354. package/dist/server/env-var-writes.js +0 -18
  355. package/dist/server/env-var-writes.js.map +0 -1
@@ -1,5 +1,3 @@
1
- import path from "path";
2
-
3
1
  import {
4
2
  createApp,
5
3
  createRouter,
@@ -11,8 +9,8 @@ import {
11
9
  type H3Event,
12
10
  } from "h3";
13
11
 
12
+ import { getOrgContext } from "../org/context.js";
14
13
  import { readBody } from "../server/h3-helpers.js";
15
- import { agentEnv } from "../shared/agent-env.js";
16
14
  import { EMBED_TARGET_HEADER } from "../shared/embed-auth.js";
17
15
  import {
18
16
  EMBED_TRANSPLANT_HEADER,
@@ -20,12 +18,19 @@ import {
20
18
  MCP_EMBED_CORS_ALLOW_HEADERS,
21
19
  shouldAllowMcpEmbedCredentials,
22
20
  } from "../shared/mcp-embed-headers.js";
23
- import { BUILDER_ENV_KEYS } from "./builder-browser.js";
21
+ import { getSession } from "./auth.js";
24
22
  import {
25
23
  getAllowedCorsOrigin,
26
24
  readCorsAllowedOrigins,
27
25
  } from "./cors-origins.js";
28
- import { isEnvVarWriteAllowed } from "./env-var-writes.js";
26
+ import { resolveSecret } from "./credential-provider.js";
27
+ import { runWithRequestContext } from "./request-context.js";
28
+ import {
29
+ findUnsupportedScopedKeyNames,
30
+ saveKeyValuesToScopedSecrets,
31
+ ScopedKeyStorageError,
32
+ type ScopedKeySaveRequestScope,
33
+ } from "./scoped-key-storage.js";
29
34
 
30
35
  export interface EnvKeyConfig {
31
36
  /** Environment variable name (e.g. "HUBSPOT_ACCESS_TOKEN") */
@@ -47,70 +52,10 @@ export interface CreateServerOptions {
47
52
  pingMessage?: string;
48
53
  /** Disable the /_agent-native/ping health check. Default: false */
49
54
  disablePing?: boolean;
50
- /** Env key configuration for the settings UI. Enables /_agent-native/env-status and /_agent-native/env-vars routes. */
55
+ /** Key configuration for the settings UI. Enables status plus the scoped-secret compatibility save route. */
51
56
  envKeys?: EnvKeyConfig[];
52
57
  }
53
58
 
54
- /**
55
- * Upsert vars into a .env file, preserving existing structure.
56
- */
57
- export async function upsertEnvFile(
58
- envPath: string,
59
- vars: Array<{ key: string; value: string }>,
60
- ): Promise<void> {
61
- // Sanitize: reject values that could inject additional env vars
62
- for (const { key, value } of vars) {
63
- if (/[\n\r\0]/.test(value)) {
64
- throw new Error(
65
- `Invalid env var value for ${key}: must not contain newlines or control characters`,
66
- );
67
- }
68
- }
69
-
70
- const fs = await import("fs");
71
-
72
- let content = "";
73
- try {
74
- content = fs.readFileSync(envPath, "utf-8");
75
- } catch {
76
- // File doesn't exist yet
77
- }
78
-
79
- const lines = content.split("\n");
80
- const remaining = new Map(vars.map((v) => [v.key, v.value]));
81
-
82
- // Update existing lines in place
83
- const updated = lines.map((line) => {
84
- const trimmed = line.trim();
85
- if (!trimmed || trimmed.startsWith("#")) return line;
86
- const eqIndex = trimmed.indexOf("=");
87
- if (eqIndex === -1) return line;
88
- const key = trimmed.slice(0, eqIndex).trim();
89
- if (remaining.has(key)) {
90
- const value = remaining.get(key)!;
91
- remaining.delete(key);
92
- return `${key}=${value}`;
93
- }
94
- return line;
95
- });
96
-
97
- // Append new vars
98
- for (const [key, value] of remaining) {
99
- updated.push(`${key}=${value}`);
100
- }
101
-
102
- // Ensure trailing newline
103
- let result = updated.join("\n");
104
- if (!result.endsWith("\n")) result += "\n";
105
-
106
- try {
107
- fs.mkdirSync(path.dirname(envPath), { recursive: true });
108
- fs.writeFileSync(envPath, result);
109
- } catch {
110
- // Edge runtimes don't have writable filesystem — skip silently
111
- }
112
- }
113
-
114
59
  export interface CreateServerResult {
115
60
  app: ReturnType<typeof createApp>;
116
61
  router: ReturnType<typeof createRouter>;
@@ -120,7 +65,8 @@ export interface CreateServerResult {
120
65
  * Create a pre-configured H3 app with standard agent-native setup:
121
66
  * - CORS headers via middleware
122
67
  * - /_agent-native/ping health check
123
- * - /_agent-native/env-status and /_agent-native/env-vars (when envKeys is provided)
68
+ * - /_agent-native/env-status and the scoped-secret compatibility save route
69
+ * at /_agent-native/env-vars (when envKeys is provided)
124
70
  *
125
71
  * Returns { app, router } — mount routes on `router`.
126
72
  */
@@ -242,7 +188,6 @@ export function createServer(
242
188
  }
243
189
 
244
190
  const router = createRouter();
245
- app.use(router);
246
191
 
247
192
  // Health check
248
193
  if (!options.disablePing) {
@@ -259,73 +204,67 @@ export function createServer(
259
204
  // Env key management routes
260
205
  if (options.envKeys) {
261
206
  const envKeys = options.envKeys;
207
+ const allowedEnvKeyNames = envKeys.map(({ key }) => key);
262
208
 
263
209
  router.get(
264
210
  "/_agent-native/env-status",
265
- defineEventHandler(() => {
266
- return envKeys.map((cfg) => ({
267
- key: cfg.key,
268
- label: cfg.label,
269
- required: cfg.required ?? false,
270
- configured: !!process.env[cfg.key],
271
- ...(cfg.helpText ? { helpText: cfg.helpText } : {}),
272
- }));
211
+ defineEventHandler(async (event) => {
212
+ const session = await getSession(event).catch(() => null);
213
+ const userEmail = session?.email;
214
+ let orgId: string | undefined;
215
+ if (userEmail) {
216
+ const orgCtx = await getOrgContext(event).catch(() => null);
217
+ orgId = orgCtx?.orgId ?? undefined;
218
+ }
219
+ return Promise.all(
220
+ envKeys.map(async (cfg) => ({
221
+ key: cfg.key,
222
+ label: cfg.label,
223
+ required: cfg.required ?? false,
224
+ configured:
225
+ Boolean(process.env[cfg.key]) ||
226
+ (await runWithRequestContext({ userEmail, orgId }, () =>
227
+ resolveSecret(cfg.key).then(Boolean),
228
+ )),
229
+ ...(cfg.helpText ? { helpText: cfg.helpText } : {}),
230
+ })),
231
+ );
273
232
  }),
274
233
  );
275
234
 
276
235
  router.post(
277
236
  "/_agent-native/env-vars",
278
237
  defineEventHandler(async (event: H3Event) => {
279
- // Env vars are deployment-wide globals — see isEnvVarWriteAllowed
280
- // above. Disable the endpoint on any multi-tenant deploy.
281
- if (!isEnvVarWriteAllowed()) {
282
- setResponseStatus(event, 403);
283
- return {
284
- error:
285
- "env-vars endpoint disabled on multi-tenant deployments. Use scoped secrets or credentials for user/org API keys.",
286
- };
287
- }
288
-
289
238
  const body = await readBody(event);
290
- const { vars } = body as {
239
+ const { vars, scope } = body as {
291
240
  vars?: Array<{ key: string; value: string }>;
241
+ scope?: ScopedKeySaveRequestScope;
292
242
  };
293
-
294
- if (!Array.isArray(vars) || vars.length === 0) {
295
- setResponseStatus(event, 400);
296
- return { error: "vars array required" };
297
- }
298
-
299
- // Only allow keys that are in the env config
300
- const allowedKeys = new Set(envKeys.map((k) => k.key));
301
- const blockedEnvVarWriteKeys = new Set<string>(BUILDER_ENV_KEYS);
302
- const filtered = vars.filter(
303
- (v) =>
304
- typeof v.key === "string" &&
305
- allowedKeys.has(v.key) &&
306
- !blockedEnvVarWriteKeys.has(v.key),
243
+ const unsupportedKeys = findUnsupportedScopedKeyNames(
244
+ vars,
245
+ allowedEnvKeyNames,
307
246
  );
308
- if (filtered.length === 0) {
247
+ if (unsupportedKeys.length > 0) {
309
248
  setResponseStatus(event, 400);
310
- return { error: "No recognized env keys in request" };
249
+ return {
250
+ error: `Unsupported env key${unsupportedKeys.length === 1 ? "" : "s"}: ${unsupportedKeys.join(", ")}`,
251
+ };
311
252
  }
312
-
313
- // Write to .env file
314
- const envPath = path.join(process.cwd(), ".env");
315
- await upsertEnvFile(envPath, filtered);
316
-
317
- // Update process.env so the app picks up the new values immediately
318
- for (const { key, value } of filtered) {
319
- process.env[key] = value;
253
+ try {
254
+ const result = await saveKeyValuesToScopedSecrets(event, vars, scope);
255
+ return { saved: result.saved, storage: "scoped-secrets" };
256
+ } catch (err) {
257
+ if (err instanceof ScopedKeyStorageError) {
258
+ setResponseStatus(event, err.statusCode);
259
+ return { error: err.message };
260
+ }
261
+ setResponseStatus(event, 500);
262
+ return { error: "Failed to save keys" };
320
263
  }
321
-
322
- // Notify parent (Builder or frame) via postMessage
323
- agentEnv.setVars(filtered);
324
-
325
- return { saved: filtered.map((v) => v.key) };
326
264
  }),
327
265
  );
328
266
  }
329
267
 
268
+ app.use(router);
330
269
  return { app, router };
331
270
  }
@@ -2,9 +2,8 @@
2
2
  * Framework-level agent action for sending transactional/notification emails
3
3
  * via the configured core email transport (Resend or SendGrid).
4
4
  *
5
- * Registered as a native tool in every template when RESEND_API_KEY or
6
- * SENDGRID_API_KEY is set. When neither key is present the tool is omitted
7
- * from the agent surface so the agent never sees it.
5
+ * Registered as a native tool in every template. sendEmail() checks the
6
+ * scoped Resend/SendGrid configuration at call time.
8
7
  *
9
8
  * SAFETY: the action description instructs the agent to draft-first and only
10
9
  * send when the user explicitly confirms, matching the mail template convention.
@@ -16,7 +15,7 @@
16
15
  */
17
16
 
18
17
  import type { ActionEntry } from "../agent/production-agent.js";
19
- import { isEmailConfigured, sendEmail } from "./email.js";
18
+ import { sendEmail } from "./email.js";
20
19
 
21
20
  function markdownToText(md: string): string {
22
21
  return md
@@ -115,8 +114,6 @@ function markdownToHtml(md: string): string {
115
114
  }
116
115
 
117
116
  export function createCoreEmailActionEntries(): Record<string, ActionEntry> {
118
- if (!isEmailConfigured()) return {};
119
-
120
117
  return {
121
118
  "core-send-email": {
122
119
  tool: {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Email transport for system emails (password resets, invitations, notifications).
3
3
  *
4
- * Providers are selected by env var:
4
+ * Providers are selected by scoped secrets:
5
5
  * RESEND_API_KEY — https://resend.com
6
6
  * SENDGRID_API_KEY — https://sendgrid.com
7
7
  * EMAIL_FROM — "Name <addr@domain>" (optional; defaults to Resend's sandbox)
@@ -10,6 +10,8 @@
10
10
  * so the reset-password flow still works end-to-end for local development.
11
11
  */
12
12
 
13
+ import { resolveSecret } from "./credential-provider.js";
14
+
13
15
  export type EmailProvider = "resend" | "sendgrid" | "dev";
14
16
 
15
17
  export interface EmailAttachment {
@@ -33,32 +35,65 @@ export interface SendEmailArgs {
33
35
  attachments?: EmailAttachment[];
34
36
  }
35
37
 
36
- export function isEmailConfigured(): boolean {
37
- return !!(process.env.RESEND_API_KEY || process.env.SENDGRID_API_KEY);
38
+ interface EmailTransportConfig {
39
+ provider: EmailProvider;
40
+ resendApiKey?: string;
41
+ sendgridApiKey?: string;
42
+ from?: string;
38
43
  }
39
44
 
40
- export function getEmailProvider(): EmailProvider {
41
- if (process.env.RESEND_API_KEY) return "resend";
42
- if (process.env.SENDGRID_API_KEY) return "sendgrid";
43
- return "dev";
45
+ async function resolveEmailTransport(): Promise<EmailTransportConfig> {
46
+ const [resendApiKey, sendgridApiKey, from] = await Promise.all([
47
+ resolveSecret("RESEND_API_KEY"),
48
+ resolveSecret("SENDGRID_API_KEY"),
49
+ resolveSecret("EMAIL_FROM"),
50
+ ]);
51
+ const resolvedFrom = from ?? undefined;
52
+ if (resendApiKey) {
53
+ return {
54
+ provider: "resend",
55
+ resendApiKey,
56
+ from: resolvedFrom,
57
+ };
58
+ }
59
+ if (sendgridApiKey) {
60
+ return {
61
+ provider: "sendgrid",
62
+ sendgridApiKey,
63
+ from: resolvedFrom,
64
+ };
65
+ }
66
+ return { provider: "dev", from: resolvedFrom };
44
67
  }
45
68
 
46
- function getFromAddress(override?: string, provider?: EmailProvider): string {
47
- const explicit = override || process.env.EMAIL_FROM;
69
+ export async function isEmailConfigured(): Promise<boolean> {
70
+ return (await resolveEmailTransport()).provider !== "dev";
71
+ }
72
+
73
+ export async function getEmailProvider(): Promise<EmailProvider> {
74
+ return (await resolveEmailTransport()).provider;
75
+ }
76
+
77
+ function getFromAddress(
78
+ config: EmailTransportConfig,
79
+ override?: string,
80
+ ): string {
81
+ const explicit = override || config.from;
48
82
  if (explicit) return explicit;
49
83
  // Resend lets unverified accounts send from its sandbox domain; SendGrid
50
84
  // does not, so falling back there would cause silent 403s at runtime.
51
- if (provider === "sendgrid") {
85
+ if (config.provider === "sendgrid") {
52
86
  throw new Error(
53
- "EMAIL_FROM is required when using SendGrid — set it to a verified sender address.",
87
+ "EMAIL_FROM is required when using SendGrid — save it as a verified sender address.",
54
88
  );
55
89
  }
56
90
  return "Agent Native <onboarding@resend.dev>";
57
91
  }
58
92
 
59
93
  export async function sendEmail(args: SendEmailArgs): Promise<void> {
60
- const provider = getEmailProvider();
61
- const from = getFromAddress(args.from, provider);
94
+ const config = await resolveEmailTransport();
95
+ const provider = config.provider;
96
+ const from = getFromAddress(config, args.from);
62
97
 
63
98
  if (provider === "resend") {
64
99
  const payload: Record<string, unknown> = {
@@ -89,7 +124,7 @@ export async function sendEmail(args: SendEmailArgs): Promise<void> {
89
124
  const res = await fetch("https://api.resend.com/emails", {
90
125
  method: "POST",
91
126
  headers: {
92
- Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
127
+ Authorization: `Bearer ${config.resendApiKey}`,
93
128
  "Content-Type": "application/json",
94
129
  },
95
130
  body: JSON.stringify(payload),
@@ -140,7 +175,7 @@ export async function sendEmail(args: SendEmailArgs): Promise<void> {
140
175
  const res = await fetch("https://api.sendgrid.com/v3/mail/send", {
141
176
  method: "POST",
142
177
  headers: {
143
- Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,
178
+ Authorization: `Bearer ${config.sendgridApiKey}`,
144
179
  "Content-Type": "application/json",
145
180
  },
146
181
  body: JSON.stringify(sgPayload),
@@ -157,12 +192,12 @@ export async function sendEmail(args: SendEmailArgs): Promise<void> {
157
192
  // to send rather than silently leaking secrets to logs.
158
193
  if (process.env.NODE_ENV === "production") {
159
194
  throw new Error(
160
- "No email provider configured. Set RESEND_API_KEY or SENDGRID_API_KEY.",
195
+ "No email provider configured. Save RESEND_API_KEY or SENDGRID_API_KEY in settings.",
161
196
  );
162
197
  }
163
198
  console.log(
164
199
  `\n[agent-native:email] No email provider configured. ` +
165
- `Set RESEND_API_KEY or SENDGRID_API_KEY to send real emails.\n` +
200
+ `Save RESEND_API_KEY or SENDGRID_API_KEY in settings to send real emails.\n` +
166
201
  `---\nTo: ${args.to}\nFrom: ${from}\nSubject: ${args.subject}\n\n` +
167
202
  `${args.text || stripHtml(args.html)}\n---\n`,
168
203
  );
@@ -1,6 +1,5 @@
1
1
  export {
2
2
  createServer,
3
- upsertEnvFile,
4
3
  type CreateServerOptions,
5
4
  type EnvKeyConfig,
6
5
  } from "./create-server.js";
@@ -325,6 +324,7 @@ export {
325
324
  unregisterFileUploadProvider,
326
325
  listFileUploadProviders,
327
326
  getActiveFileUploadProvider,
327
+ getActiveFileUploadProviderForRequest,
328
328
  uploadFile,
329
329
  builderFileUploadProvider,
330
330
  type FileUploadInput,
@@ -381,6 +381,7 @@ export {
381
381
  resolveHasCompleteBuilderConnection,
382
382
  resolveBuilderCredentials,
383
383
  resolveBuilderCredential,
384
+ readDeployCredentialEnv,
384
385
  writeBuilderCredentials,
385
386
  deleteBuilderCredentials,
386
387
  resolveSecret,
@@ -0,0 +1,223 @@
1
+ import type { H3Event } from "h3";
2
+
3
+ import { getOrgContext } from "../org/context.js";
4
+ import {
5
+ getRequiredSecret,
6
+ type RegisteredSecret,
7
+ type SecretScope,
8
+ } from "../secrets/register.js";
9
+ import { writeAppSecret } from "../secrets/storage.js";
10
+ import { getSession } from "./auth.js";
11
+
12
+ const KEY_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
13
+
14
+ export type ScopedKeySaveRequestScope =
15
+ | "app"
16
+ | "auto"
17
+ | "org"
18
+ | "user"
19
+ | "workspace"
20
+ | undefined;
21
+
22
+ export interface ScopedKeyValue {
23
+ key: string;
24
+ value: string;
25
+ }
26
+
27
+ export interface ScopedKeySaveRow {
28
+ key: string;
29
+ scope: SecretScope;
30
+ scopeId: string;
31
+ }
32
+
33
+ export class ScopedKeyStorageError extends Error {
34
+ constructor(
35
+ readonly statusCode: number,
36
+ message: string,
37
+ ) {
38
+ super(message);
39
+ this.name = "ScopedKeyStorageError";
40
+ }
41
+ }
42
+
43
+ export function findUnsupportedScopedKeyNames(
44
+ vars: unknown,
45
+ allowedKeys: Iterable<string>,
46
+ ): string[] {
47
+ if (!Array.isArray(vars)) return [];
48
+
49
+ const allowed = new Set(allowedKeys);
50
+ const unsupported = new Set<string>();
51
+ for (const entry of vars) {
52
+ if (!entry || typeof entry !== "object") continue;
53
+ const key = (entry as { key?: unknown }).key;
54
+ const normalizedKey = typeof key === "string" ? key.trim() : "";
55
+ if (normalizedKey && !allowed.has(normalizedKey)) {
56
+ unsupported.add(normalizedKey);
57
+ }
58
+ }
59
+ return [...unsupported];
60
+ }
61
+
62
+ function redactSecretFromMessage(message: string, secretValue: string): string {
63
+ if (!message || !secretValue) return message;
64
+ return message.split(secretValue).join("[redacted]");
65
+ }
66
+
67
+ function normalizeKeyValueVars(vars: unknown): ScopedKeyValue[] {
68
+ if (!Array.isArray(vars) || vars.length === 0) {
69
+ throw new ScopedKeyStorageError(400, "vars array required");
70
+ }
71
+
72
+ const out: ScopedKeyValue[] = [];
73
+ for (const entry of vars) {
74
+ if (!entry || typeof entry !== "object") {
75
+ throw new ScopedKeyStorageError(400, "Each var must be an object");
76
+ }
77
+ const { key, value } = entry as { key?: unknown; value?: unknown };
78
+ const normalizedKey = typeof key === "string" ? key.trim() : "";
79
+ if (!normalizedKey || !KEY_NAME_RE.test(normalizedKey)) {
80
+ throw new ScopedKeyStorageError(
81
+ 400,
82
+ normalizedKey
83
+ ? `Invalid key name "${normalizedKey}"`
84
+ : "Each var requires a key",
85
+ );
86
+ }
87
+ const normalizedValue = typeof value === "string" ? value.trim() : "";
88
+ if (!normalizedValue) {
89
+ throw new ScopedKeyStorageError(
90
+ 400,
91
+ `Value for ${normalizedKey} must be non-empty`,
92
+ );
93
+ }
94
+ out.push({ key: normalizedKey, value: normalizedValue });
95
+ }
96
+ return out;
97
+ }
98
+
99
+ function resolveTargetScope(
100
+ secret: RegisteredSecret | undefined,
101
+ requestedScope: ScopedKeySaveRequestScope,
102
+ ): SecretScope {
103
+ if (secret?.kind === "api-key") return secret.scope;
104
+ if (requestedScope === "org") return "org";
105
+ if (requestedScope === "workspace" || requestedScope === "app") {
106
+ return "workspace";
107
+ }
108
+ return "user";
109
+ }
110
+
111
+ async function resolveScopeId(
112
+ event: H3Event,
113
+ scope: SecretScope,
114
+ ): Promise<{
115
+ scopeId: string;
116
+ orgRole: string | null;
117
+ orgId: string | null;
118
+ }> {
119
+ const session = await getSession(event).catch(() => null);
120
+ const email = session?.email ?? null;
121
+
122
+ if (scope === "user") {
123
+ if (!email) {
124
+ throw new ScopedKeyStorageError(401, "Sign in to save keys");
125
+ }
126
+ return { scopeId: email, orgRole: null, orgId: null };
127
+ }
128
+
129
+ const orgCtx = await getOrgContext(event).catch(() => null);
130
+ const orgId = orgCtx?.orgId ?? null;
131
+ const orgRole = orgCtx?.role ?? null;
132
+
133
+ if (scope === "org") {
134
+ if (!orgId) {
135
+ throw new ScopedKeyStorageError(401, "No active organization");
136
+ }
137
+ return { scopeId: orgId, orgRole, orgId };
138
+ }
139
+
140
+ if (orgId) {
141
+ return { scopeId: orgId, orgRole, orgId };
142
+ }
143
+ if (!email) {
144
+ throw new ScopedKeyStorageError(401, "Sign in to save workspace keys");
145
+ }
146
+ return { scopeId: `solo:${email}`, orgRole: null, orgId: null };
147
+ }
148
+
149
+ function assertCanMutateScope(
150
+ scope: SecretScope,
151
+ scopeId: string,
152
+ orgRole: string | null,
153
+ ): void {
154
+ if (scope === "user") return;
155
+ if (scope === "workspace" && scopeId.startsWith("solo:")) return;
156
+ if (orgRole === "owner" || orgRole === "admin") return;
157
+ throw new ScopedKeyStorageError(
158
+ 403,
159
+ scope === "org"
160
+ ? "Only organization owners and admins can set org-scoped keys"
161
+ : "Only organization owners and admins can set workspace-scoped keys",
162
+ );
163
+ }
164
+
165
+ async function validateRegisteredSecret(
166
+ secret: RegisteredSecret | undefined,
167
+ value: string,
168
+ ): Promise<void> {
169
+ if (!secret) return;
170
+ if (secret.kind === "oauth") {
171
+ throw new ScopedKeyStorageError(
172
+ 400,
173
+ `"${secret.key}" is an OAuth-kind secret and must be connected via OAuth`,
174
+ );
175
+ }
176
+ if (!secret.validator) return;
177
+ try {
178
+ const result = await secret.validator(value);
179
+ const ok = typeof result === "boolean" ? result : result?.ok === true;
180
+ if (ok) return;
181
+ const error =
182
+ typeof result === "object" && result && result.error
183
+ ? String(result.error)
184
+ : "Validator rejected the value";
185
+ throw new ScopedKeyStorageError(400, redactSecretFromMessage(error, value));
186
+ } catch (err) {
187
+ if (err instanceof ScopedKeyStorageError) throw err;
188
+ const message =
189
+ err instanceof Error
190
+ ? `Validator threw: ${err.message}`
191
+ : "Validator threw";
192
+ throw new ScopedKeyStorageError(
193
+ 400,
194
+ redactSecretFromMessage(message, value),
195
+ );
196
+ }
197
+ }
198
+
199
+ export async function saveKeyValuesToScopedSecrets(
200
+ event: H3Event,
201
+ vars: unknown,
202
+ requestedScope?: ScopedKeySaveRequestScope,
203
+ ): Promise<{ saved: string[]; rows: ScopedKeySaveRow[] }> {
204
+ const normalized = normalizeKeyValueVars(vars);
205
+ const rows: ScopedKeySaveRow[] = [];
206
+
207
+ for (const entry of normalized) {
208
+ const secret = getRequiredSecret(entry.key);
209
+ const scope = resolveTargetScope(secret, requestedScope);
210
+ const { scopeId, orgRole } = await resolveScopeId(event, scope);
211
+ assertCanMutateScope(scope, scopeId, orgRole);
212
+ await validateRegisteredSecret(secret, entry.value);
213
+ await writeAppSecret({
214
+ key: entry.key,
215
+ value: entry.value,
216
+ scope,
217
+ scopeId,
218
+ });
219
+ rows.push({ key: entry.key, scope, scopeId });
220
+ }
221
+
222
+ return { saved: rows.map((row) => row.key), rows };
223
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Isomorphic Agent Env Utility
2
+ * Isomorphic Agent Key Utility
3
3
  *
4
4
  * Works in both browser and Node.js contexts:
5
5
  * - Browser: sends via postMessage to the parent window
@@ -18,7 +18,7 @@ const isBrowser =
18
18
  typeof window !== "undefined" && typeof window.postMessage === "function";
19
19
 
20
20
  /**
21
- * Send env vars to the host (Builder cloud or Claude frame).
21
+ * Send key/value settings to the host for scoped secret persistence.
22
22
  * Automatically detects environment (browser vs Node.js) and uses the right transport.
23
23
  */
24
24
  function setVars(vars: EnvVar[]): void {
@@ -41,6 +41,6 @@ function setVars(vars: EnvVar[]): void {
41
41
  }
42
42
 
43
43
  export const agentEnv = {
44
- /** Send environment variables to the host for persistence */
44
+ /** Send key/value settings to the host for scoped secret persistence. */
45
45
  setVars,
46
46
  };
@@ -262,7 +262,7 @@ export default defineAction({
262
262
  if (
263
263
  args.notify !== false &&
264
264
  args.principalType === "user" &&
265
- isEmailConfigured() &&
265
+ (await isEmailConfigured()) &&
266
266
  !isSyntheticQaEmail(principalId)
267
267
  ) {
268
268
  try {