@agenticmail/enterprise 0.5.71 → 0.5.73

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.
@@ -0,0 +1,47 @@
1
+ import {
2
+ AgentRuntime,
3
+ EmailChannel,
4
+ FollowUpScheduler,
5
+ SessionManager,
6
+ SubAgentManager,
7
+ ToolRegistry,
8
+ callLLM,
9
+ createAgentRuntime,
10
+ createNoopHooks,
11
+ createRuntimeHooks,
12
+ estimateMessageTokens,
13
+ estimateTokens,
14
+ executeTool,
15
+ runAgentLoop,
16
+ toolsToDefinitions
17
+ } from "./chunk-WBHMHTF3.js";
18
+ import "./chunk-TYW5XTOW.js";
19
+ import "./chunk-JLSQOQ5L.js";
20
+ import {
21
+ PROVIDER_REGISTRY,
22
+ listAllProviders,
23
+ resolveApiKeyForProvider,
24
+ resolveProvider
25
+ } from "./chunk-67KZYSLU.js";
26
+ import "./chunk-KFQGP6VL.js";
27
+ export {
28
+ AgentRuntime,
29
+ EmailChannel,
30
+ FollowUpScheduler,
31
+ PROVIDER_REGISTRY,
32
+ SessionManager,
33
+ SubAgentManager,
34
+ ToolRegistry,
35
+ callLLM,
36
+ createAgentRuntime,
37
+ createNoopHooks,
38
+ createRuntimeHooks,
39
+ estimateMessageTokens,
40
+ estimateTokens,
41
+ executeTool,
42
+ listAllProviders,
43
+ resolveApiKeyForProvider,
44
+ resolveProvider,
45
+ runAgentLoop,
46
+ toolsToDefinitions
47
+ };
@@ -0,0 +1,12 @@
1
+ import {
2
+ createServer
3
+ } from "./chunk-KQCDQUAK.js";
4
+ import "./chunk-3SMTCIR4.js";
5
+ import "./chunk-JLSQOQ5L.js";
6
+ import "./chunk-RO537U6H.js";
7
+ import "./chunk-DRXMYYKN.js";
8
+ import "./chunk-67KZYSLU.js";
9
+ import "./chunk-KFQGP6VL.js";
10
+ export {
11
+ createServer
12
+ };
@@ -0,0 +1,20 @@
1
+ import {
2
+ promptCompanyInfo,
3
+ promptDatabase,
4
+ promptDeployment,
5
+ promptDomain,
6
+ promptRegistration,
7
+ provision,
8
+ runSetupWizard
9
+ } from "./chunk-G7BU3BUW.js";
10
+ import "./chunk-QDXUZP7Y.js";
11
+ import "./chunk-KFQGP6VL.js";
12
+ export {
13
+ promptCompanyInfo,
14
+ promptDatabase,
15
+ promptDeployment,
16
+ promptDomain,
17
+ promptRegistration,
18
+ provision,
19
+ runSetupWizard
20
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/enterprise",
3
- "version": "0.5.71",
3
+ "version": "0.5.73",
4
4
  "description": "AgenticMail Enterprise — cloud-hosted AI agent identity, email, auth & compliance for organizations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -602,6 +602,49 @@ export function createAdminRoutes(db: DatabaseAdapter) {
602
602
  }
603
603
  });
604
604
 
605
+ // ─── Organization Email Config ─────────────────────
606
+
607
+ api.get('/settings/org-email', requireRole('admin'), async (c) => {
608
+ const settings = await db.getSettings();
609
+ const cfg = settings?.orgEmailConfig;
610
+ if (!cfg) return c.json({ configured: false });
611
+ return c.json({
612
+ configured: cfg.configured || false,
613
+ provider: cfg.provider,
614
+ label: cfg.label,
615
+ oauthClientId: cfg.oauthClientId,
616
+ oauthTenantId: cfg.oauthTenantId,
617
+ });
618
+ });
619
+
620
+ api.put('/settings/org-email', requireRole('admin'), async (c) => {
621
+ const body = await c.req.json();
622
+ const { provider, oauthClientId, oauthClientSecret, oauthTenantId } = body;
623
+ if (!provider || !['google', 'microsoft'].includes(provider)) {
624
+ return c.json({ error: 'provider must be "google" or "microsoft"' }, 400);
625
+ }
626
+ if (!oauthClientId || !oauthClientSecret) {
627
+ return c.json({ error: 'oauthClientId and oauthClientSecret are required' }, 400);
628
+ }
629
+ const label = provider === 'google' ? 'Google Workspace' : 'Microsoft 365';
630
+ const orgEmailConfig = {
631
+ provider,
632
+ oauthClientId,
633
+ oauthClientSecret,
634
+ oauthTenantId: provider === 'microsoft' ? (oauthTenantId || 'common') : undefined,
635
+ oauthRedirectUri: '', // Will be set per-agent at OAuth time
636
+ configured: true,
637
+ label,
638
+ };
639
+ await db.updateSettings({ orgEmailConfig } as any);
640
+ return c.json({ success: true, orgEmailConfig: { configured: true, provider, label, oauthClientId, oauthTenantId: orgEmailConfig.oauthTenantId } });
641
+ });
642
+
643
+ api.delete('/settings/org-email', requireRole('admin'), async (c) => {
644
+ await db.updateSettings({ orgEmailConfig: null } as any);
645
+ return c.json({ success: true });
646
+ });
647
+
605
648
  // ─── Tool Security Config ─────────────────────────
606
649
 
607
650
  api.get('/settings/tool-security', requireRole('admin'), async (c) => {
@@ -3875,6 +3875,23 @@ function EmailSection(props) {
3875
3875
 
3876
3876
  useEffect(function() { loadConfig(); }, [agentId]);
3877
3877
 
3878
+ // Listen for OAuth popup completion
3879
+ useEffect(function() {
3880
+ function onMessage(e) {
3881
+ if (e.data && e.data.type === 'oauth-result') {
3882
+ if (e.data.status === 'success') {
3883
+ toast('Email connected successfully', 'success');
3884
+ } else {
3885
+ toast('OAuth failed: ' + (e.data.message || 'Unknown error'), 'error');
3886
+ }
3887
+ loadConfig();
3888
+ if (reload) reload();
3889
+ }
3890
+ }
3891
+ window.addEventListener('message', onMessage);
3892
+ return function() { window.removeEventListener('message', onMessage); };
3893
+ }, []);
3894
+
3878
3895
  // Preset changed → auto-fill hosts
3879
3896
  var PRESETS = {
3880
3897
  microsoft365: { label: 'Microsoft 365 / Outlook', imapHost: 'outlook.office365.com', imapPort: 993, smtpHost: 'smtp.office365.com', smtpPort: 587 },
@@ -3908,18 +3925,22 @@ function EmailSection(props) {
3908
3925
  });
3909
3926
  } else if (form.provider === 'microsoft') {
3910
3927
  var baseUrl = window.location.origin;
3928
+ var hasOrgMs = emailConfig && emailConfig.orgEmailConfig && emailConfig.orgEmailConfig.provider === 'microsoft';
3911
3929
  Object.assign(body, {
3912
- oauthClientId: form.oauthClientId,
3913
- oauthClientSecret: form.oauthClientSecret,
3930
+ oauthClientId: form.oauthClientId || undefined,
3931
+ oauthClientSecret: form.oauthClientSecret || undefined,
3914
3932
  oauthTenantId: form.oauthTenantId,
3915
3933
  oauthRedirectUri: baseUrl + '/api/engine/oauth/callback',
3934
+ useOrgConfig: hasOrgMs && !form.oauthClientId ? true : undefined,
3916
3935
  });
3917
3936
  } else if (form.provider === 'google') {
3918
3937
  var gBaseUrl = window.location.origin;
3938
+ var hasOrgG = emailConfig && emailConfig.orgEmailConfig && emailConfig.orgEmailConfig.provider === 'google';
3919
3939
  Object.assign(body, {
3920
- oauthClientId: form.oauthClientId,
3921
- oauthClientSecret: form.oauthClientSecret,
3940
+ oauthClientId: form.oauthClientId || undefined,
3941
+ oauthClientSecret: form.oauthClientSecret || undefined,
3922
3942
  oauthRedirectUri: gBaseUrl + '/api/engine/oauth/callback',
3943
+ useOrgConfig: hasOrgG && !form.oauthClientId ? true : undefined,
3923
3944
  });
3924
3945
  }
3925
3946
 
@@ -4006,6 +4027,15 @@ function EmailSection(props) {
4006
4027
  ),
4007
4028
  h('div', { className: 'card-body' },
4008
4029
 
4030
+ // ─── Org Email Config Banner ──────────────────────
4031
+ emailConfig && emailConfig.orgEmailConfig && h('div', { style: { padding: '12px 16px', background: 'var(--success-soft)', borderRadius: 'var(--radius)', marginBottom: 16, display: 'flex', alignItems: 'center', gap: 10 } },
4032
+ h('span', { style: { fontSize: 18 } }, '\u2705'),
4033
+ h('div', null,
4034
+ h('div', { style: { fontSize: 13, fontWeight: 600 } }, 'Your organization has configured ', emailConfig.orgEmailConfig.label || emailConfig.orgEmailConfig.provider),
4035
+ h('div', { style: { fontSize: 12, color: 'var(--text-muted)' } }, 'Select ', emailConfig.orgEmailConfig.provider === 'google' ? 'Google OAuth' : 'Microsoft OAuth', ' below — Client ID and Secret will be inherited automatically.')
4036
+ )
4037
+ ),
4038
+
4009
4039
  // ─── Provider Selection ─────────────────────────
4010
4040
  h('div', { style: { marginBottom: 20 } },
4011
4041
  h('label', { style: labelStyle }, 'Connection Method'),
@@ -4128,16 +4158,24 @@ function EmailSection(props) {
4128
4158
  ),
4129
4159
 
4130
4160
  // ─── Google OAuth Config ────────────────────────
4131
- form.provider === 'google' && h(Fragment, null,
4132
- h('div', { style: { padding: '12px 16px', background: 'var(--info-soft)', borderRadius: 'var(--radius)', fontSize: 12, color: 'var(--info)', marginBottom: 16 } },
4133
- h('strong', null, 'Setup Instructions:'), h('br'),
4134
- '1. Go to ', h('a', { href: 'https://console.cloud.google.com/apis/credentials', target: '_blank', style: { color: 'var(--accent)' } }, 'Google Cloud Console → Credentials'), h('br'),
4135
- '2. Create an OAuth 2.0 Client ID (Web application) → add redirect URI: ', h('code', { style: { background: 'var(--bg-tertiary)', padding: '1px 4px', borderRadius: 3 } }, window.location.origin + '/api/engine/oauth/callback'), h('br'),
4136
- '3. Enable the Gmail API in your project', h('br'),
4137
- '4. Copy the Client ID and Client Secret below'
4138
- ),
4161
+ form.provider === 'google' && (function() {
4162
+ var hasOrg = emailConfig && emailConfig.orgEmailConfig && emailConfig.orgEmailConfig.provider === 'google';
4163
+ return h(Fragment, null,
4164
+ hasOrg
4165
+ ? h('div', { style: { padding: '12px 16px', background: 'var(--success-soft)', borderRadius: 'var(--radius)', fontSize: 12, marginBottom: 16 } },
4166
+ h('strong', null, '\u2705 Using organization Google Workspace credentials'), h('br'),
4167
+ 'Client ID: ', h('code', { style: { fontSize: 11 } }, emailConfig.orgEmailConfig.oauthClientId), h('br'),
4168
+ h('span', { style: { color: 'var(--text-muted)' } }, 'Just click "Save Configuration" then authorize with the agent\'s Google account.')
4169
+ )
4170
+ : h('div', { style: { padding: '12px 16px', background: 'var(--info-soft)', borderRadius: 'var(--radius)', fontSize: 12, color: 'var(--info)', marginBottom: 16 } },
4171
+ h('strong', null, 'Setup Instructions:'), h('br'),
4172
+ '1. Go to ', h('a', { href: 'https://console.cloud.google.com/apis/credentials', target: '_blank', style: { color: 'var(--accent)' } }, 'Google Cloud Console \u2192 Credentials'), h('br'),
4173
+ '2. Create an OAuth 2.0 Client ID (Web application) \u2192 add redirect URI: ', h('code', { style: { background: 'var(--bg-tertiary)', padding: '1px 4px', borderRadius: 3 } }, window.location.origin + '/api/engine/oauth/callback'), h('br'),
4174
+ '3. Enable the Gmail API in your project', h('br'),
4175
+ '4. Copy the Client ID and Client Secret below'
4176
+ ),
4139
4177
 
4140
- h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 16 } },
4178
+ !hasOrg && h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 16 } },
4141
4179
  h('div', null,
4142
4180
  h('label', { style: labelStyle }, 'OAuth Client ID *'),
4143
4181
  h('input', { style: inputStyle, value: form.oauthClientId, placeholder: 'xxxx.apps.googleusercontent.com', onChange: function(e) { set('oauthClientId', e.target.value); } })
@@ -4153,7 +4191,7 @@ function EmailSection(props) {
4153
4191
  h('p', { style: { fontSize: 12, margin: '0 0 8px', color: 'var(--text-secondary)' } }, 'Click the button below to sign in with the agent\'s Google account and grant Gmail permissions.'),
4154
4192
  h('button', { className: 'btn btn-primary btn-sm', onClick: openOAuth }, 'Authorize with Google')
4155
4193
  )
4156
- ),
4194
+ ); })(),
4157
4195
 
4158
4196
  // ─── Test Result ─────────────────────────────────
4159
4197
  testResult && h('div', { style: { padding: '12px 16px', borderRadius: 'var(--radius)', marginBottom: 16, background: testResult.success ? 'var(--success-soft)' : 'var(--danger-soft)' } },
@@ -53,6 +53,12 @@ export function SettingsPage() {
53
53
  var _apiKeyInput = useState('');
54
54
  var apiKeyInput = _apiKeyInput[0]; var setApiKeyInput = _apiKeyInput[1];
55
55
 
56
+ // Org Email Config
57
+ var _orgEmail = useState({ configured: false, provider: '', oauthClientId: '', oauthClientSecret: '', oauthTenantId: 'common', label: '' });
58
+ var orgEmail = _orgEmail[0]; var setOrgEmail = _orgEmail[1];
59
+ var _orgEmailSaving = useState(false);
60
+ var orgEmailSaving = _orgEmailSaving[0]; var setOrgEmailSaving = _orgEmailSaving[1];
61
+
56
62
  useEffect(() => {
57
63
  apiCall('/settings').then(d => { const s = d.settings || d || {}; setSettings(s); if (s.primaryColor) applyBrandColor(s.primaryColor); if (s.orgId) setOrgId(s.orgId); }).catch(() => {});
58
64
  apiCall('/api-keys').then(d => setApiKeys(d.keys || [])).catch(() => {});
@@ -66,6 +72,9 @@ export function SettingsPage() {
66
72
  }));
67
73
  }).catch(() => {});
68
74
  engineCall('/deploy-credentials?orgId=' + getOrgId()).then(d => setDeployCreds(d.credentials || [])).catch(() => {});
75
+ apiCall('/settings/org-email').then(d => {
76
+ if (d.configured) setOrgEmail({ configured: true, provider: d.provider, oauthClientId: d.oauthClientId || '', oauthClientSecret: '', oauthTenantId: d.oauthTenantId || 'common', label: d.label || '' });
77
+ }).catch(() => {});
69
78
  apiCall('/settings/tool-security').then(d => {
70
79
  var cfg = d.toolSecurityConfig || {};
71
80
  setToolSec({
@@ -108,6 +117,29 @@ export function SettingsPage() {
108
117
  try { await apiCall('/settings', { method: 'PATCH', body: JSON.stringify({ [key]: value }) }); toast('Settings saved', 'success'); } catch (e) { toast(e.message, 'error'); }
109
118
  };
110
119
 
120
+ const saveOrgEmail = async () => {
121
+ if (!orgEmail.provider) { toast('Select a provider', 'error'); return; }
122
+ if (!orgEmail.oauthClientId) { toast('Enter Client ID', 'error'); return; }
123
+ if (!orgEmail.oauthClientSecret) { toast('Enter Client Secret', 'error'); return; }
124
+ setOrgEmailSaving(true);
125
+ try {
126
+ var result = await apiCall('/settings/org-email', { method: 'PUT', body: JSON.stringify({ provider: orgEmail.provider, oauthClientId: orgEmail.oauthClientId, oauthClientSecret: orgEmail.oauthClientSecret, oauthTenantId: orgEmail.oauthTenantId }) });
127
+ setOrgEmail(function(prev) { return Object.assign({}, prev, { configured: true, label: result.orgEmailConfig?.label || prev.label, oauthClientSecret: '' }); });
128
+ toast('Organization email configuration saved', 'success');
129
+ } catch (e) { toast(e.message, 'error'); }
130
+ setOrgEmailSaving(false);
131
+ };
132
+
133
+ const removeOrgEmail = async () => {
134
+ var ok = await showConfirm({ title: 'Remove Organization Email', message: 'Agents using this org-level config will need to be individually configured.', danger: true, confirmText: 'Remove' });
135
+ if (!ok) return;
136
+ try {
137
+ await apiCall('/settings/org-email', { method: 'DELETE' });
138
+ setOrgEmail({ configured: false, provider: '', oauthClientId: '', oauthClientSecret: '', oauthTenantId: 'common', label: '' });
139
+ toast('Organization email configuration removed', 'success');
140
+ } catch (e) { toast(e.message, 'error'); }
141
+ };
142
+
111
143
  const saveSaml = async () => {
112
144
  try {
113
145
  await apiCall('/settings/sso/saml', { method: 'PUT', body: JSON.stringify({ entityId: settings.samlEntityId, ssoUrl: settings.samlSsoUrl, certificate: settings.samlCertificate }) });
@@ -225,14 +257,76 @@ export function SettingsPage() {
225
257
  )
226
258
  ),
227
259
  h('div', { className: 'card' },
228
- h('div', { className: 'card-header' }, h('h3', null, 'Email Configuration')),
260
+ h('div', { className: 'card-header' },
261
+ h('h3', null, 'Organization Email'),
262
+ orgEmail.configured && h('span', { className: 'badge badge-success', style: { marginLeft: 8 } }, orgEmail.label || 'Configured')
263
+ ),
229
264
  h('div', { className: 'card-body' },
230
- h('div', { style: { display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: 'var(--bg-tertiary)', borderRadius: 'var(--radius)' } },
231
- h('div', { style: { fontSize: 24 } }, '\u2709\uFE0F'),
232
- h('div', null,
233
- h('p', { style: { fontSize: 14, fontWeight: 600, marginBottom: 4 } }, 'Email is configured per agent'),
234
- h('p', { style: { fontSize: 13, color: 'var(--text-muted)' } }, 'Each agent has its own email credentials (IMAP/SMTP, Microsoft 365 OAuth, or Google OAuth). Configure email from each agent\'s detail page under the Email tab.')
265
+ h('p', { style: { fontSize: 13, color: 'var(--text-muted)', marginBottom: 16 } }, 'Set up a shared OAuth application for all agents. Each agent will still authorize individually with their own account, but they\'ll use the same Client ID and Secret.'),
266
+
267
+ // Provider selector
268
+ h('div', { style: { display: 'flex', gap: 12, marginBottom: 16 } },
269
+ h('div', {
270
+ onClick: function() { setOrgEmail(function(p) { return Object.assign({}, p, { provider: 'google' }); }); },
271
+ style: { flex: 1, padding: '16px 12px', border: '2px solid ' + (orgEmail.provider === 'google' ? 'var(--accent)' : 'var(--border)'), borderRadius: 'var(--radius)', cursor: 'pointer', textAlign: 'center', background: orgEmail.provider === 'google' ? 'var(--accent-soft)' : 'var(--bg-primary)' }
272
+ },
273
+ h('div', { style: { fontSize: 20, marginBottom: 4 } }, '\uD83D\uDD35'),
274
+ h('div', { style: { fontSize: 13, fontWeight: 600 } }, 'Google Workspace'),
275
+ h('div', { style: { fontSize: 11, color: 'var(--text-muted)' } }, 'Gmail API OAuth')
276
+ ),
277
+ h('div', {
278
+ onClick: function() { setOrgEmail(function(p) { return Object.assign({}, p, { provider: 'microsoft' }); }); },
279
+ style: { flex: 1, padding: '16px 12px', border: '2px solid ' + (orgEmail.provider === 'microsoft' ? 'var(--accent)' : 'var(--border)'), borderRadius: 'var(--radius)', cursor: 'pointer', textAlign: 'center', background: orgEmail.provider === 'microsoft' ? 'var(--accent-soft)' : 'var(--bg-primary)' }
280
+ },
281
+ h('div', { style: { fontSize: 20, marginBottom: 4 } }, '\uD83C\uDFE2'),
282
+ h('div', { style: { fontSize: 13, fontWeight: 600 } }, 'Microsoft 365'),
283
+ h('div', { style: { fontSize: 11, color: 'var(--text-muted)' } }, 'Azure AD / Entra ID')
284
+ )
285
+ ),
286
+
287
+ // Setup instructions
288
+ orgEmail.provider && h('div', { style: { padding: '12px 16px', background: 'var(--info-soft)', borderRadius: 'var(--radius)', marginBottom: 16, fontSize: 12, color: 'var(--text-secondary)' } },
289
+ h('strong', { style: { color: 'var(--accent)' } }, 'Setup Instructions:'), h('br'),
290
+ orgEmail.provider === 'google'
291
+ ? h(Fragment, null,
292
+ '1. Go to ', h('a', { href: 'https://console.cloud.google.com/apis/credentials', target: '_blank', style: { color: 'var(--accent)' } }, 'Google Cloud Console \u2192 Credentials'), h('br'),
293
+ '2. Create an OAuth 2.0 Client ID (Web application) \u2192 add redirect URI: ', h('code', { style: { background: 'var(--bg-tertiary)', padding: '1px 4px', borderRadius: 3, fontSize: 11 } }, window.location.origin + '/api/engine/oauth/callback'), h('br'),
294
+ '3. Enable the Gmail API in your project', h('br'),
295
+ '4. Copy the Client ID and Client Secret below'
296
+ )
297
+ : h(Fragment, null,
298
+ '1. Go to ', h('a', { href: 'https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade', target: '_blank', style: { color: 'var(--accent)' } }, 'Azure Portal \u2192 App Registrations'), h('br'),
299
+ '2. Click "New Registration" \u2192 set redirect URI to: ', h('code', { style: { background: 'var(--bg-tertiary)', padding: '1px 4px', borderRadius: 3, fontSize: 11 } }, window.location.origin + '/api/engine/oauth/callback'), h('br'),
300
+ '3. Copy the Client ID and create a Client Secret below'
301
+ )
302
+ ),
303
+
304
+ // Credentials form
305
+ orgEmail.provider && h(Fragment, null,
306
+ h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 12 } },
307
+ h('div', null,
308
+ h('label', { className: 'form-label' }, 'OAuth Client ID *'),
309
+ h('input', { className: 'input', value: orgEmail.oauthClientId, placeholder: orgEmail.provider === 'google' ? 'xxxx.apps.googleusercontent.com' : 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', onChange: function(e) { setOrgEmail(function(p) { return Object.assign({}, p, { oauthClientId: e.target.value }); }); } })
310
+ ),
311
+ h('div', null,
312
+ h('label', { className: 'form-label' }, 'Client Secret *'),
313
+ h('input', { className: 'input', type: 'password', value: orgEmail.oauthClientSecret, placeholder: orgEmail.configured ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (saved)' : 'Enter client secret', onChange: function(e) { setOrgEmail(function(p) { return Object.assign({}, p, { oauthClientSecret: e.target.value }); }); } })
314
+ )
315
+ ),
316
+ orgEmail.provider === 'microsoft' && h('div', { style: { marginBottom: 12 } },
317
+ h('label', { className: 'form-label' }, 'Tenant ID'),
318
+ h('input', { className: 'input', style: { maxWidth: 400 }, value: orgEmail.oauthTenantId, placeholder: 'common', onChange: function(e) { setOrgEmail(function(p) { return Object.assign({}, p, { oauthTenantId: e.target.value }); }); } }),
319
+ h('p', { className: 'form-help' }, 'Use "common" for multi-tenant or your specific tenant ID')
320
+ ),
321
+ h('div', { style: { display: 'flex', gap: 8 } },
322
+ h('button', { className: 'btn btn-primary', disabled: orgEmailSaving, onClick: saveOrgEmail }, orgEmailSaving ? 'Saving...' : (orgEmail.configured ? 'Update Configuration' : 'Save Configuration')),
323
+ orgEmail.configured && h('button', { className: 'btn btn-danger', onClick: removeOrgEmail }, 'Remove')
235
324
  )
325
+ ),
326
+
327
+ // Info about per-agent auth
328
+ orgEmail.configured && h('div', { style: { marginTop: 16, padding: '12px 16px', background: 'var(--bg-tertiary)', borderRadius: 'var(--radius)', fontSize: 12, color: 'var(--text-muted)' } },
329
+ '\u2139\uFE0F Each agent still needs to individually authorize via their Email tab. This org config provides the shared OAuth app credentials so agents don\'t need to enter Client ID/Secret individually.'
236
330
  )
237
331
  )
238
332
  ),
package/src/db/adapter.ts CHANGED
@@ -200,10 +200,22 @@ export interface CompanySettings {
200
200
  toolSecurityConfig?: Record<string, any>;
201
201
  firewallConfig?: FirewallConfig;
202
202
  modelPricingConfig?: ModelPricingConfig;
203
+ orgEmailConfig?: OrgEmailConfig;
203
204
  createdAt: Date;
204
205
  updatedAt: Date;
205
206
  }
206
207
 
208
+ export interface OrgEmailConfig {
209
+ provider: 'google' | 'microsoft';
210
+ oauthClientId: string;
211
+ oauthClientSecret: string;
212
+ oauthTenantId?: string; // Microsoft only
213
+ oauthRedirectUri?: string;
214
+ oauthScopes?: string[];
215
+ configured: boolean;
216
+ label?: string; // e.g. "Google Workspace" or "Microsoft 365"
217
+ }
218
+
207
219
  export interface FirewallConfig {
208
220
  ipAccess?: {
209
221
  enabled?: boolean;
@@ -106,6 +106,9 @@ export class PostgresAdapter extends DatabaseAdapter {
106
106
  ALTER TABLE company_settings ADD COLUMN IF NOT EXISTS cf_api_token TEXT;
107
107
  ALTER TABLE company_settings ADD COLUMN IF NOT EXISTS cf_account_id TEXT;
108
108
  `).catch(() => {});
109
+ await client.query(`
110
+ ALTER TABLE company_settings ADD COLUMN IF NOT EXISTS org_email_config JSONB;
111
+ `).catch(() => {});
109
112
  await client.query('COMMIT');
110
113
  } catch (err) {
111
114
  await client.query('ROLLBACK');
@@ -175,6 +178,11 @@ export class PostgresAdapter extends DatabaseAdapter {
175
178
  values.push(JSON.stringify(updates.modelPricingConfig));
176
179
  i++;
177
180
  }
181
+ if (updates.orgEmailConfig !== undefined) {
182
+ fields.push(`org_email_config = $${i}`);
183
+ values.push(JSON.stringify(updates.orgEmailConfig));
184
+ i++;
185
+ }
178
186
  fields.push(`updated_at = NOW()`);
179
187
  values.push('default');
180
188
  const { rows } = await this.pool.query(
@@ -553,6 +561,7 @@ export class PostgresAdapter extends DatabaseAdapter {
553
561
  domainStatus: r.domain_status || 'unregistered',
554
562
  cfApiToken: r.cf_api_token || undefined,
555
563
  cfAccountId: r.cf_account_id || undefined,
564
+ orgEmailConfig: r.org_email_config ? (typeof r.org_email_config === 'string' ? JSON.parse(r.org_email_config) : r.org_email_config) : undefined,
556
565
  };
557
566
  }
558
567
  }
@@ -410,13 +410,30 @@ export function createAgentRoutes(opts: {
410
410
  /**
411
411
  * GET /bridge/agents/:id/email-config — Get agent's email configuration (without password).
412
412
  */
413
- router.get('/bridge/agents/:id/email-config', (c) => {
413
+ router.get('/bridge/agents/:id/email-config', async (c) => {
414
414
  const agentId = c.req.param('id');
415
415
  const managed = lifecycle.getAgent(agentId);
416
416
  if (!managed) return c.json({ error: 'Agent not found' }, 404);
417
417
 
418
+ // Fetch org-level email config
419
+ let orgEmailConfig: any = null;
420
+ try {
421
+ const adminDb = getAdminDb();
422
+ if (adminDb) {
423
+ const settings = await adminDb.getSettings();
424
+ if (settings?.orgEmailConfig?.configured) {
425
+ orgEmailConfig = {
426
+ provider: settings.orgEmailConfig.provider,
427
+ label: settings.orgEmailConfig.label,
428
+ oauthClientId: settings.orgEmailConfig.oauthClientId,
429
+ oauthTenantId: settings.orgEmailConfig.oauthTenantId,
430
+ };
431
+ }
432
+ }
433
+ } catch {}
434
+
418
435
  const emailConfig = managed.config?.emailConfig || null;
419
- if (!emailConfig) return c.json({ configured: false });
436
+ if (!emailConfig) return c.json({ configured: false, orgEmailConfig });
420
437
 
421
438
  // Return config without sensitive data
422
439
  return c.json({
@@ -436,6 +453,7 @@ export function createAgentRoutes(opts: {
436
453
  oauthAuthUrl: emailConfig.oauthAuthUrl || undefined,
437
454
  lastConnected: emailConfig.lastConnected,
438
455
  lastError: emailConfig.lastError,
456
+ orgEmailConfig,
439
457
  });
440
458
  });
441
459
 
@@ -455,10 +473,28 @@ export function createAgentRoutes(opts: {
455
473
  if (!managed) return c.json({ error: 'Agent not found' }, 404);
456
474
 
457
475
  const body = await c.req.json();
458
- const { provider, email, password, imapHost, imapPort, smtpHost, smtpPort, preset,
476
+ let { provider, email, password, imapHost, imapPort, smtpHost, smtpPort, preset,
459
477
  oauthClientId, oauthClientSecret, oauthTenantId, oauthRedirectUri } = body;
478
+ const useOrgConfig = body.useOrgConfig === true;
460
479
 
461
480
  if (!provider) return c.json({ error: 'provider is required (imap, microsoft, or google)' }, 400);
481
+
482
+ // If using org-level OAuth config, inherit client credentials
483
+ if (useOrgConfig && (provider === 'google' || provider === 'microsoft')) {
484
+ try {
485
+ const adminDb = getAdminDb();
486
+ if (adminDb) {
487
+ const settings = await adminDb.getSettings();
488
+ if (settings?.orgEmailConfig?.configured && settings.orgEmailConfig.provider === provider) {
489
+ oauthClientId = oauthClientId || settings.orgEmailConfig.oauthClientId;
490
+ oauthClientSecret = oauthClientSecret || settings.orgEmailConfig.oauthClientSecret;
491
+ if (provider === 'microsoft') oauthTenantId = oauthTenantId || settings.orgEmailConfig.oauthTenantId;
492
+ } else {
493
+ return c.json({ error: 'Organization email config not found or provider mismatch' }, 400);
494
+ }
495
+ }
496
+ } catch {}
497
+ }
462
498
  if (!email && provider === 'imap') return c.json({ error: 'email is required' }, 400);
463
499
 
464
500
  const emailConfig: any = {