@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/docs/content/file-uploads.md +11 -4
- package/corpus/core/docs/content/locales/ar-SA/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/de-DE/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/es-ES/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/fr-FR/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/hi-IN/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/ja-JP/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/ko-KR/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/pt-BR/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/zh-CN/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/zh-TW/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/template-clips.md +14 -5
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/engine/builder-engine.ts +8 -3
- package/corpus/core/src/cli/mcp.ts +11 -6
- package/corpus/core/src/client/AssistantChat.tsx +6 -2
- package/corpus/core/src/client/chat/markdown-renderer.tsx +1 -0
- package/corpus/core/src/client/i18n.tsx +77 -65
- package/corpus/core/src/client/index.ts +7 -1
- package/corpus/core/src/client/org/OrgSwitcher.tsx +5 -5
- package/corpus/core/src/client/org/TeamPage.tsx +11 -3
- package/corpus/core/src/client/settings/SettingsTabsPage.tsx +216 -0
- package/corpus/core/src/client/settings/index.ts +5 -0
- package/corpus/core/src/file-upload/index.ts +1 -0
- package/corpus/core/src/file-upload/registry.ts +22 -6
- package/corpus/core/src/file-upload/types.ts +6 -1
- package/corpus/core/src/integrations/adapters/email.ts +26 -18
- package/corpus/core/src/integrations/adapters/slack.ts +8 -7
- package/corpus/core/src/integrations/adapters/telegram.ts +9 -11
- package/corpus/core/src/integrations/adapters/whatsapp.ts +12 -9
- package/corpus/core/src/integrations/plugin.ts +79 -13
- package/corpus/core/src/onboarding/default-steps.ts +6 -3
- package/corpus/core/src/onboarding/types.ts +1 -1
- package/corpus/core/src/org/handlers.ts +1 -1
- package/corpus/core/src/server/agent-chat-plugin.ts +4 -7
- package/corpus/core/src/server/better-auth-instance.ts +19 -62
- package/corpus/core/src/server/core-routes-plugin.ts +93 -180
- package/corpus/core/src/server/create-server.ts +55 -116
- package/corpus/core/src/server/email-actions.ts +3 -6
- package/corpus/core/src/server/email.ts +52 -17
- package/corpus/core/src/server/index.ts +2 -1
- package/corpus/core/src/server/scoped-key-storage.ts +223 -0
- package/corpus/core/src/shared/agent-env.ts +3 -3
- package/corpus/core/src/sharing/actions/share-resource.ts +1 -1
- package/corpus/core/src/styles/agent-native.css +47 -3
- package/corpus/core/src/templates/workspace-root/AGENTS.md +3 -2
- package/corpus/core/src/templates/workspace-root/README.md +1 -1
- package/corpus/core/src/templates/workspace-root/scripts/repair-workspace-org.ts +29 -90
- package/corpus/core/src/terminal/pty-server.ts +3 -38
- package/corpus/core/src/triggers/dispatcher.ts +1 -2
- package/corpus/templates/.retired/issues/package.json +7 -0
- package/corpus/templates/.retired/meeting-notes/package.json +7 -0
- package/corpus/templates/.retired/recruiting/package.json +7 -0
- package/corpus/templates/.retired/scheduling/package.json +7 -0
- package/corpus/templates/.retired/voice/package.json +7 -0
- package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -18
- package/corpus/templates/analytics/app/global.css +15 -0
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +7 -1
- package/corpus/templates/analytics/app/i18n-data.ts +71 -2
- package/corpus/templates/analytics/app/pages/Ask.tsx +1 -1
- package/corpus/templates/analytics/app/pages/Settings.tsx +120 -73
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +0 -11
- package/corpus/templates/analytics/app/routes/catalog.tsx +33 -176
- package/corpus/templates/analytics/app/routes/team.tsx +3 -2
- package/corpus/templates/analytics/changelog/2026-06-26-dashboard-templates-are-tucked-into-settings-and-show-simple.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-the-ask-tab-uses-a-softer-dark-gray-canvas-while-sidebar-cha.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-the-sessions-page-shows-a-single-page-title-instead-of-repea.md +6 -0
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +37 -28
- package/corpus/templates/assets/app/routes/settings.tsx +65 -43
- package/corpus/templates/assets/app/routes/team.tsx +2 -10
- package/corpus/templates/brain/app/routes/settings.tsx +390 -358
- package/corpus/templates/brain/app/routes/team.tsx +2 -8
- package/corpus/templates/calendar/actions/db-connect.ts +5 -29
- package/corpus/templates/calendar/app/components/CloudUpgrade.tsx +8 -51
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +2 -1
- package/corpus/templates/calendar/app/components/calendar/GoogleSetupWizard.tsx +4 -2
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +0 -2
- package/corpus/templates/calendar/app/pages/Settings.tsx +304 -271
- package/corpus/templates/calendar/app/routes/_app.team.tsx +2 -17
- package/corpus/templates/calendar/server/handlers/google-auth.ts +22 -9
- package/corpus/templates/calendar/server/lib/booking-emails.ts +1 -1
- package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +1 -1
- package/corpus/templates/calendar/server/lib/google-calendar.ts +48 -28
- package/corpus/templates/chat/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/chat/app/hooks/use-navigation-state.ts +4 -2
- package/corpus/templates/chat/app/routes/settings.tsx +54 -54
- package/corpus/templates/chat/app/routes/team.tsx +2 -10
- package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +7 -2
- package/corpus/templates/clips/.agents/skills/recording/SKILL.md +1 -1
- package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +7 -0
- package/corpus/templates/clips/AGENTS.md +8 -0
- package/corpus/templates/clips/actions/invite-member.ts +1 -1
- package/corpus/templates/clips/actions/request-transcript.ts +1 -1
- package/corpus/templates/clips/app/components/library/library-layout.tsx +325 -333
- package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +80 -40
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +2 -0
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +629 -643
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +263 -158
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +10 -2
- package/corpus/templates/clips/changelog/2026-06-26-long-desktop-recordings-are-checkpointed-so-macos-finalizati.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-non-mac-desktop-recordings-wait-for-upload-transcription.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-recording-details-button-stays-at-the-far-right-on-small-screen.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-s3-compatible-storage-keys-now-save-securely-from-clips-sett.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-shared-clip-links-guide-agents-through-processing-transcripts.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-the-agent-sidebar-now-slides-in-smoothly-without-duplicate-e.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-the-clip-details-sidebar-now-collapses-into-a-slide-out-pane.md +6 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +55 -3
- package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +217 -5
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +176 -5
- package/corpus/templates/clips/server/lib/public-agent-context.ts +31 -0
- package/corpus/templates/clips/server/lib/s3-upload-provider.ts +60 -14
- package/corpus/templates/clips/server/lib/video-storage.ts +37 -9
- package/corpus/templates/clips/server/routes/api/agent-transcript.json.get.ts +5 -0
- package/corpus/templates/content/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +1 -0
- package/corpus/templates/content/app/routes/_app.settings.tsx +54 -55
- package/corpus/templates/content/app/routes/_app.team.tsx +2 -28
- package/corpus/templates/content/server/lib/notion.ts +43 -11
- package/corpus/templates/content/server/routes/api/notion/auth-url.get.ts +2 -2
- package/corpus/templates/content/server/routes/api/notion/status.get.ts +3 -3
- package/corpus/templates/design/app/routes/settings.tsx +60 -38
- package/corpus/templates/design/app/routes/team.tsx +2 -10
- package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +2 -2
- package/corpus/templates/dispatch/app/routes/settings.tsx +70 -55
- package/corpus/templates/dispatch/app/routes/team.tsx +5 -1
- package/corpus/templates/forms/actions/db-connect.ts +7 -36
- package/corpus/templates/forms/actions/navigate.ts +2 -2
- package/corpus/templates/forms/app/components/CloudUpgrade.tsx +8 -47
- package/corpus/templates/forms/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/forms/app/components/layout/Layout.tsx +42 -25
- package/corpus/templates/forms/app/components/layout/Sidebar.tsx +35 -76
- package/corpus/templates/forms/app/global.css +9 -25
- package/corpus/templates/forms/app/hooks/use-navigation-state.ts +4 -4
- package/corpus/templates/forms/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/forms/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/forms/app/i18n/en-US.ts +1 -0
- package/corpus/templates/forms/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/forms/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/forms/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/forms/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/forms/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/forms/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/forms/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/forms/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/forms/app/lib/route-prewarm.ts +9 -8
- package/corpus/templates/forms/app/pages/AskPage.tsx +70 -0
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +74 -81
- package/corpus/templates/forms/app/root.tsx +1 -1
- package/corpus/templates/forms/app/routes/_app.ask.tsx +14 -0
- package/corpus/templates/forms/app/routes/_app.settings.tsx +54 -54
- package/corpus/templates/forms/app/routes/_app.team.tsx +2 -9
- package/corpus/templates/forms/app/routes/_index.tsx +21 -130
- package/corpus/templates/forms/changelog/2026-06-26-ask-forms-opens-as-a-standard-app-tab-with-a-simpler-sidebar.md +6 -0
- package/corpus/templates/forms/changelog/2026-06-26-forms-are-shown-in-a-tighter-row-list-with-a-denser-sidebar.md +6 -0
- package/corpus/templates/forms/shared/navigation.ts +4 -2
- package/corpus/templates/macros/app/routes/settings.tsx +54 -54
- package/corpus/templates/macros/app/routes/team.tsx +2 -10
- package/corpus/templates/mail/actions/helpers.ts +2 -2
- package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +2 -1
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +5 -5
- package/corpus/templates/mail/app/routes/team.tsx +3 -2
- package/corpus/templates/mail/server/handlers/emails.ts +3 -2
- package/corpus/templates/mail/server/handlers/google-auth.ts +19 -12
- package/corpus/templates/mail/server/lib/automation-engine.ts +3 -2
- package/corpus/templates/mail/server/lib/email-state.ts +2 -3
- package/corpus/templates/mail/server/lib/google-auth.ts +28 -11
- package/corpus/templates/mail/server/lib/jobs.ts +3 -2
- package/corpus/templates/mail/server/onboarding.ts +9 -6
- package/corpus/templates/mail/server/routes/api/attachments.get.ts +3 -3
- package/corpus/templates/plan/actions/request-plan-access.ts +1 -1
- package/corpus/templates/plan/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +56 -4
- package/corpus/templates/plan/app/global.css +8 -3
- package/corpus/templates/plan/app/hooks/use-navigation-state.ts +4 -2
- package/corpus/templates/plan/app/lib/route-prewarm.ts +0 -4
- package/corpus/templates/plan/app/pages/PlansPage.tsx +1 -1
- package/corpus/templates/plan/app/routes/settings.tsx +75 -73
- package/corpus/templates/plan/app/routes/team.tsx +2 -8
- package/corpus/templates/plan/changelog/2026-06-26-canvas-grid-panning-now-stays-smooth-and-fills-the-workspace.md +6 -0
- package/corpus/templates/plan/server/lib/comment-notifications.ts +1 -1
- package/corpus/templates/slides/actions/check-image-gen.ts +2 -1
- package/corpus/templates/slides/actions/db-connect.ts +7 -45
- package/corpus/templates/slides/actions/edit-image.ts +6 -3
- package/corpus/templates/slides/actions/generate-image-api.ts +1 -1
- package/corpus/templates/slides/actions/generate-image.ts +2 -2
- package/corpus/templates/slides/actions/generate-slides-ai.ts +5 -3
- package/corpus/templates/slides/actions/image-gen-status.ts +4 -3
- package/corpus/templates/slides/actions/image-search.ts +6 -3
- package/corpus/templates/slides/actions/search-images.ts +6 -3
- package/corpus/templates/slides/app/components/CloudUpgrade.tsx +8 -51
- package/corpus/templates/slides/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/slides/app/components/layout/Sidebar.tsx +0 -2
- package/corpus/templates/slides/app/routes/settings.tsx +54 -54
- package/corpus/templates/slides/app/routes/team.tsx +2 -12
- package/corpus/templates/slides/server/handlers/google-docs-auth.ts +9 -9
- package/corpus/templates/slides/server/handlers/image-gen.ts +5 -1
- package/corpus/templates/slides/server/handlers/image-providers/gemini.ts +9 -1
- package/corpus/templates/slides/server/handlers/image-providers/index.ts +18 -8
- package/corpus/templates/slides/server/handlers/image-providers/openai.ts +10 -4
- package/corpus/templates/slides/server/handlers/image-providers/types.ts +1 -0
- package/corpus/templates/slides/server/lib/google-docs-oauth.ts +59 -34
- package/corpus/templates/slides/server/register-secrets.ts +60 -0
- package/corpus/templates/videos/actions/db-connect.ts +7 -45
- package/corpus/templates/videos/app/components/CloudUpgrade.tsx +8 -51
- package/corpus/templates/videos/app/components/Sidebar.tsx +1 -9
- package/corpus/templates/videos/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/videos/app/components/layout/NavSidebar.tsx +0 -3
- package/corpus/templates/videos/app/routes/settings.tsx +54 -54
- package/corpus/templates/videos/app/routes/team.tsx +2 -8
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/engine/builder-engine.js +8 -3
- package/dist/agent/engine/builder-engine.js.map +1 -1
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +3 -4
- package/dist/cli/mcp.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +6 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
- package/dist/client/chat/markdown-renderer.js +1 -0
- package/dist/client/chat/markdown-renderer.js.map +1 -1
- package/dist/client/i18n.d.ts.map +1 -1
- package/dist/client/i18n.js +20 -9
- package/dist/client/i18n.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/org/OrgSwitcher.d.ts +3 -3
- package/dist/client/org/OrgSwitcher.js +2 -2
- package/dist/client/org/OrgSwitcher.js.map +1 -1
- package/dist/client/org/TeamPage.d.ts +6 -1
- package/dist/client/org/TeamPage.d.ts.map +1 -1
- package/dist/client/org/TeamPage.js +2 -2
- package/dist/client/org/TeamPage.js.map +1 -1
- package/dist/client/settings/SettingsTabsPage.d.ts +27 -0
- package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -0
- package/dist/client/settings/SettingsTabsPage.js +108 -0
- package/dist/client/settings/SettingsTabsPage.js.map +1 -0
- package/dist/client/settings/index.d.ts +1 -0
- package/dist/client/settings/index.d.ts.map +1 -1
- package/dist/client/settings/index.js +1 -0
- package/dist/client/settings/index.js.map +1 -1
- package/dist/file-upload/index.d.ts +1 -1
- package/dist/file-upload/index.d.ts.map +1 -1
- package/dist/file-upload/index.js +1 -1
- package/dist/file-upload/index.js.map +1 -1
- package/dist/file-upload/registry.d.ts +1 -0
- package/dist/file-upload/registry.d.ts.map +1 -1
- package/dist/file-upload/registry.js +24 -6
- package/dist/file-upload/registry.js.map +1 -1
- package/dist/file-upload/types.d.ts +6 -1
- package/dist/file-upload/types.d.ts.map +1 -1
- package/dist/file-upload/types.js.map +1 -1
- package/dist/integrations/adapters/email.d.ts.map +1 -1
- package/dist/integrations/adapters/email.js +18 -18
- package/dist/integrations/adapters/email.js.map +1 -1
- package/dist/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +8 -7
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/adapters/telegram.d.ts.map +1 -1
- package/dist/integrations/adapters/telegram.js +9 -9
- package/dist/integrations/adapters/telegram.js.map +1 -1
- package/dist/integrations/adapters/whatsapp.d.ts.map +1 -1
- package/dist/integrations/adapters/whatsapp.js +10 -9
- package/dist/integrations/adapters/whatsapp.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +47 -13
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +6 -6
- package/dist/onboarding/default-steps.d.ts.map +1 -1
- package/dist/onboarding/default-steps.js +6 -5
- package/dist/onboarding/default-steps.js.map +1 -1
- package/dist/onboarding/types.d.ts +1 -1
- package/dist/onboarding/types.d.ts.map +1 -1
- package/dist/onboarding/types.js.map +1 -1
- package/dist/org/handlers.js +1 -1
- package/dist/org/handlers.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +4 -6
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +2 -2
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +17 -56
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +67 -153
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/create-server.d.ts +3 -9
- package/dist/server/create-server.d.ts.map +1 -1
- package/dist/server/create-server.js +37 -91
- package/dist/server/create-server.js.map +1 -1
- package/dist/server/email-actions.d.ts +2 -3
- package/dist/server/email-actions.d.ts.map +1 -1
- package/dist/server/email-actions.js +3 -6
- package/dist/server/email-actions.js.map +1 -1
- package/dist/server/email.d.ts +3 -3
- package/dist/server/email.d.ts.map +1 -1
- package/dist/server/email.js +40 -19
- package/dist/server/email.js.map +1 -1
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/scoped-key-storage.d.ts +22 -0
- package/dist/server/scoped-key-storage.d.ts.map +1 -0
- package/dist/server/scoped-key-storage.js +152 -0
- package/dist/server/scoped-key-storage.js.map +1 -0
- package/dist/shared/agent-env.d.ts +3 -3
- package/dist/shared/agent-env.d.ts.map +1 -1
- package/dist/shared/agent-env.js +3 -3
- package/dist/shared/agent-env.js.map +1 -1
- package/dist/sharing/actions/share-resource.js +1 -1
- package/dist/sharing/actions/share-resource.js.map +1 -1
- package/dist/styles/agent-native.css +47 -3
- package/dist/templates/workspace-root/AGENTS.md +3 -2
- package/dist/templates/workspace-root/README.md +1 -1
- package/dist/templates/workspace-root/scripts/repair-workspace-org.ts +29 -90
- package/dist/terminal/pty-server.d.ts.map +1 -1
- package/dist/terminal/pty-server.js +3 -36
- package/dist/terminal/pty-server.js.map +1 -1
- package/dist/triggers/dispatcher.js +1 -1
- package/dist/triggers/dispatcher.js.map +1 -1
- package/docs/content/file-uploads.md +11 -4
- package/docs/content/locales/ar-SA/multi-app-workspace.md +3 -3
- package/docs/content/locales/de-DE/multi-app-workspace.md +3 -3
- package/docs/content/locales/es-ES/multi-app-workspace.md +3 -3
- package/docs/content/locales/fr-FR/multi-app-workspace.md +3 -3
- package/docs/content/locales/hi-IN/multi-app-workspace.md +3 -3
- package/docs/content/locales/ja-JP/multi-app-workspace.md +3 -3
- package/docs/content/locales/ko-KR/multi-app-workspace.md +3 -3
- package/docs/content/locales/pt-BR/multi-app-workspace.md +3 -3
- package/docs/content/locales/zh-CN/multi-app-workspace.md +3 -3
- package/docs/content/locales/zh-TW/multi-app-workspace.md +3 -3
- package/docs/content/multi-app-workspace.md +3 -3
- package/docs/content/template-clips.md +14 -5
- package/package.json +1 -1
- package/src/templates/workspace-root/AGENTS.md +3 -2
- package/src/templates/workspace-root/README.md +1 -1
- package/src/templates/workspace-root/scripts/repair-workspace-org.ts +29 -90
- package/corpus/core/src/server/env-var-writes.ts +0 -16
- package/corpus/templates/analytics/app/pages/Team.tsx +0 -9
- package/corpus/templates/calendar/app/pages/Team.tsx +0 -12
- package/corpus/templates/mail/app/pages/Team.tsx +0 -17
- package/dist/server/env-var-writes.d.ts +0 -10
- package/dist/server/env-var-writes.d.ts.map +0 -1
- package/dist/server/env-var-writes.js +0 -18
- 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 {
|
|
21
|
+
import { getSession } from "./auth.js";
|
|
24
22
|
import {
|
|
25
23
|
getAllowedCorsOrigin,
|
|
26
24
|
readCorsAllowedOrigins,
|
|
27
25
|
} from "./cors-origins.js";
|
|
28
|
-
import {
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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 (
|
|
247
|
+
if (unsupportedKeys.length > 0) {
|
|
309
248
|
setResponseStatus(event, 400);
|
|
310
|
-
return {
|
|
249
|
+
return {
|
|
250
|
+
error: `Unsupported env key${unsupportedKeys.length === 1 ? "" : "s"}: ${unsupportedKeys.join(", ")}`,
|
|
251
|
+
};
|
|
311
252
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
6
|
-
*
|
|
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 {
|
|
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
|
|
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
|
-
|
|
37
|
-
|
|
38
|
+
interface EmailTransportConfig {
|
|
39
|
+
provider: EmailProvider;
|
|
40
|
+
resendApiKey?: string;
|
|
41
|
+
sendgridApiKey?: string;
|
|
42
|
+
from?: string;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
47
|
-
|
|
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 —
|
|
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
|
|
61
|
-
const
|
|
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 ${
|
|
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 ${
|
|
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.
|
|
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
|
-
`
|
|
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
|
|
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
|
|
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
|
|
44
|
+
/** Send key/value settings to the host for scoped secret persistence. */
|
|
45
45
|
setVars,
|
|
46
46
|
};
|