@agent-native/core 0.134.1 → 0.135.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +3 -3
- package/corpus/core/CHANGELOG.md +113 -0
- package/corpus/core/package.json +3 -2
- package/corpus/core/src/automations/service.ts +41 -4
- package/corpus/core/src/cli/mcp-config-writers.ts +27 -21
- package/corpus/core/src/client/agent-page/AgentJobsTab.tsx +187 -85
- package/corpus/core/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
- package/corpus/core/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
- package/corpus/core/src/client/agent-page/TimezoneSelect.tsx +241 -0
- package/corpus/core/src/client/agent-page/use-jobs.ts +50 -10
- package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -0
- package/corpus/core/src/client/settings/AccountSettingsCard.tsx +35 -31
- package/corpus/core/src/client/settings/SchedulingTimezoneField.tsx +71 -0
- package/corpus/core/src/client/settings/SettingsRow.tsx +113 -0
- package/corpus/core/src/client/settings/agent-settings-search.ts +2 -1
- package/corpus/core/src/client/settings/index.ts +6 -0
- package/corpus/core/src/integrations/identity.ts +2 -12
- package/corpus/core/src/jobs/actions/list-automation-runs.ts +33 -0
- package/corpus/core/src/jobs/actions/list-recurring-jobs.ts +27 -6
- package/corpus/core/src/jobs/actions/manage-recurring-job.ts +41 -9
- package/corpus/core/src/jobs/background-automation-runner.ts +88 -0
- package/corpus/core/src/jobs/cron.ts +52 -6
- package/corpus/core/src/jobs/frontmatter.ts +20 -0
- package/corpus/core/src/jobs/run-history.ts +242 -0
- package/corpus/core/src/jobs/scheduler.ts +73 -18
- package/corpus/core/src/jobs/tools.ts +54 -6
- package/corpus/core/src/localization/actions/get-localization-preference.ts +2 -2
- package/corpus/core/src/localization/actions/set-localization-preference.ts +49 -11
- package/corpus/core/src/localization/default-messages.ts +27 -0
- package/corpus/core/src/localization/shared.ts +39 -6
- package/corpus/core/src/localization/user-timezone.ts +36 -0
- package/corpus/core/src/org/app-roles-handlers.ts +1 -9
- package/corpus/core/src/org/index.ts +2 -0
- package/corpus/core/src/org/membership.ts +18 -0
- package/corpus/core/src/review/actions/create-review-comment.ts +4 -1
- package/corpus/core/src/review/actions/reply-review-comment.ts +4 -1
- package/corpus/core/src/review/index.ts +5 -0
- package/corpus/core/src/review/notifications.ts +163 -0
- package/corpus/core/src/review/types.ts +7 -0
- package/corpus/core/src/server/action-discovery.ts +4 -0
- package/corpus/core/src/server/activity-notifications.ts +174 -0
- package/corpus/core/src/server/app-name.ts +31 -0
- package/corpus/core/src/server/better-auth-instance.ts +4 -4
- package/corpus/core/src/server/email-templates.ts +70 -14
- package/corpus/core/src/server/email.ts +66 -3
- package/corpus/core/src/server/index.ts +10 -0
- package/corpus/core/src/sharing/access.ts +44 -2
- package/corpus/core/src/sharing/index.ts +5 -0
- package/corpus/core/src/sharing/recipients.ts +67 -0
- package/corpus/core/src/triggers/actions/list-automations.ts +27 -8
- package/corpus/core/src/triggers/actions/manage-automation.ts +17 -6
- package/corpus/core/src/triggers/actions.ts +16 -4
- package/corpus/core/src/triggers/dispatcher.ts +44 -30
- package/corpus/core/src/triggers/routes.ts +19 -6
- package/corpus/core/src/vitest-config.ts +68 -0
- package/corpus/core/vitest.config.ts +5 -0
- package/corpus/templates/analytics/app/pages/Settings.tsx +37 -53
- package/corpus/templates/analytics/changelog/2026-08-01-new-error-alerts-now-reach-you-by-email-and-a-scheduled-dash.md +6 -0
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +43 -0
- package/corpus/templates/analytics/server/lib/error-capture.ts +5 -1
- package/corpus/templates/analytics/vitest.config.ts +16 -12
- package/corpus/templates/assets/actions/refresh-generation-run.ts +3 -0
- package/corpus/templates/assets/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/assets/app/hooks/use-assets-prefs.ts +55 -0
- package/corpus/templates/assets/app/i18n-data.ts +30 -0
- package/corpus/templates/assets/app/routes/settings.tsx +58 -55
- package/corpus/templates/assets/changelog/2026-08-01-get-an-email-when-a-generation-you-started-finishes-or-fails.md +6 -0
- package/corpus/templates/assets/server/lib/generation-run-notifications.ts +89 -0
- package/corpus/templates/assets/server/lib/video-runs.ts +8 -1
- package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.get.ts +17 -0
- package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.put.ts +36 -0
- package/corpus/templates/assets/shared/assets-user-prefs.ts +10 -0
- package/corpus/templates/assets/vitest.config.ts +16 -12
- package/corpus/templates/brain/app/routes/settings.tsx +15 -15
- package/corpus/templates/brain/vitest.config.ts +16 -12
- package/corpus/templates/calendar/app/pages/Settings.tsx +32 -40
- package/corpus/templates/calendar/changelog/2026-08-01-language-and-appearance-now-sit-together-in-one-preferences-.md +6 -0
- package/corpus/templates/calendar/vitest.config.ts +16 -12
- package/corpus/templates/chat/app/routes/settings.tsx +14 -22
- package/corpus/templates/chat/vitest.config.ts +9 -0
- package/corpus/templates/clips/.agents/skills/video-editing/SKILL.md +31 -0
- package/corpus/templates/clips/AGENTS.md +1 -0
- package/corpus/templates/clips/actions/add-comment.ts +12 -1
- package/corpus/templates/clips/actions/create-meeting.ts +2 -2
- package/corpus/templates/clips/actions/create-recording.ts +2 -2
- package/corpus/templates/clips/actions/finalize-recording.ts +42 -6
- package/corpus/templates/clips/actions/generate-filmstrip.ts +154 -0
- package/corpus/templates/clips/actions/get-recording-player-data.ts +6 -0
- package/corpus/templates/clips/actions/import-loom-recording.ts +2 -2
- package/corpus/templates/clips/actions/lib/ensure-recording-filmstrip.ts +258 -0
- package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +11 -3
- package/corpus/templates/clips/actions/react-to-comment.ts +13 -0
- package/corpus/templates/clips/actions/react-to-recording.ts +10 -1
- package/corpus/templates/clips/actions/reply-to-comment.ts +12 -1
- package/corpus/templates/clips/actions/stitch-recordings.ts +2 -2
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +152 -23
- package/corpus/templates/clips/app/components/editor/waveform.tsx +237 -75
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +23 -1
- package/corpus/templates/clips/app/components/settings/ai-setup-section.tsx +441 -0
- package/corpus/templates/clips/app/components/settings/slack-section.tsx +329 -0
- package/corpus/templates/clips/app/components/settings/types.ts +14 -0
- package/corpus/templates/clips/app/components/settings/video-storage-section.tsx +443 -0
- package/corpus/templates/clips/app/hooks/use-secret-status.ts +69 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +9 -1
- package/corpus/templates/clips/app/lib/video-filmstrip.ts +262 -0
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +165 -1351
- package/corpus/templates/clips/changelog/2026-07-27-saved-clips-now-resume-from-the-last-uploaded-byte-after-an-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-31-editor-timeline-now-shows-video-frames-behind-the-waveform-g.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-31-email-notifications-now-go-out-when-someone-comments-on-or-r.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-31-you-can-now-set-a-default-visibility-for-new-recordings-in-s.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-01-email-notifications-for-comments-and-reactions-now-actually-.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-03-the-desktop-app-now-opens-directly-beneath-its-tray-icon-on-.md +6 -0
- package/corpus/templates/clips/chrome-extension/package.json +1 -0
- package/corpus/templates/clips/chrome-extension/vitest.config.ts +9 -0
- package/corpus/templates/clips/desktop/package.json +1 -0
- package/corpus/templates/clips/desktop/src/app.tsx +17 -1
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +331 -36
- package/corpus/templates/clips/desktop/src/lib/upload-recovery.ts +129 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +815 -15
- package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +61 -11
- package/corpus/templates/clips/desktop/vitest.config.ts +9 -0
- package/corpus/templates/clips/server/db/schema.ts +17 -0
- package/corpus/templates/clips/server/jobs/media-verification.ts +4 -0
- package/corpus/templates/clips/server/lib/activity-notifications.ts +185 -0
- package/corpus/templates/clips/server/lib/recording-media-cleanup.ts +2 -0
- package/corpus/templates/clips/server/lib/recording-upload-state.ts +46 -9
- package/corpus/templates/clips/server/lib/recordings.ts +24 -0
- package/corpus/templates/clips/server/lib/resumable-session.ts +10 -4
- package/corpus/templates/clips/server/lib/transactional-email-templates.ts +109 -0
- package/corpus/templates/clips/server/lib/upload-interruption.ts +8 -0
- package/corpus/templates/clips/server/lib/upload-lease.ts +50 -4
- package/corpus/templates/clips/server/lib/video-filmstrip-sprite.ts +207 -0
- package/corpus/templates/clips/server/lib/video-remux.ts +13 -4
- package/corpus/templates/clips/server/plugins/db.ts +16 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +4 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +123 -36
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +159 -29
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +197 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +193 -39
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts +191 -12
- package/corpus/templates/clips/shared/clips-ai-prefs.ts +6 -0
- package/corpus/templates/clips/shared/feature-flags.ts +12 -0
- package/corpus/templates/clips/shared/recording-core.ts +4 -0
- package/corpus/templates/clips/vitest.config.ts +23 -19
- package/corpus/templates/content/actions/_database-utils.ts +113 -4
- package/corpus/templates/content/actions/add-comment.ts +15 -1
- package/corpus/templates/content/actions/get-document.ts +84 -3
- package/corpus/templates/content/actions/list-documents.ts +1 -0
- package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +9 -2
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +34 -15
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +55 -13
- package/corpus/templates/content/app/components/editor/body-hydration.ts +12 -6
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2 -3
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -12
- package/corpus/templates/content/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +16 -27
- package/corpus/templates/content/app/hooks/use-content-prefs.ts +55 -0
- package/corpus/templates/content/app/hooks/use-create-page.ts +3 -6
- package/corpus/templates/content/app/hooks/use-document-properties.ts +9 -16
- package/corpus/templates/content/app/hooks/use-document-versions.ts +3 -3
- package/corpus/templates/content/app/hooks/use-documents.ts +85 -29
- package/corpus/templates/content/app/hooks/use-notion.ts +15 -13
- package/corpus/templates/content/app/i18n-data.ts +72 -1
- package/corpus/templates/content/app/lib/document-query.ts +36 -0
- package/corpus/templates/content/app/routes/_app.settings.tsx +47 -18
- package/corpus/templates/content/changelog/2026-07-30-breadcrumb-menus-now-hide-internal-system-pages.md +6 -0
- package/corpus/templates/content/changelog/2026-08-01-get-an-email-when-someone-comments-on-replies-in-or-mentions.md +6 -0
- package/corpus/templates/content/changelog/2026-08-02-pages-opened-from-a-database-now-keep-that-database-s-fields.md +6 -0
- package/corpus/templates/content/server/lib/comment-notifications.ts +146 -0
- package/corpus/templates/content/server/lib/document-context.ts +11 -6
- package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.get.ts +17 -0
- package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.put.ts +36 -0
- package/corpus/templates/content/shared/api.ts +8 -0
- package/corpus/templates/content/shared/content-user-prefs.ts +11 -0
- package/corpus/templates/content/vitest.config.ts +20 -16
- package/corpus/templates/crm/app/routes/settings.tsx +14 -22
- package/corpus/templates/crm/vitest.config.ts +8 -4
- package/corpus/templates/design/app/routes/settings.tsx +14 -22
- package/corpus/templates/design/changelog/2026-08-01-review-comments-replies-and-mentions-now-send-an-email-to-th.md +6 -0
- package/corpus/templates/design/vitest.config.ts +18 -12
- package/corpus/templates/dispatch/app/routes/settings.tsx +26 -40
- package/corpus/templates/dispatch/vitest.config.ts +11 -7
- package/corpus/templates/factory/app/routes/settings.tsx +14 -22
- package/corpus/templates/factory/vitest.config.ts +9 -0
- package/corpus/templates/forms/app/routes/_app.settings.tsx +14 -22
- package/corpus/templates/forms/vitest.config.ts +27 -23
- package/corpus/templates/macros/app/routes/settings.tsx +14 -22
- package/corpus/templates/macros/vitest.config.ts +16 -12
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +14 -16
- package/corpus/templates/mail/vitest.config.ts +16 -12
- package/corpus/templates/plan/app/routes/settings.tsx +30 -44
- package/corpus/templates/plan/vitest.config.ts +24 -20
- package/corpus/templates/slides/actions/add-slide-comment.ts +12 -1
- package/corpus/templates/slides/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/slides/app/hooks/use-slides-prefs.ts +55 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
- package/corpus/templates/slides/app/routes/settings.tsx +45 -22
- package/corpus/templates/slides/changelog/2026-08-01-get-an-email-when-someone-comments-on-or-replies-in-your-dec.md +6 -0
- package/corpus/templates/slides/server/lib/comment-notifications.ts +182 -0
- package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.get.ts +16 -0
- package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.put.ts +36 -0
- package/corpus/templates/slides/shared/slides-user-prefs.ts +11 -0
- package/corpus/templates/slides/vitest.config.ts +17 -13
- package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +2 -0
- package/corpus/templates/tasks/app/routes/settings.tsx +14 -22
- package/corpus/templates/tasks/vitest.config.ts +17 -13
- package/corpus/toolkit/CHANGELOG.md +9 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/vitest.config.ts +3 -0
- package/dist/automations/service.d.ts +2 -0
- package/dist/automations/service.d.ts.map +1 -1
- package/dist/automations/service.js +29 -4
- package/dist/automations/service.js.map +1 -1
- package/dist/cli/mcp-config-writers.d.ts.map +1 -1
- package/dist/cli/mcp-config-writers.js +24 -23
- package/dist/cli/mcp-config-writers.js.map +1 -1
- package/dist/client/agent-page/AgentJobsTab.d.ts.map +1 -1
- package/dist/client/agent-page/AgentJobsTab.js +86 -29
- package/dist/client/agent-page/AgentJobsTab.js.map +1 -1
- package/dist/client/agent-page/AutomationDetailsDialog.d.ts +21 -0
- package/dist/client/agent-page/AutomationDetailsDialog.d.ts.map +1 -0
- package/dist/client/agent-page/AutomationDetailsDialog.js +33 -0
- package/dist/client/agent-page/AutomationDetailsDialog.js.map +1 -0
- package/dist/client/agent-page/AutomationScheduleDialog.d.ts +15 -0
- package/dist/client/agent-page/AutomationScheduleDialog.d.ts.map +1 -0
- package/dist/client/agent-page/AutomationScheduleDialog.js +46 -0
- package/dist/client/agent-page/AutomationScheduleDialog.js.map +1 -0
- package/dist/client/agent-page/TimezoneSelect.d.ts +17 -0
- package/dist/client/agent-page/TimezoneSelect.d.ts.map +1 -0
- package/dist/client/agent-page/TimezoneSelect.js +107 -0
- package/dist/client/agent-page/TimezoneSelect.js.map +1 -0
- package/dist/client/agent-page/use-jobs.d.ts +20 -7
- package/dist/client/agent-page/use-jobs.d.ts.map +1 -1
- package/dist/client/agent-page/use-jobs.js +24 -4
- package/dist/client/agent-page/use-jobs.js.map +1 -1
- package/dist/client/org/OrgSwitcher.d.ts +8 -1
- package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
- package/dist/client/org/OrgSwitcher.js +8 -2
- package/dist/client/org/OrgSwitcher.js.map +1 -1
- package/dist/client/settings/AccountSettingsCard.d.ts.map +1 -1
- package/dist/client/settings/AccountSettingsCard.js +10 -9
- package/dist/client/settings/AccountSettingsCard.js.map +1 -1
- package/dist/client/settings/SchedulingTimezoneField.d.ts +10 -0
- package/dist/client/settings/SchedulingTimezoneField.d.ts.map +1 -0
- package/dist/client/settings/SchedulingTimezoneField.js +30 -0
- package/dist/client/settings/SchedulingTimezoneField.js.map +1 -0
- package/dist/client/settings/SettingsRow.d.ts +36 -0
- package/dist/client/settings/SettingsRow.d.ts.map +1 -0
- package/dist/client/settings/SettingsRow.js +15 -0
- package/dist/client/settings/SettingsRow.js.map +1 -0
- package/dist/client/settings/agent-settings-search.d.ts.map +1 -1
- package/dist/client/settings/agent-settings-search.js +1 -1
- package/dist/client/settings/agent-settings-search.js.map +1 -1
- 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/actions/upload-image.d.ts +1 -1
- package/dist/integrations/identity.d.ts.map +1 -1
- package/dist/integrations/identity.js +2 -11
- package/dist/integrations/identity.js.map +1 -1
- package/dist/jobs/actions/list-automation-runs.d.ts +8 -0
- package/dist/jobs/actions/list-automation-runs.d.ts.map +1 -0
- package/dist/jobs/actions/list-automation-runs.js +29 -0
- package/dist/jobs/actions/list-automation-runs.js.map +1 -0
- package/dist/jobs/actions/list-recurring-jobs.d.ts +2 -0
- package/dist/jobs/actions/list-recurring-jobs.d.ts.map +1 -1
- package/dist/jobs/actions/list-recurring-jobs.js +20 -7
- package/dist/jobs/actions/list-recurring-jobs.js.map +1 -1
- package/dist/jobs/actions/manage-recurring-job.d.ts +2 -0
- package/dist/jobs/actions/manage-recurring-job.js +30 -9
- package/dist/jobs/actions/manage-recurring-job.js.map +1 -1
- package/dist/jobs/background-automation-runner.d.ts.map +1 -1
- package/dist/jobs/background-automation-runner.js +60 -0
- package/dist/jobs/background-automation-runner.js.map +1 -1
- package/dist/jobs/cron.d.ts +16 -2
- package/dist/jobs/cron.d.ts.map +1 -1
- package/dist/jobs/cron.js +43 -6
- package/dist/jobs/cron.js.map +1 -1
- package/dist/jobs/frontmatter.d.ts +12 -0
- package/dist/jobs/frontmatter.d.ts.map +1 -1
- package/dist/jobs/frontmatter.js +8 -0
- package/dist/jobs/frontmatter.js.map +1 -1
- package/dist/jobs/run-history.d.ts +56 -0
- package/dist/jobs/run-history.d.ts.map +1 -0
- package/dist/jobs/run-history.js +169 -0
- package/dist/jobs/run-history.js.map +1 -0
- package/dist/jobs/scheduler.d.ts.map +1 -1
- package/dist/jobs/scheduler.js +51 -18
- package/dist/jobs/scheduler.js.map +1 -1
- package/dist/jobs/tools.d.ts.map +1 -1
- package/dist/jobs/tools.js +37 -6
- package/dist/jobs/tools.js.map +1 -1
- package/dist/localization/actions/get-localization-preference.d.ts +1 -2
- package/dist/localization/actions/get-localization-preference.d.ts.map +1 -1
- package/dist/localization/actions/get-localization-preference.js.map +1 -1
- package/dist/localization/actions/set-localization-preference.d.ts +3 -3
- package/dist/localization/actions/set-localization-preference.d.ts.map +1 -1
- package/dist/localization/actions/set-localization-preference.js +33 -8
- package/dist/localization/actions/set-localization-preference.js.map +1 -1
- package/dist/localization/default-messages.d.ts +25 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +25 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/localization/shared.d.ts +16 -1
- package/dist/localization/shared.d.ts.map +1 -1
- package/dist/localization/shared.js +22 -4
- package/dist/localization/shared.js.map +1 -1
- package/dist/localization/user-timezone.d.ts +11 -0
- package/dist/localization/user-timezone.d.ts.map +1 -0
- package/dist/localization/user-timezone.js +31 -0
- package/dist/localization/user-timezone.js.map +1 -0
- package/dist/notifications/routes.d.ts +4 -4
- package/dist/observability/routes.d.ts +3 -3
- package/dist/org/app-roles-handlers.d.ts.map +1 -1
- package/dist/org/app-roles-handlers.js +1 -8
- package/dist/org/app-roles-handlers.js.map +1 -1
- package/dist/org/index.d.ts +1 -0
- package/dist/org/index.d.ts.map +1 -1
- package/dist/org/index.js +1 -0
- package/dist/org/index.js.map +1 -1
- package/dist/org/membership.d.ts +6 -0
- package/dist/org/membership.d.ts.map +1 -0
- package/dist/org/membership.js +16 -0
- package/dist/org/membership.js.map +1 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/review/actions/create-review-comment.d.ts +32 -1
- package/dist/review/actions/create-review-comment.d.ts.map +1 -1
- package/dist/review/actions/create-review-comment.js +3 -1
- package/dist/review/actions/create-review-comment.js.map +1 -1
- package/dist/review/actions/reply-review-comment.d.ts +32 -1
- package/dist/review/actions/reply-review-comment.d.ts.map +1 -1
- package/dist/review/actions/reply-review-comment.js +3 -1
- package/dist/review/actions/reply-review-comment.js.map +1 -1
- package/dist/review/index.d.ts +1 -0
- package/dist/review/index.d.ts.map +1 -1
- package/dist/review/index.js +1 -0
- package/dist/review/index.js.map +1 -1
- package/dist/review/notifications.d.ts +24 -0
- package/dist/review/notifications.d.ts.map +1 -0
- package/dist/review/notifications.js +123 -0
- package/dist/review/notifications.js.map +1 -0
- package/dist/review/types.d.ts +5 -0
- package/dist/review/types.d.ts.map +1 -1
- package/dist/review/types.js.map +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +4 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/activity-notifications.d.ts +61 -0
- package/dist/server/activity-notifications.d.ts.map +1 -0
- package/dist/server/activity-notifications.js +103 -0
- package/dist/server/activity-notifications.js.map +1 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/app-name.d.ts +18 -0
- package/dist/server/app-name.d.ts.map +1 -1
- package/dist/server/app-name.js +31 -0
- package/dist/server/app-name.js.map +1 -1
- package/dist/server/better-auth-instance.js +4 -4
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/email-templates.d.ts +12 -0
- package/dist/server/email-templates.d.ts.map +1 -1
- package/dist/server/email-templates.js +57 -14
- package/dist/server/email-templates.js.map +1 -1
- package/dist/server/email.d.ts +12 -0
- package/dist/server/email.d.ts.map +1 -1
- package/dist/server/email.js +52 -5
- package/dist/server/email.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/sharing/access.d.ts.map +1 -1
- package/dist/sharing/access.js +32 -2
- package/dist/sharing/access.js.map +1 -1
- package/dist/sharing/index.d.ts +1 -0
- package/dist/sharing/index.d.ts.map +1 -1
- package/dist/sharing/index.js +1 -0
- package/dist/sharing/index.js.map +1 -1
- package/dist/sharing/recipients.d.ts +31 -0
- package/dist/sharing/recipients.d.ts.map +1 -0
- package/dist/sharing/recipients.js +40 -0
- package/dist/sharing/recipients.js.map +1 -0
- package/dist/templates/chat/app/routes/settings.tsx +14 -22
- package/dist/templates/chat/vitest.config.ts +9 -0
- package/dist/triggers/actions/list-automations.d.ts +2 -0
- package/dist/triggers/actions/list-automations.d.ts.map +1 -1
- package/dist/triggers/actions/list-automations.js +19 -8
- package/dist/triggers/actions/list-automations.js.map +1 -1
- package/dist/triggers/actions/manage-automation.d.ts +6 -0
- package/dist/triggers/actions/manage-automation.js +12 -6
- package/dist/triggers/actions/manage-automation.js.map +1 -1
- package/dist/triggers/actions.d.ts.map +1 -1
- package/dist/triggers/actions.js +15 -4
- package/dist/triggers/actions.js.map +1 -1
- package/dist/triggers/dispatcher.d.ts.map +1 -1
- package/dist/triggers/dispatcher.js +19 -17
- package/dist/triggers/dispatcher.js.map +1 -1
- package/dist/triggers/routes.d.ts.map +1 -1
- package/dist/triggers/routes.js +6 -6
- package/dist/triggers/routes.js.map +1 -1
- package/dist/vitest-config.d.ts +10 -0
- package/dist/vitest-config.d.ts.map +1 -0
- package/dist/vitest-config.js +55 -0
- package/dist/vitest-config.js.map +1 -0
- package/package.json +5 -4
- package/src/automations/service.ts +41 -4
- package/src/cli/mcp-config-writers.ts +27 -21
- package/src/client/agent-page/AgentJobsTab.tsx +187 -85
- package/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
- package/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
- package/src/client/agent-page/TimezoneSelect.tsx +241 -0
- package/src/client/agent-page/use-jobs.ts +50 -10
- package/src/client/org/OrgSwitcher.tsx +26 -0
- package/src/client/settings/AccountSettingsCard.tsx +35 -31
- package/src/client/settings/SchedulingTimezoneField.tsx +71 -0
- package/src/client/settings/SettingsRow.tsx +113 -0
- package/src/client/settings/agent-settings-search.ts +2 -1
- package/src/client/settings/index.ts +6 -0
- package/src/integrations/identity.ts +2 -12
- package/src/jobs/actions/list-automation-runs.ts +33 -0
- package/src/jobs/actions/list-recurring-jobs.ts +27 -6
- package/src/jobs/actions/manage-recurring-job.ts +41 -9
- package/src/jobs/background-automation-runner.ts +88 -0
- package/src/jobs/cron.ts +52 -6
- package/src/jobs/frontmatter.ts +20 -0
- package/src/jobs/run-history.ts +242 -0
- package/src/jobs/scheduler.ts +73 -18
- package/src/jobs/tools.ts +54 -6
- package/src/localization/actions/get-localization-preference.ts +2 -2
- package/src/localization/actions/set-localization-preference.ts +49 -11
- package/src/localization/default-messages.ts +27 -0
- package/src/localization/shared.ts +39 -6
- package/src/localization/user-timezone.ts +36 -0
- package/src/org/app-roles-handlers.ts +1 -9
- package/src/org/index.ts +2 -0
- package/src/org/membership.ts +18 -0
- package/src/review/actions/create-review-comment.ts +4 -1
- package/src/review/actions/reply-review-comment.ts +4 -1
- package/src/review/index.ts +5 -0
- package/src/review/notifications.ts +163 -0
- package/src/review/types.ts +7 -0
- package/src/server/action-discovery.ts +4 -0
- package/src/server/activity-notifications.ts +174 -0
- package/src/server/app-name.ts +31 -0
- package/src/server/better-auth-instance.ts +4 -4
- package/src/server/email-templates.ts +70 -14
- package/src/server/email.ts +66 -3
- package/src/server/index.ts +10 -0
- package/src/sharing/access.ts +44 -2
- package/src/sharing/index.ts +5 -0
- package/src/sharing/recipients.ts +67 -0
- package/src/templates/chat/app/routes/settings.tsx +14 -22
- package/src/templates/chat/vitest.config.ts +9 -0
- package/src/triggers/actions/list-automations.ts +27 -8
- package/src/triggers/actions/manage-automation.ts +17 -6
- package/src/triggers/actions.ts +16 -4
- package/src/triggers/dispatcher.ts +44 -30
- package/src/triggers/routes.ts +19 -6
- package/src/vitest-config.ts +68 -0
|
@@ -625,6 +625,10 @@ export async function mergeCoreSharingActions(
|
|
|
625
625
|
"manage-recurring-job",
|
|
626
626
|
() => import("../jobs/actions/manage-recurring-job.js"),
|
|
627
627
|
],
|
|
628
|
+
[
|
|
629
|
+
"list-automation-runs",
|
|
630
|
+
() => import("../jobs/actions/list-automation-runs.js"),
|
|
631
|
+
],
|
|
628
632
|
[
|
|
629
633
|
"list-automations",
|
|
630
634
|
() => import("../triggers/actions/list-automations.js"),
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared plumbing for collaboration emails (comments, replies, reactions,
|
|
3
|
+
* mentions).
|
|
4
|
+
*
|
|
5
|
+
* Two things every app was about to reimplement live here: resolving who
|
|
6
|
+
* should receive an activity email, and reporting delivery honestly. A
|
|
7
|
+
* workspace with no email provider is a *different* outcome from "nobody
|
|
8
|
+
* wanted this email" — collapsing both into an empty success is what let a
|
|
9
|
+
* dead notification toggle ship unnoticed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getUserSetting } from "../settings/user-settings.js";
|
|
13
|
+
import { isEmailConfigured } from "./email.js";
|
|
14
|
+
|
|
15
|
+
export type ActivityDeliveryFailure = { email: string; error: string };
|
|
16
|
+
|
|
17
|
+
export type ActivityNotificationStatus =
|
|
18
|
+
| "delivered"
|
|
19
|
+
| "delivery-failed"
|
|
20
|
+
| "email-not-configured"
|
|
21
|
+
| "no-recipients"
|
|
22
|
+
| "notification-error";
|
|
23
|
+
|
|
24
|
+
export type ActivityNotificationResult = {
|
|
25
|
+
status: ActivityNotificationStatus;
|
|
26
|
+
sent: string[];
|
|
27
|
+
failed: ActivityDeliveryFailure[];
|
|
28
|
+
/** Set only on `notification-error`: why recipients could not be resolved. */
|
|
29
|
+
error?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Default field read from an app's per-user preference blob. */
|
|
33
|
+
const DEFAULT_PREFERENCE_FIELD = "emailNotifications";
|
|
34
|
+
|
|
35
|
+
export interface ResolveActivityRecipientsInput {
|
|
36
|
+
/** Owner, thread participants, mentions — duplicates and blanks are fine. */
|
|
37
|
+
candidates: (string | null | undefined)[];
|
|
38
|
+
/** The person who caused the activity. Never emailed about their own action. */
|
|
39
|
+
actorEmail?: string | null;
|
|
40
|
+
/** App-owned user settings key, e.g. `clips-user-prefs`. */
|
|
41
|
+
preferenceKey: string;
|
|
42
|
+
/** Field inside that blob. Absent or non-`false` means opted in. */
|
|
43
|
+
preferenceField?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeEmail(value: string | null | undefined): string {
|
|
47
|
+
return (value ?? "").trim().toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function wantsActivityEmail(
|
|
51
|
+
email: string,
|
|
52
|
+
preferenceKey: string,
|
|
53
|
+
preferenceField: string,
|
|
54
|
+
): Promise<boolean> {
|
|
55
|
+
const prefs = await getUserSetting(email, preferenceKey);
|
|
56
|
+
// A user who never opened settings has no stored blob; treat that as opted
|
|
57
|
+
// in rather than silently dropping their notifications.
|
|
58
|
+
return prefs?.[preferenceField] !== false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Normalize, dedupe, drop the actor, and filter by each recipient's stored
|
|
63
|
+
* preference. Order follows first appearance in `candidates`.
|
|
64
|
+
*/
|
|
65
|
+
export async function resolveActivityRecipients({
|
|
66
|
+
candidates,
|
|
67
|
+
actorEmail,
|
|
68
|
+
preferenceKey,
|
|
69
|
+
preferenceField = DEFAULT_PREFERENCE_FIELD,
|
|
70
|
+
}: ResolveActivityRecipientsInput): Promise<string[]> {
|
|
71
|
+
const actor = normalizeEmail(actorEmail);
|
|
72
|
+
const unique = new Set<string>();
|
|
73
|
+
for (const candidate of candidates) {
|
|
74
|
+
const email = normalizeEmail(candidate);
|
|
75
|
+
if (!email || email === actor || !email.includes("@")) continue;
|
|
76
|
+
unique.add(email);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const decisions = await Promise.all(
|
|
80
|
+
[...unique].map(async (email) =>
|
|
81
|
+
(await wantsActivityEmail(email, preferenceKey, preferenceField))
|
|
82
|
+
? email
|
|
83
|
+
: null,
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
return decisions.filter((email): email is string => email !== null);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface NotifyActivityInput extends ResolveActivityRecipientsInput {
|
|
90
|
+
/** Sends one email. Throwing marks that recipient failed, not the batch. */
|
|
91
|
+
send: (to: string) => Promise<unknown>;
|
|
92
|
+
/** Prefix for the delivery-failure log line, e.g. `[slides]`. */
|
|
93
|
+
logLabel?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve recipients and deliver one email each. Returns a status that
|
|
98
|
+
* distinguishes "no email provider" from "nobody to email" from a real send.
|
|
99
|
+
*/
|
|
100
|
+
export async function notifyActivity({
|
|
101
|
+
send,
|
|
102
|
+
logLabel,
|
|
103
|
+
...resolve
|
|
104
|
+
}: NotifyActivityInput): Promise<ActivityNotificationResult> {
|
|
105
|
+
if (!(await isEmailConfigured())) {
|
|
106
|
+
return { status: "email-not-configured", sent: [], failed: [] };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const recipients = await resolveActivityRecipients(resolve);
|
|
110
|
+
if (recipients.length === 0) {
|
|
111
|
+
return { status: "no-recipients", sent: [], failed: [] };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sent: string[] = [];
|
|
115
|
+
const failed: ActivityDeliveryFailure[] = [];
|
|
116
|
+
for (const to of recipients) {
|
|
117
|
+
try {
|
|
118
|
+
await send(to);
|
|
119
|
+
sent.push(to);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
failed.push({
|
|
122
|
+
email: to,
|
|
123
|
+
error: error instanceof Error ? error.message : String(error),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (failed.length > 0) {
|
|
129
|
+
console.error(
|
|
130
|
+
`${logLabel ?? "[activity-notifications]"} delivery failed for ${failed
|
|
131
|
+
.map((failure) => `${failure.email} (${failure.error})`)
|
|
132
|
+
.join(", ")}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Every recipient failing is an outage, not a delivery. Callers that log or
|
|
137
|
+
// surface this must not see it as the same outcome as a successful send.
|
|
138
|
+
return {
|
|
139
|
+
status: sent.length === 0 ? "delivery-failed" : "delivered",
|
|
140
|
+
sent,
|
|
141
|
+
failed,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run an activity notification without letting it fail the write that caused
|
|
147
|
+
* it. The comment/reaction is already persisted by the time notification runs;
|
|
148
|
+
* rejecting here makes the client roll back and retry, which duplicates the
|
|
149
|
+
* row. The error still surfaces — as a distinct `notification-error` status,
|
|
150
|
+
* never as a silent success.
|
|
151
|
+
*/
|
|
152
|
+
export async function runActivityNotification<
|
|
153
|
+
T extends {
|
|
154
|
+
status: string;
|
|
155
|
+
sent: string[];
|
|
156
|
+
failed: ActivityDeliveryFailure[];
|
|
157
|
+
},
|
|
158
|
+
>(
|
|
159
|
+
logLabel: string,
|
|
160
|
+
resolve: () => Promise<T>,
|
|
161
|
+
): Promise<T | ActivityNotificationResult> {
|
|
162
|
+
try {
|
|
163
|
+
return await resolve();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
console.error(`${logLabel} could not be resolved: ${message}`);
|
|
167
|
+
return {
|
|
168
|
+
status: "notification-error",
|
|
169
|
+
error: message,
|
|
170
|
+
sent: [],
|
|
171
|
+
failed: [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
package/src/server/app-name.ts
CHANGED
|
@@ -51,3 +51,34 @@ export function getAppName(): string | undefined {
|
|
|
51
51
|
cachedFromPkg = name ?? undefined;
|
|
52
52
|
return name;
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the app's slug — the machine-readable identifier used for the
|
|
57
|
+
* per-app transactional email sender (e.g. `clips@agent-native.com`).
|
|
58
|
+
*
|
|
59
|
+
* Only a first-party template name matched by package.json qualifies. A slug
|
|
60
|
+
* derived from an arbitrary `APP_NAME` must not mint a mailbox on the
|
|
61
|
+
* agent-native.com domain: a custom app would then send as
|
|
62
|
+
* `<its-name>@agent-native.com` and route replies to the Builder mailbox.
|
|
63
|
+
* Returns `undefined` for anything unrecognized, which leaves the deployment's
|
|
64
|
+
* configured sender in place.
|
|
65
|
+
*/
|
|
66
|
+
export function getAppSlug(): string | undefined {
|
|
67
|
+
const pkg = readPkg();
|
|
68
|
+
if (!pkg?.name) return undefined;
|
|
69
|
+
return TEMPLATES.find((t) => t.name === pkg.name)?.name;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* One-line description of the app, used to tailor transactional email bodies
|
|
74
|
+
* so recipients recognize which app they signed up for. Sourced from the
|
|
75
|
+
* first-party template metadata; `undefined` for unknown apps.
|
|
76
|
+
*/
|
|
77
|
+
export function getAppDescription(): string | undefined {
|
|
78
|
+
const pkg = readPkg();
|
|
79
|
+
if (pkg?.name) {
|
|
80
|
+
const tmpl = TEMPLATES.find((t) => t.name === pkg.name);
|
|
81
|
+
if (tmpl) return tmpl.hint;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
@@ -933,11 +933,11 @@ async function createBetterAuthInstance(
|
|
|
933
933
|
""
|
|
934
934
|
).replace(/\/$/, "");
|
|
935
935
|
const resetUrl = `${appUrl}${appBasePath}/_agent-native/auth/reset?token=${encodeURIComponent(token)}`;
|
|
936
|
-
const { subject, html, text } = renderResetPasswordEmail({
|
|
936
|
+
const { subject, html, text, appSender } = renderResetPasswordEmail({
|
|
937
937
|
email: user.email,
|
|
938
938
|
resetUrl,
|
|
939
939
|
});
|
|
940
|
-
await sendEmail({ to: user.email, subject, html, text });
|
|
940
|
+
await sendEmail({ to: user.email, subject, html, text, appSender });
|
|
941
941
|
},
|
|
942
942
|
},
|
|
943
943
|
emailVerification: {
|
|
@@ -960,11 +960,11 @@ async function createBetterAuthInstance(
|
|
|
960
960
|
const verifyUrl = verifyBasePath
|
|
961
961
|
? url.replace(/(\/\/[^/]+)(\/)/, `$1${verifyBasePath}$2`)
|
|
962
962
|
: url;
|
|
963
|
-
const { subject, html, text } = renderVerifySignupEmail({
|
|
963
|
+
const { subject, html, text, appSender } = renderVerifySignupEmail({
|
|
964
964
|
email: user.email,
|
|
965
965
|
verifyUrl,
|
|
966
966
|
});
|
|
967
|
-
await sendEmail({ to: user.email, subject, html, text });
|
|
967
|
+
await sendEmail({ to: user.email, subject, html, text, appSender });
|
|
968
968
|
},
|
|
969
969
|
},
|
|
970
970
|
socialProviders,
|
|
@@ -11,13 +11,22 @@
|
|
|
11
11
|
* site — keeps the transactional look-and-feel consistent.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { getAppName } from "./app-name.js";
|
|
14
|
+
import { getAppName, getAppSlug, getAppDescription } from "./app-name.js";
|
|
15
15
|
import { renderEmail, emailStrong } from "./email-template.js";
|
|
16
16
|
|
|
17
|
+
/** Shared reply-to for the framework's transactional emails. */
|
|
18
|
+
export const AGENT_NATIVE_REPLY_TO = "agent-native@builder.io";
|
|
19
|
+
|
|
17
20
|
export interface RenderedEmailMessage {
|
|
18
21
|
subject: string;
|
|
19
22
|
html: string;
|
|
20
23
|
text: string;
|
|
24
|
+
/**
|
|
25
|
+
* Per-app sender branding, applied by `sendEmail` only on first-party
|
|
26
|
+
* agent-native.com deployments. Self-hosted deployments keep the sender and
|
|
27
|
+
* reply-to they configured via EMAIL_FROM.
|
|
28
|
+
*/
|
|
29
|
+
appSender?: { name: string; slug: string; replyTo?: string };
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
/**
|
|
@@ -33,6 +42,17 @@ function resolveAppName(): string {
|
|
|
33
42
|
return stripCrlf(getAppName() || "Agent Native");
|
|
34
43
|
}
|
|
35
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Recipient-facing brand for auth emails. Only a recognized first-party
|
|
47
|
+
* template is presented as "Agent-Native <App>"; a custom deployment keeps its
|
|
48
|
+
* own name so its users aren't told they signed up for an Agent Native app.
|
|
49
|
+
*/
|
|
50
|
+
function resolveBrand(slug: string | undefined): string {
|
|
51
|
+
const appName = getAppName();
|
|
52
|
+
if (!appName) return "Agent Native";
|
|
53
|
+
return slug ? `Agent-Native ${stripCrlf(appName)}` : stripCrlf(appName);
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
// ---------------------------------------------------------------------------
|
|
37
57
|
// Organization invitation
|
|
38
58
|
// ---------------------------------------------------------------------------
|
|
@@ -89,28 +109,58 @@ export interface RenderVerifySignupEmailArgs {
|
|
|
89
109
|
verifyUrl: string;
|
|
90
110
|
}
|
|
91
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Customer-facing description overrides for the verification email body. The
|
|
114
|
+
* default descriptions come from the app-picker hints, which name competitors
|
|
115
|
+
* for internal positioning; these rewrites frame them as "replacement" for a
|
|
116
|
+
* customer-facing email without changing the picker copy.
|
|
117
|
+
*/
|
|
118
|
+
const VERIFY_EMAIL_DESCRIPTIONS: Record<string, string> = {
|
|
119
|
+
calendar:
|
|
120
|
+
"Agent-native Google Calendar replacement — manage events, sync, and public booking",
|
|
121
|
+
content:
|
|
122
|
+
"Open-source Obsidian/Notion replacement for MDX — edit local docs with agent assistance",
|
|
123
|
+
slides:
|
|
124
|
+
"Agent-native Google Slides replacement — generate and edit React presentations",
|
|
125
|
+
analytics:
|
|
126
|
+
"Agent-native Amplitude/Mixpanel replacement — connect data sources, prompt for charts",
|
|
127
|
+
mail: "Agent-native Superhuman replacement — email client with keyboard shortcuts and AI triage",
|
|
128
|
+
};
|
|
129
|
+
|
|
92
130
|
export function renderVerifySignupEmail(
|
|
93
131
|
args: RenderVerifySignupEmailArgs,
|
|
94
132
|
): RenderedEmailMessage {
|
|
95
133
|
const email = stripCrlf(args.email);
|
|
96
|
-
const
|
|
134
|
+
const slug = getAppSlug();
|
|
135
|
+
const brand = resolveBrand(slug);
|
|
136
|
+
const description = slug
|
|
137
|
+
? (VERIFY_EMAIL_DESCRIPTIONS[slug] ?? getAppDescription())
|
|
138
|
+
: undefined;
|
|
139
|
+
|
|
140
|
+
const paragraphs = [
|
|
141
|
+
`Thanks for signing up for ${emailStrong(brand)}. To finish creating your account, confirm that ${emailStrong(email)} is your email address.`,
|
|
142
|
+
];
|
|
143
|
+
if (description) {
|
|
144
|
+
paragraphs.push(`${stripCrlf(description).replace(/\.\s*$/, "")}.`);
|
|
145
|
+
}
|
|
146
|
+
paragraphs.push(`This link expires in 1 hour.`);
|
|
97
147
|
|
|
98
148
|
const { html, text } = renderEmail({
|
|
99
|
-
brandName:
|
|
100
|
-
preheader: `Confirm ${email} to finish setting up your ${
|
|
101
|
-
heading: `Verify your email for ${
|
|
102
|
-
paragraphs
|
|
103
|
-
`Thanks for signing up for ${emailStrong(appName)}. To finish creating your account, confirm that ${emailStrong(email)} is your email address.`,
|
|
104
|
-
`This link expires in 1 hour.`,
|
|
105
|
-
],
|
|
149
|
+
brandName: brand,
|
|
150
|
+
preheader: `Confirm ${email} to finish setting up your ${brand} account.`,
|
|
151
|
+
heading: `Verify your email for ${brand}`,
|
|
152
|
+
paragraphs,
|
|
106
153
|
cta: { label: "Verify email", url: args.verifyUrl },
|
|
107
154
|
footer: `If you didn't sign up, you can safely ignore this email.`,
|
|
108
155
|
});
|
|
109
156
|
|
|
110
157
|
return {
|
|
111
|
-
subject: `Verify your email for ${
|
|
158
|
+
subject: `Verify your email for ${brand}`,
|
|
112
159
|
html,
|
|
113
160
|
text,
|
|
161
|
+
appSender: slug
|
|
162
|
+
? { name: brand, slug, replyTo: AGENT_NATIVE_REPLY_TO }
|
|
163
|
+
: undefined,
|
|
114
164
|
};
|
|
115
165
|
}
|
|
116
166
|
|
|
@@ -129,12 +179,15 @@ export function renderResetPasswordEmail(
|
|
|
129
179
|
args: RenderResetPasswordEmailArgs,
|
|
130
180
|
): RenderedEmailMessage {
|
|
131
181
|
const email = stripCrlf(args.email);
|
|
132
|
-
|
|
182
|
+
// Match the verification email branding so password resets are clearly tied
|
|
183
|
+
// to the specific app. No value pitch here — it's a security email.
|
|
184
|
+
const slug = getAppSlug();
|
|
185
|
+
const brand = resolveBrand(slug);
|
|
133
186
|
|
|
134
187
|
const { html, text } = renderEmail({
|
|
135
|
-
brandName:
|
|
188
|
+
brandName: brand,
|
|
136
189
|
preheader: `Reset the password for ${email}. This link expires in 1 hour.`,
|
|
137
|
-
heading: `Reset your ${
|
|
190
|
+
heading: `Reset your ${brand} password`,
|
|
138
191
|
paragraphs: [
|
|
139
192
|
`Someone requested a password reset for ${emailStrong(email)}. Click the button below to choose a new password.`,
|
|
140
193
|
`This link expires in 1 hour.`,
|
|
@@ -144,8 +197,11 @@ export function renderResetPasswordEmail(
|
|
|
144
197
|
});
|
|
145
198
|
|
|
146
199
|
return {
|
|
147
|
-
subject: `Reset your ${
|
|
200
|
+
subject: `Reset your ${brand} password`,
|
|
148
201
|
html,
|
|
149
202
|
text,
|
|
203
|
+
appSender: slug
|
|
204
|
+
? { name: brand, slug, replyTo: AGENT_NATIVE_REPLY_TO }
|
|
205
|
+
: undefined,
|
|
150
206
|
};
|
|
151
207
|
}
|
package/src/server/email.ts
CHANGED
|
@@ -39,6 +39,14 @@ export interface SendEmailArgs {
|
|
|
39
39
|
fromName?: string;
|
|
40
40
|
cc?: string | string[];
|
|
41
41
|
replyTo?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Per-app branding for first-party agent-native.com deployments. Applied
|
|
44
|
+
* only when the configured EMAIL_FROM is already on agent-native.com, so a
|
|
45
|
+
* self-hosted deployment keeps its own verified sender and support mailbox
|
|
46
|
+
* instead of sending as an unverified address a provider would reject.
|
|
47
|
+
* An explicit `from` / `replyTo` always wins.
|
|
48
|
+
*/
|
|
49
|
+
appSender?: { name: string; slug: string; replyTo?: string };
|
|
42
50
|
inReplyTo?: string;
|
|
43
51
|
references?: string;
|
|
44
52
|
attachments?: EmailAttachment[];
|
|
@@ -150,6 +158,56 @@ function withDisplayName(from: string, name: string): string {
|
|
|
150
158
|
return `"${safe}" <${address}>`;
|
|
151
159
|
}
|
|
152
160
|
|
|
161
|
+
const AGENT_NATIVE_SENDER_DOMAIN = "agent-native.com";
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve the per-app sender address, but only for deployments whose
|
|
165
|
+
* configured sender is already on agent-native.com. Any other (or missing)
|
|
166
|
+
* EMAIL_FROM means we cannot prove the branded address is a verified sender,
|
|
167
|
+
* so the deployment's own configuration is left untouched.
|
|
168
|
+
*/
|
|
169
|
+
let warnedAppSenderSuppressed = false;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Suppressing the branding is correct for a sender we cannot prove we own,
|
|
173
|
+
* but it must not be invisible, or an operator sees generic senders with
|
|
174
|
+
* nothing pointing at why.
|
|
175
|
+
*
|
|
176
|
+
* EMAIL_FROM resolves per user/org/workspace through the scoped secret store,
|
|
177
|
+
* so the resolved address is tenant data and must never reach shared logs, and
|
|
178
|
+
* anything keyed by it would grow without bound in a warm worker. The message
|
|
179
|
+
* therefore carries no tenant values, which also makes it identical for every
|
|
180
|
+
* suppressed config — so emitting it once per process loses nothing.
|
|
181
|
+
*/
|
|
182
|
+
function warnAppSenderSuppressed(): void {
|
|
183
|
+
if (warnedAppSenderSuppressed) return;
|
|
184
|
+
warnedAppSenderSuppressed = true;
|
|
185
|
+
console.warn(
|
|
186
|
+
`[agent-native:email] Per-app sender branding is off because the ` +
|
|
187
|
+
`configured EMAIL_FROM is not on ${AGENT_NATIVE_SENDER_DOMAIN}. ` +
|
|
188
|
+
`Transactional email keeps the configured sender. Expected when self-hosting.`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function resolveAppSender(
|
|
193
|
+
configuredFrom: string | undefined,
|
|
194
|
+
appSender: SendEmailArgs["appSender"],
|
|
195
|
+
): { address: string; name: string; replyTo?: string } | undefined {
|
|
196
|
+
if (!appSender) return undefined;
|
|
197
|
+
const address = configuredFrom
|
|
198
|
+
? parseSendGridFrom(configuredFrom).email.toLowerCase()
|
|
199
|
+
: undefined;
|
|
200
|
+
if (!address?.endsWith(`@${AGENT_NATIVE_SENDER_DOMAIN}`)) {
|
|
201
|
+
warnAppSenderSuppressed();
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
address: `${appSender.slug}@${AGENT_NATIVE_SENDER_DOMAIN}`,
|
|
206
|
+
name: appSender.name,
|
|
207
|
+
replyTo: appSender.replyTo,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
153
211
|
async function sendEmailWithSignal(
|
|
154
212
|
args: SendEmailArgs,
|
|
155
213
|
signal?: AbortSignal,
|
|
@@ -157,7 +215,12 @@ async function sendEmailWithSignal(
|
|
|
157
215
|
const config = await resolveEmailTransport();
|
|
158
216
|
signal?.throwIfAborted();
|
|
159
217
|
const provider = config.provider;
|
|
160
|
-
const
|
|
218
|
+
const branded = resolveAppSender(config.from, args.appSender);
|
|
219
|
+
const from =
|
|
220
|
+
branded && !args.from
|
|
221
|
+
? withDisplayName(branded.address, args.fromName ?? branded.name)
|
|
222
|
+
: getFromAddress(config, args.from, args.fromName);
|
|
223
|
+
const replyTo = args.replyTo ?? branded?.replyTo;
|
|
161
224
|
const attachments = resolveAttachments(args);
|
|
162
225
|
|
|
163
226
|
if (provider === "resend") {
|
|
@@ -169,7 +232,7 @@ async function sendEmailWithSignal(
|
|
|
169
232
|
text: args.text,
|
|
170
233
|
};
|
|
171
234
|
if (args.cc) payload.cc = Array.isArray(args.cc) ? args.cc : [args.cc];
|
|
172
|
-
if (
|
|
235
|
+
if (replyTo) payload.reply_to = replyTo;
|
|
173
236
|
if (attachments?.length) {
|
|
174
237
|
payload.attachments = attachments.map((a) => ({
|
|
175
238
|
filename: a.filename,
|
|
@@ -220,7 +283,7 @@ async function sendEmailWithSignal(
|
|
|
220
283
|
{ type: "text/html", value: args.html },
|
|
221
284
|
],
|
|
222
285
|
};
|
|
223
|
-
if (
|
|
286
|
+
if (replyTo) sgPayload.reply_to = parseSendGridFrom(replyTo);
|
|
224
287
|
const sgHeaders: Record<string, string> = {};
|
|
225
288
|
if (args.inReplyTo) sgHeaders["In-Reply-To"] = args.inReplyTo;
|
|
226
289
|
if (args.references) sgHeaders["References"] = args.references;
|
package/src/server/index.ts
CHANGED
|
@@ -547,6 +547,16 @@ export {
|
|
|
547
547
|
type EmailProvider,
|
|
548
548
|
type SendEmailArgs,
|
|
549
549
|
} from "./email.js";
|
|
550
|
+
export {
|
|
551
|
+
notifyActivity,
|
|
552
|
+
runActivityNotification,
|
|
553
|
+
resolveActivityRecipients,
|
|
554
|
+
type ActivityDeliveryFailure,
|
|
555
|
+
type ActivityNotificationResult,
|
|
556
|
+
type ActivityNotificationStatus,
|
|
557
|
+
type NotifyActivityInput,
|
|
558
|
+
type ResolveActivityRecipientsInput,
|
|
559
|
+
} from "./activity-notifications.js";
|
|
550
560
|
export {
|
|
551
561
|
renderEmail,
|
|
552
562
|
emailStrong,
|
package/src/sharing/access.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { and, eq, or, sql, type SQL } from "drizzle-orm";
|
|
17
17
|
|
|
18
|
+
import { orgMembers } from "../org/schema.js";
|
|
18
19
|
import {
|
|
19
20
|
getRequestAuthCapability,
|
|
20
21
|
getRequestUserEmail,
|
|
@@ -87,6 +88,35 @@ function emailColumnMatches(column: any, email: string): SQL {
|
|
|
87
88
|
return sql`lower(${column}) = ${email}`;
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Real `org_members` membership, independent of the caller's currently
|
|
93
|
+
* active org (`ctx.orgId`). `org`-visibility access must key off actual
|
|
94
|
+
* membership — a user's active-org selection is a UI convenience, not a
|
|
95
|
+
* statement of which orgs they belong to.
|
|
96
|
+
*
|
|
97
|
+
* Queries through the resource's own `reg.getDb()` — the same connection
|
|
98
|
+
* every other lookup in this file uses — not the ambient `getDbExec()`,
|
|
99
|
+
* since `org_members` lives in that same app database.
|
|
100
|
+
*/
|
|
101
|
+
async function isOrgMember(
|
|
102
|
+
reg: ShareableResourceRegistration,
|
|
103
|
+
memberOrgId: string,
|
|
104
|
+
email: string,
|
|
105
|
+
): Promise<boolean> {
|
|
106
|
+
const db = reg.getDb() as any;
|
|
107
|
+
const rows = await db
|
|
108
|
+
.select({ id: orgMembers.id })
|
|
109
|
+
.from(orgMembers)
|
|
110
|
+
.where(
|
|
111
|
+
and(
|
|
112
|
+
eq(orgMembers.orgId, memberOrgId),
|
|
113
|
+
emailColumnMatches(orgMembers.email, email),
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
.limit(1);
|
|
117
|
+
return rows.length > 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
90
120
|
/**
|
|
91
121
|
* Build a Drizzle `WHERE` clause that admits rows the current user can see.
|
|
92
122
|
* Pass the ownable resource table and its shares table; optional min role
|
|
@@ -470,7 +500,7 @@ async function resolveAccessImpl(
|
|
|
470
500
|
const resource = await loadResourceForAccess(reg, resourceId, options);
|
|
471
501
|
if (!resource) return null;
|
|
472
502
|
|
|
473
|
-
const { userEmail
|
|
503
|
+
const { userEmail } = ctx;
|
|
474
504
|
const normalizedUserEmail = normalizeEmailForAccess(userEmail);
|
|
475
505
|
|
|
476
506
|
if (
|
|
@@ -491,7 +521,19 @@ async function resolveAccessImpl(
|
|
|
491
521
|
// `visibility === "public"` on an `allowPublic: false` resource is treated
|
|
492
522
|
// as private: only owner + explicit shares grant access. Falls through to
|
|
493
523
|
// the explicit-share lookup below.
|
|
494
|
-
|
|
524
|
+
//
|
|
525
|
+
// Membership in the resource's own org, not equality with the caller's
|
|
526
|
+
// currently active org: a caller can be a genuine member of the
|
|
527
|
+
// resource's org while a *different* org is their active selection, and
|
|
528
|
+
// `org` visibility should still admit them. Still requires some active
|
|
529
|
+
// org to be set at all (`orgId`), matching the pre-existing behavior for
|
|
530
|
+
// a caller with no active org.
|
|
531
|
+
if (
|
|
532
|
+
resource.visibility === "org" &&
|
|
533
|
+
resource.orgId &&
|
|
534
|
+
normalizedUserEmail &&
|
|
535
|
+
(await isOrgMember(reg, resource.orgId, normalizedUserEmail))
|
|
536
|
+
) {
|
|
495
537
|
const role = await highestShareRole(reg, resourceId, ctx, resource);
|
|
496
538
|
return { role: role ?? "viewer", resource };
|
|
497
539
|
}
|
package/src/sharing/index.ts
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { isOrgMember } from "../org/membership.js";
|
|
2
|
+
import { resolveAccess } from "./access.js";
|
|
3
|
+
import { roleSatisfies, type ShareRole } from "./schema.js";
|
|
4
|
+
|
|
5
|
+
export interface FilterRecipientsInput {
|
|
6
|
+
resourceType: string;
|
|
7
|
+
resourceId: string;
|
|
8
|
+
emails: Iterable<string>;
|
|
9
|
+
/** The resource's org, needed to honor `org` visibility. */
|
|
10
|
+
orgId?: string | null;
|
|
11
|
+
minimumRole?: ShareRole;
|
|
12
|
+
/**
|
|
13
|
+
* Override for resources that are not registered through `registerShareableResource`
|
|
14
|
+
* (the review registry resolves its own). Must honor `userEmail`/`orgId`
|
|
15
|
+
* rather than the ambient request, or the filter checks the wrong principal.
|
|
16
|
+
*/
|
|
17
|
+
resolveRole?: (ctx: {
|
|
18
|
+
userEmail: string;
|
|
19
|
+
orgId?: string;
|
|
20
|
+
}) => Promise<{ role: string } | null>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Keep only the addresses that can still open the resource *right now*.
|
|
25
|
+
*
|
|
26
|
+
* Notification recipient lists are built from historical rows — thread
|
|
27
|
+
* authors, stored mentions, caller-supplied addresses — none of which are an
|
|
28
|
+
* access grant. Without this, mentioning an arbitrary address mails it the
|
|
29
|
+
* comment body, and a collaborator whose share was revoked keeps receiving the
|
|
30
|
+
* thread. Access is re-resolved per address against the live ACL.
|
|
31
|
+
*/
|
|
32
|
+
export async function filterRecipientsByResourceAccess({
|
|
33
|
+
resourceType,
|
|
34
|
+
resourceId,
|
|
35
|
+
emails,
|
|
36
|
+
orgId,
|
|
37
|
+
minimumRole = "viewer",
|
|
38
|
+
resolveRole,
|
|
39
|
+
}: FilterRecipientsInput): Promise<string[]> {
|
|
40
|
+
const resolve =
|
|
41
|
+
resolveRole ??
|
|
42
|
+
((ctx: { userEmail: string; orgId?: string }) =>
|
|
43
|
+
resolveAccess(resourceType, resourceId, ctx));
|
|
44
|
+
const unique = new Set<string>();
|
|
45
|
+
for (const raw of emails) {
|
|
46
|
+
const email = raw.trim().toLowerCase();
|
|
47
|
+
if (email) unique.add(email);
|
|
48
|
+
}
|
|
49
|
+
if (unique.size === 0) return [];
|
|
50
|
+
|
|
51
|
+
const decisions = await Promise.all(
|
|
52
|
+
[...unique].map(async (email) => {
|
|
53
|
+
// Resolve without an org first: passing the resource's org up front
|
|
54
|
+
// would hand org-visibility access to anyone who is not in it.
|
|
55
|
+
const direct = await resolve({ userEmail: email });
|
|
56
|
+
if (direct && roleSatisfies(direct.role as ShareRole, minimumRole)) {
|
|
57
|
+
return email;
|
|
58
|
+
}
|
|
59
|
+
if (!orgId || !(await isOrgMember(orgId, email))) return null;
|
|
60
|
+
const viaOrg = await resolve({ userEmail: email, orgId });
|
|
61
|
+
return viaOrg && roleSatisfies(viaOrg.role as ShareRole, minimumRole)
|
|
62
|
+
? email
|
|
63
|
+
: null;
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
return decisions.filter((email): email is string => email !== null);
|
|
67
|
+
}
|