@agenticmail/enterprise 0.5.229 → 0.5.230

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,114 @@
1
+ import "./chunk-KFQGP6VL.js";
2
+
3
+ // src/cli-serve.ts
4
+ import { existsSync, readFileSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ function loadEnvFile() {
8
+ const candidates = [
9
+ join(process.cwd(), ".env"),
10
+ join(homedir(), ".agenticmail", ".env")
11
+ ];
12
+ for (const envPath of candidates) {
13
+ if (!existsSync(envPath)) continue;
14
+ try {
15
+ const content = readFileSync(envPath, "utf8");
16
+ for (const line of content.split("\n")) {
17
+ const trimmed = line.trim();
18
+ if (!trimmed || trimmed.startsWith("#")) continue;
19
+ const eq = trimmed.indexOf("=");
20
+ if (eq < 0) continue;
21
+ const key = trimmed.slice(0, eq).trim();
22
+ let val = trimmed.slice(eq + 1).trim();
23
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
24
+ val = val.slice(1, -1);
25
+ }
26
+ if (!process.env[key]) process.env[key] = val;
27
+ }
28
+ console.log(`Loaded config from ${envPath}`);
29
+ return;
30
+ } catch {
31
+ }
32
+ }
33
+ }
34
+ async function ensureSecrets() {
35
+ const { randomUUID } = await import("crypto");
36
+ const envDir = join(homedir(), ".agenticmail");
37
+ const envPath = join(envDir, ".env");
38
+ let dirty = false;
39
+ if (!process.env.JWT_SECRET) {
40
+ process.env.JWT_SECRET = randomUUID() + randomUUID();
41
+ dirty = true;
42
+ console.log("[startup] Generated new JWT_SECRET (existing sessions will need to re-login)");
43
+ }
44
+ if (!process.env.AGENTICMAIL_VAULT_KEY) {
45
+ process.env.AGENTICMAIL_VAULT_KEY = randomUUID() + randomUUID();
46
+ dirty = true;
47
+ console.log("[startup] Generated new AGENTICMAIL_VAULT_KEY");
48
+ console.log("[startup] \u26A0\uFE0F Previously encrypted credentials will need to be re-entered in the dashboard");
49
+ }
50
+ if (dirty) {
51
+ try {
52
+ if (!existsSync(envDir)) {
53
+ const { mkdirSync } = await import("fs");
54
+ mkdirSync(envDir, { recursive: true });
55
+ }
56
+ const { appendFileSync } = await import("fs");
57
+ const lines = [];
58
+ let existing = "";
59
+ if (existsSync(envPath)) {
60
+ existing = readFileSync(envPath, "utf8");
61
+ }
62
+ if (!existing.includes("JWT_SECRET=")) {
63
+ lines.push(`JWT_SECRET=${process.env.JWT_SECRET}`);
64
+ }
65
+ if (!existing.includes("AGENTICMAIL_VAULT_KEY=")) {
66
+ lines.push(`AGENTICMAIL_VAULT_KEY=${process.env.AGENTICMAIL_VAULT_KEY}`);
67
+ }
68
+ if (lines.length) {
69
+ appendFileSync(envPath, "\n" + lines.join("\n") + "\n", { mode: 384 });
70
+ console.log(`[startup] Saved secrets to ${envPath}`);
71
+ }
72
+ } catch (e) {
73
+ console.warn(`[startup] Could not save secrets to ${envPath}: ${e.message}`);
74
+ }
75
+ }
76
+ }
77
+ async function runServe(_args) {
78
+ loadEnvFile();
79
+ const DATABASE_URL = process.env.DATABASE_URL;
80
+ const PORT = parseInt(process.env.PORT || "8080", 10);
81
+ await ensureSecrets();
82
+ const JWT_SECRET = process.env.JWT_SECRET;
83
+ const VAULT_KEY = process.env.AGENTICMAIL_VAULT_KEY;
84
+ if (!DATABASE_URL) {
85
+ console.error("ERROR: DATABASE_URL is required.");
86
+ console.error("");
87
+ console.error("Set it via environment variable or .env file:");
88
+ console.error(" DATABASE_URL=postgresql://user:pass@host:5432/db npx @agenticmail/enterprise start");
89
+ console.error("");
90
+ console.error("Or create a .env file (in cwd or ~/.agenticmail/.env):");
91
+ console.error(" DATABASE_URL=postgresql://user:pass@host:5432/db");
92
+ console.error(" JWT_SECRET=your-secret-here");
93
+ console.error(" PORT=3200");
94
+ process.exit(1);
95
+ }
96
+ const { createAdapter } = await import("./factory-K32DV2DR.js");
97
+ const { createServer } = await import("./server-FHXIWJUB.js");
98
+ const db = await createAdapter({
99
+ type: DATABASE_URL.startsWith("postgres") ? "postgres" : "sqlite",
100
+ connectionString: DATABASE_URL
101
+ });
102
+ await db.migrate();
103
+ const server = createServer({
104
+ port: PORT,
105
+ db,
106
+ jwtSecret: JWT_SECRET,
107
+ corsOrigins: ["*"]
108
+ });
109
+ await server.start();
110
+ console.log(`AgenticMail Enterprise server running on :${PORT}`);
111
+ }
112
+ export {
113
+ runServe
114
+ };
package/dist/cli.js CHANGED
@@ -53,14 +53,14 @@ Skill Development:
53
53
  break;
54
54
  case "serve":
55
55
  case "start":
56
- import("./cli-serve-BACNSSXS.js").then((m) => m.runServe(args.slice(1))).catch(fatal);
56
+ import("./cli-serve-YEDRINZV.js").then((m) => m.runServe(args.slice(1))).catch(fatal);
57
57
  break;
58
58
  case "agent":
59
59
  import("./cli-agent-5ZU3YHEY.js").then((m) => m.runAgent(args.slice(1))).catch(fatal);
60
60
  break;
61
61
  case "setup":
62
62
  default:
63
- import("./setup-EW5CRLLS.js").then((m) => m.runSetupWizard()).catch(fatal);
63
+ import("./setup-FNMKPXP2.js").then((m) => m.runSetupWizard()).catch(fatal);
64
64
  break;
65
65
  }
66
66
  function fatal(err) {
@@ -223,8 +223,8 @@ function App() {
223
223
  // Sidebar
224
224
  h('div', { className: sidebarClass, onMouseEnter: onSidebarEnter, onMouseLeave: onSidebarLeave },
225
225
  h('div', { className: 'sidebar-brand' },
226
- h('img', { src: '/dashboard/assets/logo.png', alt: 'AgenticMail', style: { width: 28, height: 28, objectFit: 'contain' } }),
227
- h('div', { className: 'sidebar-brand-text' }, h('h2', null, 'AgenticMail'), h('span', null, 'Enterprise')),
226
+ h('img', { src: (window.__EM_BRANDING__ && window.__EM_BRANDING__.logo) || '/dashboard/assets/logo.png', alt: 'AgenticMail', style: { width: 28, height: 28, objectFit: 'contain' } }),
227
+ h('div', { className: 'sidebar-brand-text' }, h('h2', null, (window.__EM_BRANDING__ && window.__EM_BRANDING__.companyName) || 'AgenticMail'), h('span', null, 'Enterprise')),
228
228
  h('button', { className: 'sidebar-toggle' + (sidebarPinned ? ' pinned' : ''), onClick: toggleSidebarPin, title: sidebarPinned ? 'Unpin sidebar' : 'Pin sidebar' }, sidebarPinned ? I.chevronLeft() : I.panelLeft())
229
229
  ),
230
230
  h('div', { className: 'sidebar-nav' },
@@ -430,5 +430,13 @@ tbody tr:hover { background: var(--bg-hover); }
430
430
  <body>
431
431
  <div id="root"></div>
432
432
  <script type="module" src="/dashboard/app.js?v=6"></script>
433
+ <script>
434
+ (function() {
435
+ var b = window.__EM_BRANDING__;
436
+ if (!b) return;
437
+ if (b.favicon) { var link = document.querySelector('link[rel="icon"]'); if (link) link.href = b.favicon; }
438
+ if (b.appleTouchIcon) { var at = document.createElement('link'); at.rel = 'apple-touch-icon'; at.href = b.appleTouchIcon; document.head.appendChild(at); }
439
+ })();
440
+ </script>
433
441
  </body>
434
442
  </html>
@@ -2,6 +2,10 @@ import { h, useState, useEffect, useCallback, Fragment } from '../components/uti
2
2
  import { apiCall, authCall, engineCall } from '../components/utils.js';
3
3
  import { I } from '../components/icons.js';
4
4
 
5
+ var _b = typeof window !== 'undefined' && window.__EM_BRANDING__ || {};
6
+ var _brandLogo = _b.login_logo || _b.logo || _brandLogo;
7
+ var _brandBg = _b.login_bg || null;
8
+
5
9
  export function LoginPage({ onLogin }) {
6
10
  var [tab, setTab] = useState('password'); // 'password' | 'apikey' | 'sso'
7
11
  var [email, setEmail] = useState('');
@@ -76,10 +80,10 @@ export function LoginPage({ onLogin }) {
76
80
  // ─── 2FA Verification Screen ──────────────────────────
77
81
 
78
82
  if (needs2fa) {
79
- return h('div', { className: 'login-page' },
83
+ return h('div', { className: 'login-page', style: _brandBg ? { backgroundImage: 'url(' + _brandBg + ')', backgroundSize: 'cover', backgroundPosition: 'center' } : {} },
80
84
  h('div', { className: 'login-card' },
81
85
  h('div', { className: 'login-logo' },
82
- h('img', { src: '/dashboard/assets/logo.png', alt: 'AgenticMail', style: { width: 48, height: 48, objectFit: 'contain' } }),
86
+ h('img', { src: _brandLogo, alt: 'AgenticMail', style: { width: 48, height: 48, objectFit: 'contain' } }),
83
87
  h('h1', null, 'Two-Factor Authentication'),
84
88
  h('p', null, 'Enter the code from your authenticator app')
85
89
  ),
@@ -108,10 +112,10 @@ export function LoginPage({ onLogin }) {
108
112
 
109
113
  // ─── Main Login Screen ────────────────────────────────
110
114
 
111
- return h('div', { className: 'login-page' },
115
+ return h('div', { className: 'login-page', style: _brandBg ? { backgroundImage: 'url(' + _brandBg + ')', backgroundSize: 'cover', backgroundPosition: 'center' } : {} },
112
116
  h('div', { className: 'login-card' },
113
117
  h('div', { className: 'login-logo' },
114
- h('img', { src: '/dashboard/assets/logo.png', alt: 'AgenticMail', style: { width: 48, height: 48, objectFit: 'contain' } }),
118
+ h('img', { src: _brandLogo, alt: 'AgenticMail', style: { width: 48, height: 48, objectFit: 'contain' } }),
115
119
  h('h1', null, 'AgenticMail Enterprise'),
116
120
  h('p', null, 'AI Agent Identity & Management Platform')
117
121
  ),
@@ -279,6 +279,106 @@ export function SettingsPage() {
279
279
  )
280
280
  ),
281
281
 
282
+ // ─── Branding & Assets ──────────────────────────────
283
+ h('div', { className: 'card', style: { marginBottom: 16 } },
284
+ h('div', { className: 'card-header' }, h('h3', { style: { display: 'flex', alignItems: 'center' } }, 'Branding & Assets', h(HelpButton, { label: 'Branding & Assets' },
285
+ h('p', null, 'Upload your company logo, favicon, and login page assets. The system automatically generates all required icon sizes (16px, 32px, 48px, 180px, 192px, 512px) and favicon from your logo.'),
286
+ h('p', { style: { marginTop: 8 } }, h('strong', null, 'Supported formats: '), 'PNG, JPG, SVG, WebP, GIF'),
287
+ h('p', { style: { marginTop: 8, padding: 8, background: 'var(--bg-secondary)', borderRadius: 6, fontSize: 13 } }, h('strong', null, 'Tip: '), 'Upload a square PNG logo (512x512 or larger) for best results. The system auto-converts it to favicon.ico and all app icon sizes.')
288
+ ))),
289
+ h('div', { className: 'card-body' },
290
+ h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 } },
291
+ // Company Logo
292
+ h('div', { className: 'form-group' },
293
+ h('label', { className: 'form-label' }, 'Company Logo'),
294
+ h('p', { className: 'form-help', style: { marginBottom: 8 } }, 'Used in dashboard sidebar, emails, and auto-generates favicon + app icons'),
295
+ (settings.branding && settings.branding.logo) && h('div', { style: { marginBottom: 8, padding: 8, background: 'var(--bg-tertiary)', borderRadius: 'var(--radius)', display: 'inline-block' } },
296
+ h('img', { src: settings.branding.logo, style: { maxWidth: 120, maxHeight: 60, objectFit: 'contain' } })
297
+ ),
298
+ h('input', { type: 'file', accept: 'image/*', style: { fontSize: 12 }, onChange: function(e) {
299
+ var file = e.target.files && e.target.files[0];
300
+ if (!file) return;
301
+ if (file.size > 5 * 1024 * 1024) { toast('File too large (max 5MB)', 'error'); return; }
302
+ var reader = new FileReader();
303
+ reader.onload = function() {
304
+ apiCall('/settings/branding', { method: 'POST', body: JSON.stringify({ type: 'logo', data: reader.result, filename: file.name }) })
305
+ .then(function(r) { setSettings(function(s) { return Object.assign({}, s, { branding: r.branding }); }); toast('Logo uploaded! Favicon and icons auto-generated. Refresh to see changes.', 'success'); })
306
+ .catch(function(err) { toast(err.message, 'error'); });
307
+ };
308
+ reader.readAsDataURL(file);
309
+ } })
310
+ ),
311
+ // Login Page Logo (separate from main logo)
312
+ h('div', { className: 'form-group' },
313
+ h('label', { className: 'form-label' }, 'Login Page Logo'),
314
+ h('p', { className: 'form-help', style: { marginBottom: 8 } }, 'Shown on the login page. Falls back to company logo if not set.'),
315
+ (settings.branding && settings.branding.login_logo) && h('div', { style: { marginBottom: 8, padding: 8, background: 'var(--bg-tertiary)', borderRadius: 'var(--radius)', display: 'inline-block' } },
316
+ h('img', { src: settings.branding.login_logo, style: { maxWidth: 120, maxHeight: 60, objectFit: 'contain' } })
317
+ ),
318
+ h('input', { type: 'file', accept: 'image/*', style: { fontSize: 12 }, onChange: function(e) {
319
+ var file = e.target.files && e.target.files[0];
320
+ if (!file) return;
321
+ if (file.size > 5 * 1024 * 1024) { toast('File too large (max 5MB)', 'error'); return; }
322
+ var reader = new FileReader();
323
+ reader.onload = function() {
324
+ apiCall('/settings/branding', { method: 'POST', body: JSON.stringify({ type: 'login_logo', data: reader.result, filename: file.name }) })
325
+ .then(function(r) { setSettings(function(s) { return Object.assign({}, s, { branding: r.branding }); }); toast('Login logo saved!', 'success'); })
326
+ .catch(function(err) { toast(err.message, 'error'); });
327
+ };
328
+ reader.readAsDataURL(file);
329
+ } })
330
+ ),
331
+ // Login Background
332
+ h('div', { className: 'form-group' },
333
+ h('label', { className: 'form-label' }, 'Login Page Background'),
334
+ h('p', { className: 'form-help', style: { marginBottom: 8 } }, 'Background image for the login page'),
335
+ (settings.branding && settings.branding.login_bg) && h('div', { style: { marginBottom: 8, padding: 4, background: 'var(--bg-tertiary)', borderRadius: 'var(--radius)', display: 'inline-block' } },
336
+ h('img', { src: settings.branding.login_bg, style: { maxWidth: 160, maxHeight: 80, objectFit: 'cover', borderRadius: 'var(--radius)' } })
337
+ ),
338
+ h('input', { type: 'file', accept: 'image/*', style: { fontSize: 12 }, onChange: function(e) {
339
+ var file = e.target.files && e.target.files[0];
340
+ if (!file) return;
341
+ if (file.size > 10 * 1024 * 1024) { toast('File too large (max 10MB)', 'error'); return; }
342
+ var reader = new FileReader();
343
+ reader.onload = function() {
344
+ apiCall('/settings/branding', { method: 'POST', body: JSON.stringify({ type: 'login_bg', data: reader.result, filename: file.name }) })
345
+ .then(function(r) { setSettings(function(s) { return Object.assign({}, s, { branding: r.branding }); }); toast('Login background saved!', 'success'); })
346
+ .catch(function(err) { toast(err.message, 'error'); });
347
+ };
348
+ reader.readAsDataURL(file);
349
+ } })
350
+ ),
351
+ // Favicon (manual override)
352
+ h('div', { className: 'form-group' },
353
+ h('label', { className: 'form-label' }, 'Custom Favicon'),
354
+ h('p', { className: 'form-help', style: { marginBottom: 8 } }, 'Override the auto-generated favicon. Upload .ico or .png'),
355
+ (settings.branding && settings.branding.favicon) && h('div', { style: { marginBottom: 8, display: 'inline-flex', alignItems: 'center', gap: 8 } },
356
+ h('img', { src: settings.branding.favicon, style: { width: 32, height: 32, objectFit: 'contain' } }),
357
+ h('span', { style: { fontSize: 11, color: 'var(--text-muted)' } }, 'Current favicon')
358
+ ),
359
+ h('input', { type: 'file', accept: '.ico,.png,.svg', style: { fontSize: 12 }, onChange: function(e) {
360
+ var file = e.target.files && e.target.files[0];
361
+ if (!file) return;
362
+ var reader = new FileReader();
363
+ reader.onload = function() {
364
+ apiCall('/settings/branding', { method: 'POST', body: JSON.stringify({ type: 'favicon', data: reader.result, filename: file.name }) })
365
+ .then(function(r) { setSettings(function(s) { return Object.assign({}, s, { branding: r.branding }); }); toast('Favicon saved! Refresh to see changes.', 'success'); })
366
+ .catch(function(err) { toast(err.message, 'error'); });
367
+ };
368
+ reader.readAsDataURL(file);
369
+ } })
370
+ )
371
+ ),
372
+ // Current branding status
373
+ (settings.branding && Object.keys(settings.branding).length > 0) && h('div', { style: { marginTop: 12, padding: 10, background: 'var(--bg-tertiary)', borderRadius: 'var(--radius)', fontSize: 12 } },
374
+ h('strong', null, 'Active branding: '),
375
+ Object.keys(settings.branding).filter(function(k) { return settings.branding[k]; }).map(function(k) {
376
+ return h('span', { key: k, style: { display: 'inline-block', padding: '2px 8px', margin: '2px 4px', background: 'var(--success-soft)', color: 'var(--success)', borderRadius: 4, fontSize: 11 } }, k.replace(/_/g, ' '));
377
+ })
378
+ )
379
+ )
380
+ ),
381
+
282
382
  // ─── Email Signature Template ─────────────────────
283
383
  h('div', { className: 'card' },
284
384
  h('div', { className: 'card-header' },
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  import {
8
8
  provision,
9
9
  runSetupWizard
10
- } from "./chunk-2HW54CJF.js";
10
+ } from "./chunk-EKAHAAGY.js";
11
11
  import {
12
12
  AgenticMailManager,
13
13
  GoogleEmailProvider,
@@ -42,7 +42,7 @@ import {
42
42
  requireRole,
43
43
  securityHeaders,
44
44
  validate
45
- } from "./chunk-QUY235JZ.js";
45
+ } from "./chunk-DFDEYMA5.js";
46
46
  import "./chunk-OF4MUWWS.js";
47
47
  import {
48
48
  PROVIDER_REGISTRY,
@@ -0,0 +1,15 @@
1
+ import {
2
+ createServer
3
+ } from "./chunk-DFDEYMA5.js";
4
+ import "./chunk-OF4MUWWS.js";
5
+ import "./chunk-UF3ZJMJO.js";
6
+ import "./chunk-3OC6RH7W.js";
7
+ import "./chunk-2DDKGTD6.js";
8
+ import "./chunk-YVK6F5OD.js";
9
+ import "./chunk-MKRNEM5A.js";
10
+ import "./chunk-DRXMYYKN.js";
11
+ import "./chunk-6WSX7QXF.js";
12
+ import "./chunk-KFQGP6VL.js";
13
+ export {
14
+ createServer
15
+ };
@@ -0,0 +1,20 @@
1
+ import {
2
+ promptCompanyInfo,
3
+ promptDatabase,
4
+ promptDeployment,
5
+ promptDomain,
6
+ promptRegistration,
7
+ provision,
8
+ runSetupWizard
9
+ } from "./chunk-EKAHAAGY.js";
10
+ import "./chunk-VQQ4SYYQ.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.229",
3
+ "version": "0.5.230",
4
4
  "description": "AgenticMail Enterprise — cloud-hosted AI agent identity, email, auth & compliance for organizations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -753,12 +753,86 @@ export function createAdminRoutes(db: DatabaseAdapter) {
753
753
  { field: 'cfAccountId', type: 'string', maxLength: 100 },
754
754
  { field: 'plan', type: 'string', maxLength: 32 },
755
755
  { field: 'signatureTemplate', type: 'string', maxLength: 10000 },
756
+ { field: 'branding', type: 'object' },
756
757
  ]);
757
758
 
758
759
  const settings = await updateSettingsAndEmit(body);
759
760
  return c.json(settings);
760
761
  });
761
762
 
763
+ // ─── Branding Asset Upload ──────────────────────────
764
+
765
+ api.post('/settings/branding', requireRole('admin'), async (c) => {
766
+ const body = await c.req.json();
767
+ const { type, data, filename } = body; // type: 'logo' | 'favicon' | 'login_bg', data: base64 string
768
+ if (!type || !data) return c.json({ error: 'type and data are required' }, 400);
769
+ if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);
770
+
771
+ const os = await import('node:os');
772
+ const fs = await import('node:fs');
773
+ const path = await import('node:path');
774
+
775
+ const brandDir = path.join(os.homedir(), '.agenticmail', 'branding');
776
+ if (!fs.existsSync(brandDir)) fs.mkdirSync(brandDir, { recursive: true });
777
+
778
+ // Decode base64 (strip data URL prefix if present)
779
+ const base64 = data.replace(/^data:[^;]+;base64,/, '');
780
+ const buffer = Buffer.from(base64, 'base64');
781
+
782
+ // Determine extension from filename or data URL
783
+ const ext = filename ? path.extname(filename).toLowerCase() : '.png';
784
+ const validExts = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp'];
785
+ if (!validExts.includes(ext)) return c.json({ error: 'Invalid file type. Supported: ' + validExts.join(', ') }, 400);
786
+
787
+ // Save original
788
+ const savedName = type + ext;
789
+ fs.writeFileSync(path.join(brandDir, savedName), buffer);
790
+
791
+ // Auto-generate favicon from logo upload
792
+ if (type === 'logo' || type === 'favicon') {
793
+ try {
794
+ // Generate multiple sizes for favicon
795
+ const sharp = (await import('sharp')).default;
796
+ const sizes = [16, 32, 48, 64, 180, 192, 512];
797
+ for (const size of sizes) {
798
+ await sharp(buffer).resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, `icon-${size}.png`));
799
+ }
800
+ // Generate ICO (just use 32px PNG as simple favicon)
801
+ await sharp(buffer).resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'favicon.png'));
802
+ // Apple touch icon
803
+ await sharp(buffer).resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(path.join(brandDir, 'apple-touch-icon.png'));
804
+ } catch (e: any) {
805
+ console.warn('[branding] Sharp not available, skipping icon generation:', e.message);
806
+ // Still save the original — just won't have auto-generated sizes
807
+ }
808
+ }
809
+
810
+ // Save branding config to settings
811
+ const settings = await db.getSettings();
812
+ const branding = settings?.branding || {};
813
+ (branding as any)[type] = `/branding/${savedName}`;
814
+ if (type === 'logo' || type === 'favicon') {
815
+ (branding as any).favicon = '/branding/favicon.png';
816
+ (branding as any).appleTouchIcon = '/branding/apple-touch-icon.png';
817
+ (branding as any).icon192 = '/branding/icon-192.png';
818
+ (branding as any).icon512 = '/branding/icon-512.png';
819
+ }
820
+ await updateSettingsAndEmit({ branding });
821
+
822
+ return c.json({ success: true, branding, message: 'Branding assets saved. Refresh to see changes.' });
823
+ });
824
+
825
+ api.delete('/settings/branding/:type', requireRole('admin'), async (c) => {
826
+ const type = c.req.param('type');
827
+ if (!['logo', 'favicon', 'login_bg', 'login_logo'].includes(type)) return c.json({ error: 'Invalid type' }, 400);
828
+
829
+ const settings = await db.getSettings();
830
+ const branding = settings?.branding || {};
831
+ delete (branding as any)[type];
832
+ await updateSettingsAndEmit({ branding });
833
+ return c.json({ success: true, branding });
834
+ });
835
+
762
836
  // ─── SSO Configuration ────────────────────────────
763
837
 
764
838
  api.get('/settings/sso', requireRole('admin'), async (c) => {
@@ -223,8 +223,8 @@ function App() {
223
223
  // Sidebar
224
224
  h('div', { className: sidebarClass, onMouseEnter: onSidebarEnter, onMouseLeave: onSidebarLeave },
225
225
  h('div', { className: 'sidebar-brand' },
226
- h('img', { src: '/dashboard/assets/logo.png', alt: 'AgenticMail', style: { width: 28, height: 28, objectFit: 'contain' } }),
227
- h('div', { className: 'sidebar-brand-text' }, h('h2', null, 'AgenticMail'), h('span', null, 'Enterprise')),
226
+ h('img', { src: (window.__EM_BRANDING__ && window.__EM_BRANDING__.logo) || '/dashboard/assets/logo.png', alt: 'AgenticMail', style: { width: 28, height: 28, objectFit: 'contain' } }),
227
+ h('div', { className: 'sidebar-brand-text' }, h('h2', null, (window.__EM_BRANDING__ && window.__EM_BRANDING__.companyName) || 'AgenticMail'), h('span', null, 'Enterprise')),
228
228
  h('button', { className: 'sidebar-toggle' + (sidebarPinned ? ' pinned' : ''), onClick: toggleSidebarPin, title: sidebarPinned ? 'Unpin sidebar' : 'Pin sidebar' }, sidebarPinned ? I.chevronLeft() : I.panelLeft())
229
229
  ),
230
230
  h('div', { className: 'sidebar-nav' },
@@ -430,5 +430,13 @@ tbody tr:hover { background: var(--bg-hover); }
430
430
  <body>
431
431
  <div id="root"></div>
432
432
  <script type="module" src="/dashboard/app.js?v=6"></script>
433
+ <script>
434
+ (function() {
435
+ var b = window.__EM_BRANDING__;
436
+ if (!b) return;
437
+ if (b.favicon) { var link = document.querySelector('link[rel="icon"]'); if (link) link.href = b.favicon; }
438
+ if (b.appleTouchIcon) { var at = document.createElement('link'); at.rel = 'apple-touch-icon'; at.href = b.appleTouchIcon; document.head.appendChild(at); }
439
+ })();
440
+ </script>
433
441
  </body>
434
442
  </html>
@@ -2,6 +2,10 @@ import { h, useState, useEffect, useCallback, Fragment } from '../components/uti
2
2
  import { apiCall, authCall, engineCall } from '../components/utils.js';
3
3
  import { I } from '../components/icons.js';
4
4
 
5
+ var _b = typeof window !== 'undefined' && window.__EM_BRANDING__ || {};
6
+ var _brandLogo = _b.login_logo || _b.logo || _brandLogo;
7
+ var _brandBg = _b.login_bg || null;
8
+
5
9
  export function LoginPage({ onLogin }) {
6
10
  var [tab, setTab] = useState('password'); // 'password' | 'apikey' | 'sso'
7
11
  var [email, setEmail] = useState('');
@@ -76,10 +80,10 @@ export function LoginPage({ onLogin }) {
76
80
  // ─── 2FA Verification Screen ──────────────────────────
77
81
 
78
82
  if (needs2fa) {
79
- return h('div', { className: 'login-page' },
83
+ return h('div', { className: 'login-page', style: _brandBg ? { backgroundImage: 'url(' + _brandBg + ')', backgroundSize: 'cover', backgroundPosition: 'center' } : {} },
80
84
  h('div', { className: 'login-card' },
81
85
  h('div', { className: 'login-logo' },
82
- h('img', { src: '/dashboard/assets/logo.png', alt: 'AgenticMail', style: { width: 48, height: 48, objectFit: 'contain' } }),
86
+ h('img', { src: _brandLogo, alt: 'AgenticMail', style: { width: 48, height: 48, objectFit: 'contain' } }),
83
87
  h('h1', null, 'Two-Factor Authentication'),
84
88
  h('p', null, 'Enter the code from your authenticator app')
85
89
  ),
@@ -108,10 +112,10 @@ export function LoginPage({ onLogin }) {
108
112
 
109
113
  // ─── Main Login Screen ────────────────────────────────
110
114
 
111
- return h('div', { className: 'login-page' },
115
+ return h('div', { className: 'login-page', style: _brandBg ? { backgroundImage: 'url(' + _brandBg + ')', backgroundSize: 'cover', backgroundPosition: 'center' } : {} },
112
116
  h('div', { className: 'login-card' },
113
117
  h('div', { className: 'login-logo' },
114
- h('img', { src: '/dashboard/assets/logo.png', alt: 'AgenticMail', style: { width: 48, height: 48, objectFit: 'contain' } }),
118
+ h('img', { src: _brandLogo, alt: 'AgenticMail', style: { width: 48, height: 48, objectFit: 'contain' } }),
115
119
  h('h1', null, 'AgenticMail Enterprise'),
116
120
  h('p', null, 'AI Agent Identity & Management Platform')
117
121
  ),
@@ -279,6 +279,106 @@ export function SettingsPage() {
279
279
  )
280
280
  ),
281
281
 
282
+ // ─── Branding & Assets ──────────────────────────────
283
+ h('div', { className: 'card', style: { marginBottom: 16 } },
284
+ h('div', { className: 'card-header' }, h('h3', { style: { display: 'flex', alignItems: 'center' } }, 'Branding & Assets', h(HelpButton, { label: 'Branding & Assets' },
285
+ h('p', null, 'Upload your company logo, favicon, and login page assets. The system automatically generates all required icon sizes (16px, 32px, 48px, 180px, 192px, 512px) and favicon from your logo.'),
286
+ h('p', { style: { marginTop: 8 } }, h('strong', null, 'Supported formats: '), 'PNG, JPG, SVG, WebP, GIF'),
287
+ h('p', { style: { marginTop: 8, padding: 8, background: 'var(--bg-secondary)', borderRadius: 6, fontSize: 13 } }, h('strong', null, 'Tip: '), 'Upload a square PNG logo (512x512 or larger) for best results. The system auto-converts it to favicon.ico and all app icon sizes.')
288
+ ))),
289
+ h('div', { className: 'card-body' },
290
+ h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 } },
291
+ // Company Logo
292
+ h('div', { className: 'form-group' },
293
+ h('label', { className: 'form-label' }, 'Company Logo'),
294
+ h('p', { className: 'form-help', style: { marginBottom: 8 } }, 'Used in dashboard sidebar, emails, and auto-generates favicon + app icons'),
295
+ (settings.branding && settings.branding.logo) && h('div', { style: { marginBottom: 8, padding: 8, background: 'var(--bg-tertiary)', borderRadius: 'var(--radius)', display: 'inline-block' } },
296
+ h('img', { src: settings.branding.logo, style: { maxWidth: 120, maxHeight: 60, objectFit: 'contain' } })
297
+ ),
298
+ h('input', { type: 'file', accept: 'image/*', style: { fontSize: 12 }, onChange: function(e) {
299
+ var file = e.target.files && e.target.files[0];
300
+ if (!file) return;
301
+ if (file.size > 5 * 1024 * 1024) { toast('File too large (max 5MB)', 'error'); return; }
302
+ var reader = new FileReader();
303
+ reader.onload = function() {
304
+ apiCall('/settings/branding', { method: 'POST', body: JSON.stringify({ type: 'logo', data: reader.result, filename: file.name }) })
305
+ .then(function(r) { setSettings(function(s) { return Object.assign({}, s, { branding: r.branding }); }); toast('Logo uploaded! Favicon and icons auto-generated. Refresh to see changes.', 'success'); })
306
+ .catch(function(err) { toast(err.message, 'error'); });
307
+ };
308
+ reader.readAsDataURL(file);
309
+ } })
310
+ ),
311
+ // Login Page Logo (separate from main logo)
312
+ h('div', { className: 'form-group' },
313
+ h('label', { className: 'form-label' }, 'Login Page Logo'),
314
+ h('p', { className: 'form-help', style: { marginBottom: 8 } }, 'Shown on the login page. Falls back to company logo if not set.'),
315
+ (settings.branding && settings.branding.login_logo) && h('div', { style: { marginBottom: 8, padding: 8, background: 'var(--bg-tertiary)', borderRadius: 'var(--radius)', display: 'inline-block' } },
316
+ h('img', { src: settings.branding.login_logo, style: { maxWidth: 120, maxHeight: 60, objectFit: 'contain' } })
317
+ ),
318
+ h('input', { type: 'file', accept: 'image/*', style: { fontSize: 12 }, onChange: function(e) {
319
+ var file = e.target.files && e.target.files[0];
320
+ if (!file) return;
321
+ if (file.size > 5 * 1024 * 1024) { toast('File too large (max 5MB)', 'error'); return; }
322
+ var reader = new FileReader();
323
+ reader.onload = function() {
324
+ apiCall('/settings/branding', { method: 'POST', body: JSON.stringify({ type: 'login_logo', data: reader.result, filename: file.name }) })
325
+ .then(function(r) { setSettings(function(s) { return Object.assign({}, s, { branding: r.branding }); }); toast('Login logo saved!', 'success'); })
326
+ .catch(function(err) { toast(err.message, 'error'); });
327
+ };
328
+ reader.readAsDataURL(file);
329
+ } })
330
+ ),
331
+ // Login Background
332
+ h('div', { className: 'form-group' },
333
+ h('label', { className: 'form-label' }, 'Login Page Background'),
334
+ h('p', { className: 'form-help', style: { marginBottom: 8 } }, 'Background image for the login page'),
335
+ (settings.branding && settings.branding.login_bg) && h('div', { style: { marginBottom: 8, padding: 4, background: 'var(--bg-tertiary)', borderRadius: 'var(--radius)', display: 'inline-block' } },
336
+ h('img', { src: settings.branding.login_bg, style: { maxWidth: 160, maxHeight: 80, objectFit: 'cover', borderRadius: 'var(--radius)' } })
337
+ ),
338
+ h('input', { type: 'file', accept: 'image/*', style: { fontSize: 12 }, onChange: function(e) {
339
+ var file = e.target.files && e.target.files[0];
340
+ if (!file) return;
341
+ if (file.size > 10 * 1024 * 1024) { toast('File too large (max 10MB)', 'error'); return; }
342
+ var reader = new FileReader();
343
+ reader.onload = function() {
344
+ apiCall('/settings/branding', { method: 'POST', body: JSON.stringify({ type: 'login_bg', data: reader.result, filename: file.name }) })
345
+ .then(function(r) { setSettings(function(s) { return Object.assign({}, s, { branding: r.branding }); }); toast('Login background saved!', 'success'); })
346
+ .catch(function(err) { toast(err.message, 'error'); });
347
+ };
348
+ reader.readAsDataURL(file);
349
+ } })
350
+ ),
351
+ // Favicon (manual override)
352
+ h('div', { className: 'form-group' },
353
+ h('label', { className: 'form-label' }, 'Custom Favicon'),
354
+ h('p', { className: 'form-help', style: { marginBottom: 8 } }, 'Override the auto-generated favicon. Upload .ico or .png'),
355
+ (settings.branding && settings.branding.favicon) && h('div', { style: { marginBottom: 8, display: 'inline-flex', alignItems: 'center', gap: 8 } },
356
+ h('img', { src: settings.branding.favicon, style: { width: 32, height: 32, objectFit: 'contain' } }),
357
+ h('span', { style: { fontSize: 11, color: 'var(--text-muted)' } }, 'Current favicon')
358
+ ),
359
+ h('input', { type: 'file', accept: '.ico,.png,.svg', style: { fontSize: 12 }, onChange: function(e) {
360
+ var file = e.target.files && e.target.files[0];
361
+ if (!file) return;
362
+ var reader = new FileReader();
363
+ reader.onload = function() {
364
+ apiCall('/settings/branding', { method: 'POST', body: JSON.stringify({ type: 'favicon', data: reader.result, filename: file.name }) })
365
+ .then(function(r) { setSettings(function(s) { return Object.assign({}, s, { branding: r.branding }); }); toast('Favicon saved! Refresh to see changes.', 'success'); })
366
+ .catch(function(err) { toast(err.message, 'error'); });
367
+ };
368
+ reader.readAsDataURL(file);
369
+ } })
370
+ )
371
+ ),
372
+ // Current branding status
373
+ (settings.branding && Object.keys(settings.branding).length > 0) && h('div', { style: { marginTop: 12, padding: 10, background: 'var(--bg-tertiary)', borderRadius: 'var(--radius)', fontSize: 12 } },
374
+ h('strong', null, 'Active branding: '),
375
+ Object.keys(settings.branding).filter(function(k) { return settings.branding[k]; }).map(function(k) {
376
+ return h('span', { key: k, style: { display: 'inline-block', padding: '2px 8px', margin: '2px 4px', background: 'var(--success-soft)', color: 'var(--success)', borderRadius: 4, fontSize: 11 } }, k.replace(/_/g, ' '));
377
+ })
378
+ )
379
+ )
380
+ ),
381
+
282
382
  // ─── Email Signature Template ─────────────────────
283
383
  h('div', { className: 'card' },
284
384
  h('div', { className: 'card-header' },