@agenticmail/enterprise 0.4.2 → 0.4.3

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,1943 @@
1
+ import {
2
+ ipAccessControl
3
+ } from "./chunk-RO537U6H.js";
4
+ import {
5
+ PROVIDER_REGISTRY
6
+ } from "./chunk-ZNR5DDTA.js";
7
+ import {
8
+ CircuitBreaker,
9
+ HealthMonitor,
10
+ KeyedRateLimiter,
11
+ requestId
12
+ } from "./chunk-JLSQOQ5L.js";
13
+
14
+ // src/server.ts
15
+ import { Hono as Hono3 } from "hono";
16
+ import { cors } from "hono/cors";
17
+ import { serve } from "@hono/node-server";
18
+ import { readFileSync, existsSync } from "fs";
19
+ import { fileURLToPath } from "url";
20
+ import { dirname, join } from "path";
21
+
22
+ // src/db/proxy.ts
23
+ function createDbProxy(initial) {
24
+ let target = initial;
25
+ const proxy = new Proxy({}, {
26
+ get(_, prop) {
27
+ if (prop === "__swap") {
28
+ return (newAdapter) => {
29
+ const old = target;
30
+ target = newAdapter;
31
+ return old;
32
+ };
33
+ }
34
+ if (prop === "__target") return target;
35
+ const val = target[prop];
36
+ return typeof val === "function" ? val.bind(target) : val;
37
+ }
38
+ });
39
+ return proxy;
40
+ }
41
+
42
+ // src/admin/routes.ts
43
+ import { Hono } from "hono";
44
+
45
+ // src/middleware/index.ts
46
+ function requestIdMiddleware() {
47
+ return async (c, next) => {
48
+ const id = c.req.header("X-Request-Id") || requestId();
49
+ c.set("requestId", id);
50
+ c.header("X-Request-Id", id);
51
+ await next();
52
+ };
53
+ }
54
+ function requestLogger() {
55
+ return async (c, next) => {
56
+ const start = Date.now();
57
+ const method = c.req.method;
58
+ const path = c.req.path;
59
+ await next();
60
+ const elapsed = Date.now() - start;
61
+ const status = c.res.status;
62
+ const reqId = c.get("requestId") || "-";
63
+ const level = status >= 500 ? "ERROR" : status >= 400 ? "WARN" : "INFO";
64
+ console.log(
65
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] ${level} ${method} ${path} ${status} ${elapsed}ms req=${reqId}`
66
+ );
67
+ };
68
+ }
69
+ function rateLimiter(config) {
70
+ const limiter = new KeyedRateLimiter({
71
+ maxTokens: config.limit,
72
+ refillRate: config.limit / config.windowSec
73
+ });
74
+ return async (c, next) => {
75
+ if (config.skipPaths?.some((p) => c.req.path.startsWith(p))) {
76
+ return next();
77
+ }
78
+ const key = config.keyFn?.(c) || c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || c.req.header("x-real-ip") || "unknown";
79
+ if (!limiter.tryConsume(key)) {
80
+ const retryAfter = Math.ceil(limiter.getRetryAfterMs(key) / 1e3);
81
+ c.header("Retry-After", String(retryAfter));
82
+ c.header("X-RateLimit-Limit", String(config.limit));
83
+ c.header("X-RateLimit-Remaining", "0");
84
+ return c.json(
85
+ { error: "Too many requests", retryAfter },
86
+ 429
87
+ );
88
+ }
89
+ await next();
90
+ };
91
+ }
92
+ function securityHeaders() {
93
+ return async (c, next) => {
94
+ await next();
95
+ c.header("X-Content-Type-Options", "nosniff");
96
+ c.header("X-Frame-Options", "DENY");
97
+ c.header("X-XSS-Protection", "0");
98
+ c.header("Referrer-Policy", "strict-origin-when-cross-origin");
99
+ c.header("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
100
+ if (c.req.url.startsWith("https://") || c.req.header("x-forwarded-proto") === "https") {
101
+ c.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
102
+ }
103
+ };
104
+ }
105
+ function errorHandler() {
106
+ return async (c, next) => {
107
+ try {
108
+ await next();
109
+ } catch (err) {
110
+ const reqId = c.get("requestId");
111
+ const status = err.status || err.statusCode || 500;
112
+ const message = status >= 500 ? "Internal server error" : err.message;
113
+ if (status >= 500) {
114
+ console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR req=${reqId}`, err);
115
+ }
116
+ const body = {
117
+ error: message,
118
+ code: err.code,
119
+ requestId: reqId
120
+ };
121
+ if (status === 400 && err.details) {
122
+ body.details = err.details;
123
+ }
124
+ return c.json(body, status);
125
+ }
126
+ };
127
+ }
128
+ var ValidationError = class extends Error {
129
+ status = 400;
130
+ code = "VALIDATION_ERROR";
131
+ details;
132
+ constructor(details) {
133
+ const fields = Object.keys(details).join(", ");
134
+ super(`Validation failed: ${fields}`);
135
+ this.details = details;
136
+ }
137
+ };
138
+ function validate(body, validators) {
139
+ const errors = {};
140
+ for (const v of validators) {
141
+ const value = body[v.field];
142
+ if (value === void 0 || value === null || value === "") {
143
+ if (v.required) errors[v.field] = "Required";
144
+ continue;
145
+ }
146
+ switch (v.type) {
147
+ case "string":
148
+ if (typeof value !== "string") {
149
+ errors[v.field] = "Must be a string";
150
+ break;
151
+ }
152
+ if (v.minLength && value.length < v.minLength) errors[v.field] = `Min length: ${v.minLength}`;
153
+ if (v.maxLength && value.length > v.maxLength) errors[v.field] = `Max length: ${v.maxLength}`;
154
+ if (v.pattern && !v.pattern.test(value)) errors[v.field] = "Invalid format";
155
+ break;
156
+ case "number":
157
+ if (typeof value !== "number" || isNaN(value)) {
158
+ errors[v.field] = "Must be a number";
159
+ break;
160
+ }
161
+ if (v.min !== void 0 && value < v.min) errors[v.field] = `Min: ${v.min}`;
162
+ if (v.max !== void 0 && value > v.max) errors[v.field] = `Max: ${v.max}`;
163
+ break;
164
+ case "boolean":
165
+ if (typeof value !== "boolean") errors[v.field] = "Must be a boolean";
166
+ break;
167
+ case "email":
168
+ if (typeof value !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
169
+ errors[v.field] = "Invalid email";
170
+ break;
171
+ case "url":
172
+ try {
173
+ new URL(value);
174
+ } catch {
175
+ errors[v.field] = "Invalid URL";
176
+ }
177
+ break;
178
+ case "uuid":
179
+ if (typeof value !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value))
180
+ errors[v.field] = "Invalid UUID";
181
+ break;
182
+ }
183
+ }
184
+ if (Object.keys(errors).length > 0) {
185
+ throw new ValidationError(errors);
186
+ }
187
+ }
188
+ function auditLogger(db) {
189
+ const AUDIT_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
190
+ return async (c, next) => {
191
+ await next();
192
+ if (!AUDIT_METHODS.has(c.req.method)) return;
193
+ if (c.res.status >= 400) return;
194
+ try {
195
+ const userId = c.get("userId") || "anonymous";
196
+ const path = c.req.path;
197
+ const method = c.req.method;
198
+ const segments = path.split("/").filter(Boolean);
199
+ const resource = segments[segments.length - 2] || segments[segments.length - 1] || "unknown";
200
+ const actionMap = {
201
+ POST: "create",
202
+ PUT: "update",
203
+ PATCH: "update",
204
+ DELETE: "delete"
205
+ };
206
+ const action = `${resource}.${actionMap[method] || method.toLowerCase()}`;
207
+ const userEmail = c.get("userEmail") || void 0;
208
+ const userRole = c.get("userRole") || void 0;
209
+ await db.logEvent({
210
+ actor: userId,
211
+ actorType: "user",
212
+ action,
213
+ resource: path,
214
+ details: {
215
+ ...userEmail ? { email: userEmail } : {},
216
+ ...userRole ? { role: userRole } : {},
217
+ method
218
+ },
219
+ ip: c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || c.req.header("x-real-ip")
220
+ });
221
+ } catch {
222
+ }
223
+ };
224
+ }
225
+ var ROLE_HIERARCHY = {
226
+ viewer: 0,
227
+ member: 1,
228
+ admin: 2,
229
+ owner: 3
230
+ };
231
+ function requireRole(minRole) {
232
+ return async (c, next) => {
233
+ const userRole = c.get("userRole");
234
+ if (c.get("authType") === "api-key") {
235
+ return next();
236
+ }
237
+ if (!userRole || ROLE_HIERARCHY[userRole] < ROLE_HIERARCHY[minRole]) {
238
+ return c.json({
239
+ error: "Insufficient permissions",
240
+ required: minRole,
241
+ current: userRole || "none"
242
+ }, 403);
243
+ }
244
+ return next();
245
+ };
246
+ }
247
+
248
+ // src/admin/routes.ts
249
+ function createAdminRoutes(db) {
250
+ const api = new Hono();
251
+ api.get("/stats", async (c) => {
252
+ const stats = await db.getStats();
253
+ return c.json(stats);
254
+ });
255
+ api.get("/agents", async (c) => {
256
+ const status = c.req.query("status");
257
+ const limit = Math.min(parseInt(c.req.query("limit") || "50"), 200);
258
+ const offset = Math.max(parseInt(c.req.query("offset") || "0"), 0);
259
+ const agents = await db.listAgents({ status, limit, offset });
260
+ const total = await db.countAgents(status);
261
+ return c.json({ agents, total, limit, offset });
262
+ });
263
+ api.get("/agents/:id", async (c) => {
264
+ const agent = await db.getAgent(c.req.param("id"));
265
+ if (!agent) return c.json({ error: "Agent not found" }, 404);
266
+ return c.json(agent);
267
+ });
268
+ api.post("/agents", async (c) => {
269
+ const body = await c.req.json();
270
+ validate(body, [
271
+ { field: "name", type: "string", required: true, minLength: 1, maxLength: 64, pattern: /^[a-zA-Z0-9_-]+$/ },
272
+ { field: "email", type: "email" },
273
+ { field: "role", type: "string", maxLength: 32 }
274
+ ]);
275
+ const existing = await db.getAgentByName(body.name);
276
+ if (existing) {
277
+ return c.json({ error: "Agent name already exists" }, 409);
278
+ }
279
+ const userId = c.get("userId") || "system";
280
+ const agent = await db.createAgent({ ...body, createdBy: userId });
281
+ return c.json(agent, 201);
282
+ });
283
+ api.patch("/agents/:id", async (c) => {
284
+ const id = c.req.param("id");
285
+ const existing = await db.getAgent(id);
286
+ if (!existing) return c.json({ error: "Agent not found" }, 404);
287
+ const body = await c.req.json();
288
+ validate(body, [
289
+ { field: "name", type: "string", minLength: 1, maxLength: 64 },
290
+ { field: "email", type: "email" },
291
+ { field: "role", type: "string", maxLength: 32 },
292
+ { field: "status", type: "string", pattern: /^(active|archived|suspended)$/ }
293
+ ]);
294
+ if (body.name && body.name !== existing.name) {
295
+ const conflict = await db.getAgentByName(body.name);
296
+ if (conflict) return c.json({ error: "Agent name already exists" }, 409);
297
+ }
298
+ const agent = await db.updateAgent(id, body);
299
+ return c.json(agent);
300
+ });
301
+ api.post("/agents/:id/archive", async (c) => {
302
+ const existing = await db.getAgent(c.req.param("id"));
303
+ if (!existing) return c.json({ error: "Agent not found" }, 404);
304
+ if (existing.status === "archived") return c.json({ error: "Agent already archived" }, 400);
305
+ await db.archiveAgent(c.req.param("id"));
306
+ return c.json({ ok: true, status: "archived" });
307
+ });
308
+ api.post("/agents/:id/restore", async (c) => {
309
+ const existing = await db.getAgent(c.req.param("id"));
310
+ if (!existing) return c.json({ error: "Agent not found" }, 404);
311
+ if (existing.status !== "archived") return c.json({ error: "Agent is not archived" }, 400);
312
+ await db.updateAgent(c.req.param("id"), { status: "active" });
313
+ return c.json({ ok: true, status: "active" });
314
+ });
315
+ api.delete("/agents/:id", requireRole("admin"), async (c) => {
316
+ const existing = await db.getAgent(c.req.param("id"));
317
+ if (!existing) return c.json({ error: "Agent not found" }, 404);
318
+ await db.deleteAgent(c.req.param("id"));
319
+ return c.json({ ok: true });
320
+ });
321
+ api.get("/users", requireRole("admin"), async (c) => {
322
+ const limit = Math.min(parseInt(c.req.query("limit") || "50"), 200);
323
+ const offset = Math.max(parseInt(c.req.query("offset") || "0"), 0);
324
+ const users = await db.listUsers({ limit, offset });
325
+ const safe = users.map(({ passwordHash, ...u }) => u);
326
+ return c.json({ users: safe, limit, offset });
327
+ });
328
+ api.post("/users", requireRole("admin"), async (c) => {
329
+ const body = await c.req.json();
330
+ validate(body, [
331
+ { field: "email", type: "email", required: true },
332
+ { field: "name", type: "string", required: true, minLength: 1, maxLength: 128 },
333
+ { field: "role", type: "string", required: true, pattern: /^(owner|admin|member|viewer)$/ },
334
+ { field: "password", type: "string", minLength: 8, maxLength: 128 }
335
+ ]);
336
+ const existing = await db.getUserByEmail(body.email);
337
+ if (existing) return c.json({ error: "Email already registered" }, 409);
338
+ const user = await db.createUser(body);
339
+ const { passwordHash, ...safe } = user;
340
+ return c.json(safe, 201);
341
+ });
342
+ api.patch("/users/:id", requireRole("admin"), async (c) => {
343
+ const existing = await db.getUser(c.req.param("id"));
344
+ if (!existing) return c.json({ error: "User not found" }, 404);
345
+ const body = await c.req.json();
346
+ validate(body, [
347
+ { field: "email", type: "email" },
348
+ { field: "name", type: "string", minLength: 1, maxLength: 128 },
349
+ { field: "role", type: "string", pattern: /^(owner|admin|member|viewer)$/ }
350
+ ]);
351
+ const user = await db.updateUser(c.req.param("id"), body);
352
+ const { passwordHash, ...safe } = user;
353
+ return c.json(safe);
354
+ });
355
+ api.delete("/users/:id", requireRole("owner"), async (c) => {
356
+ const existing = await db.getUser(c.req.param("id"));
357
+ if (!existing) return c.json({ error: "User not found" }, 404);
358
+ const requesterId = c.get("userId");
359
+ if (requesterId === c.req.param("id")) {
360
+ return c.json({ error: "Cannot delete your own account" }, 400);
361
+ }
362
+ await db.deleteUser(c.req.param("id"));
363
+ return c.json({ ok: true });
364
+ });
365
+ api.get("/audit", requireRole("admin"), async (c) => {
366
+ const filters = {
367
+ actor: c.req.query("actor") || void 0,
368
+ action: c.req.query("action") || void 0,
369
+ resource: c.req.query("resource") || void 0,
370
+ from: c.req.query("from") ? new Date(c.req.query("from")) : void 0,
371
+ to: c.req.query("to") ? new Date(c.req.query("to")) : void 0,
372
+ limit: Math.min(parseInt(c.req.query("limit") || "50"), 500),
373
+ offset: Math.max(parseInt(c.req.query("offset") || "0"), 0)
374
+ };
375
+ if (filters.from && isNaN(filters.from.getTime())) {
376
+ return c.json({ error: 'Invalid "from" date' }, 400);
377
+ }
378
+ if (filters.to && isNaN(filters.to.getTime())) {
379
+ return c.json({ error: 'Invalid "to" date' }, 400);
380
+ }
381
+ const result = await db.queryAudit(filters);
382
+ return c.json(result);
383
+ });
384
+ api.get("/api-keys", requireRole("admin"), async (c) => {
385
+ const keys = await db.listApiKeys();
386
+ const safe = keys.map(({ keyHash, ...k }) => k);
387
+ return c.json({ keys: safe });
388
+ });
389
+ api.post("/api-keys", requireRole("admin"), async (c) => {
390
+ const body = await c.req.json();
391
+ validate(body, [
392
+ { field: "name", type: "string", required: true, minLength: 1, maxLength: 64 }
393
+ ]);
394
+ const userId = c.get("userId") || "system";
395
+ const scopes = Array.isArray(body.scopes) ? body.scopes : ["*"];
396
+ const expiresAt = body.expiresAt ? new Date(body.expiresAt) : void 0;
397
+ const { key, plaintext } = await db.createApiKey({
398
+ name: body.name,
399
+ scopes,
400
+ createdBy: userId,
401
+ expiresAt
402
+ });
403
+ const { keyHash, ...safeKey } = key;
404
+ return c.json({
405
+ key: safeKey,
406
+ plaintext,
407
+ warning: "Store this key securely. It will not be shown again."
408
+ }, 201);
409
+ });
410
+ api.delete("/api-keys/:id", requireRole("admin"), async (c) => {
411
+ const existing = await db.getApiKey(c.req.param("id"));
412
+ if (!existing) return c.json({ error: "API key not found" }, 404);
413
+ await db.revokeApiKey(c.req.param("id"));
414
+ return c.json({ ok: true, revoked: true });
415
+ });
416
+ api.get("/rules", async (c) => {
417
+ const agentId = c.req.query("agentId") || void 0;
418
+ const rules = await db.getRules(agentId);
419
+ return c.json({ rules });
420
+ });
421
+ api.post("/rules", async (c) => {
422
+ const body = await c.req.json();
423
+ validate(body, [
424
+ { field: "name", type: "string", required: true, minLength: 1, maxLength: 128 }
425
+ ]);
426
+ if (body.conditions && typeof body.conditions !== "object") {
427
+ return c.json({ error: "conditions must be an object" }, 400);
428
+ }
429
+ if (body.actions && typeof body.actions !== "object") {
430
+ return c.json({ error: "actions must be an object" }, 400);
431
+ }
432
+ const rule = await db.createRule({
433
+ name: body.name,
434
+ agentId: body.agentId,
435
+ conditions: body.conditions || {},
436
+ actions: body.actions || {},
437
+ priority: body.priority ?? 0,
438
+ enabled: body.enabled ?? true
439
+ });
440
+ return c.json(rule, 201);
441
+ });
442
+ api.patch("/rules/:id", async (c) => {
443
+ const body = await c.req.json();
444
+ const rule = await db.updateRule(c.req.param("id"), body);
445
+ return c.json(rule);
446
+ });
447
+ api.delete("/rules/:id", async (c) => {
448
+ await db.deleteRule(c.req.param("id"));
449
+ return c.json({ ok: true });
450
+ });
451
+ api.get("/settings", async (c) => {
452
+ const settings = await db.getSettings();
453
+ if (!settings) return c.json({ error: "Not configured" }, 404);
454
+ const safe = { ...settings };
455
+ if (safe.smtpPass) safe.smtpPass = "***";
456
+ if (safe.dkimPrivateKey) safe.dkimPrivateKey = "***";
457
+ if (safe.ssoConfig?.oidc?.clientSecret) {
458
+ safe.ssoConfig = { ...safe.ssoConfig, oidc: { ...safe.ssoConfig.oidc, clientSecret: "***" } };
459
+ }
460
+ return c.json(safe);
461
+ });
462
+ api.patch("/settings", requireRole("admin"), async (c) => {
463
+ const body = await c.req.json();
464
+ validate(body, [
465
+ { field: "name", type: "string", minLength: 1, maxLength: 128 },
466
+ { field: "domain", type: "string", maxLength: 253 },
467
+ { field: "subdomain", type: "string", maxLength: 64 },
468
+ { field: "primaryColor", type: "string", pattern: /^#[0-9a-fA-F]{6}$/ },
469
+ { field: "logoUrl", type: "url" },
470
+ { field: "smtpHost", type: "string", maxLength: 253 },
471
+ { field: "smtpPort", type: "number" },
472
+ { field: "smtpUser", type: "string", maxLength: 253 },
473
+ { field: "smtpPass", type: "string", maxLength: 253 },
474
+ { field: "dkimPrivateKey", type: "string" },
475
+ { field: "plan", type: "string", maxLength: 32 }
476
+ ]);
477
+ const settings = await db.updateSettings(body);
478
+ return c.json(settings);
479
+ });
480
+ api.get("/settings/sso", requireRole("admin"), async (c) => {
481
+ const settings = await db.getSettings();
482
+ if (!settings) return c.json({ ssoConfig: null });
483
+ const sso = settings.ssoConfig || {};
484
+ const safe = { ...sso };
485
+ if (safe.oidc?.clientSecret) {
486
+ safe.oidc = { ...safe.oidc, clientSecret: "***" };
487
+ }
488
+ if (safe.saml?.certificate) {
489
+ const cert = safe.saml.certificate;
490
+ safe.saml = {
491
+ ...safe.saml,
492
+ certificate: cert.length > 50 ? cert.substring(0, 20) + "..." + cert.substring(cert.length - 20) : cert,
493
+ certificateConfigured: true
494
+ };
495
+ }
496
+ return c.json({ ssoConfig: safe });
497
+ });
498
+ api.put("/settings/sso/saml", requireRole("admin"), async (c) => {
499
+ const body = await c.req.json();
500
+ validate(body, [
501
+ { field: "entityId", type: "string", required: true, minLength: 1, maxLength: 512 },
502
+ { field: "ssoUrl", type: "url", required: true },
503
+ { field: "certificate", type: "string", required: true, minLength: 10 }
504
+ ]);
505
+ const settings = await db.getSettings();
506
+ const current = settings?.ssoConfig || {};
507
+ const ssoConfig = {
508
+ ...current,
509
+ saml: {
510
+ entityId: body.entityId,
511
+ ssoUrl: body.ssoUrl,
512
+ certificate: body.certificate,
513
+ signatureAlgorithm: body.signatureAlgorithm || "RSA-SHA256",
514
+ autoProvision: body.autoProvision ?? true,
515
+ defaultRole: body.defaultRole || "member",
516
+ allowedDomains: body.allowedDomains || []
517
+ }
518
+ };
519
+ await db.updateSettings({ ssoConfig });
520
+ return c.json({ ok: true, provider: "saml", configured: true });
521
+ });
522
+ api.put("/settings/sso/oidc", requireRole("admin"), async (c) => {
523
+ const body = await c.req.json();
524
+ validate(body, [
525
+ { field: "clientId", type: "string", required: true, minLength: 1, maxLength: 256 },
526
+ { field: "clientSecret", type: "string", required: true, minLength: 1, maxLength: 512 },
527
+ { field: "discoveryUrl", type: "url", required: true }
528
+ ]);
529
+ const settings = await db.getSettings();
530
+ const current = settings?.ssoConfig || {};
531
+ let clientSecret = body.clientSecret;
532
+ if (clientSecret === "***" && current.oidc?.clientSecret) {
533
+ clientSecret = current.oidc.clientSecret;
534
+ }
535
+ const ssoConfig = {
536
+ ...current,
537
+ oidc: {
538
+ clientId: body.clientId,
539
+ clientSecret,
540
+ discoveryUrl: body.discoveryUrl,
541
+ scopes: body.scopes || ["openid", "email", "profile"],
542
+ autoProvision: body.autoProvision ?? true,
543
+ defaultRole: body.defaultRole || "member",
544
+ allowedDomains: body.allowedDomains || []
545
+ }
546
+ };
547
+ await db.updateSettings({ ssoConfig });
548
+ return c.json({ ok: true, provider: "oidc", configured: true });
549
+ });
550
+ api.delete("/settings/sso/:provider", requireRole("admin"), async (c) => {
551
+ const provider = c.req.param("provider");
552
+ if (provider !== "saml" && provider !== "oidc") {
553
+ return c.json({ error: 'Invalid provider. Use "saml" or "oidc".' }, 400);
554
+ }
555
+ const settings = await db.getSettings();
556
+ const current = settings?.ssoConfig || {};
557
+ const ssoConfig = { ...current };
558
+ delete ssoConfig[provider];
559
+ await db.updateSettings({ ssoConfig });
560
+ return c.json({ ok: true, provider, removed: true });
561
+ });
562
+ api.post("/settings/sso/oidc/test", requireRole("admin"), async (c) => {
563
+ const { discoveryUrl } = await c.req.json();
564
+ if (!discoveryUrl) return c.json({ error: "discoveryUrl required" }, 400);
565
+ try {
566
+ const res = await fetch(discoveryUrl);
567
+ if (!res.ok) return c.json({ ok: false, error: `HTTP ${res.status}` });
568
+ const doc = await res.json();
569
+ return c.json({
570
+ ok: true,
571
+ issuer: doc.issuer,
572
+ hasAuthorizationEndpoint: !!doc.authorization_endpoint,
573
+ hasTokenEndpoint: !!doc.token_endpoint,
574
+ hasUserinfoEndpoint: !!doc.userinfo_endpoint,
575
+ hasJwksUri: !!doc.jwks_uri,
576
+ supportedScopes: doc.scopes_supported
577
+ });
578
+ } catch (e) {
579
+ return c.json({ ok: false, error: e.message });
580
+ }
581
+ });
582
+ api.get("/settings/tool-security", requireRole("admin"), async (c) => {
583
+ const settings = await db.getSettings();
584
+ return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });
585
+ });
586
+ api.put("/settings/tool-security", requireRole("admin"), async (c) => {
587
+ const body = await c.req.json();
588
+ if (body && typeof body !== "object") {
589
+ return c.json({ error: "Body must be a JSON object" }, 400);
590
+ }
591
+ await db.updateSettings({ toolSecurityConfig: body });
592
+ const settings = await db.getSettings();
593
+ return c.json({ toolSecurityConfig: settings?.toolSecurityConfig || {} });
594
+ });
595
+ api.get("/settings/firewall", requireRole("admin"), async (c) => {
596
+ const settings = await db.getSettings();
597
+ return c.json({ firewallConfig: settings?.firewallConfig || {} });
598
+ });
599
+ api.put("/settings/firewall", requireRole("admin"), async (c) => {
600
+ const body = await c.req.json();
601
+ if (body && typeof body !== "object") {
602
+ return c.json({ error: "Body must be a JSON object" }, 400);
603
+ }
604
+ if (body.ipAccess?.mode && !["allowlist", "blocklist"].includes(body.ipAccess.mode)) {
605
+ return c.json({ error: 'ipAccess.mode must be "allowlist" or "blocklist"' }, 400);
606
+ }
607
+ if (body.egress?.mode && !["allowlist", "blocklist"].includes(body.egress.mode)) {
608
+ return c.json({ error: 'egress.mode must be "allowlist" or "blocklist"' }, 400);
609
+ }
610
+ const { isValidIpOrCidr } = await import("./cidr-LISVZSM2.js");
611
+ for (const entry of body.ipAccess?.allowlist || []) {
612
+ if (!isValidIpOrCidr(entry)) return c.json({ error: "Invalid IP/CIDR in allowlist: " + entry }, 400);
613
+ }
614
+ for (const entry of body.ipAccess?.blocklist || []) {
615
+ if (!isValidIpOrCidr(entry)) return c.json({ error: "Invalid IP/CIDR in blocklist: " + entry }, 400);
616
+ }
617
+ for (const entry of body.trustedProxies?.ips || []) {
618
+ if (!isValidIpOrCidr(entry)) return c.json({ error: "Invalid IP/CIDR in trusted proxies: " + entry }, 400);
619
+ }
620
+ if (body.ipAccess?.enabled && body.ipAccess?.mode === "allowlist" && body.ipAccess?.allowlist?.length > 0) {
621
+ const clientIp = c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || c.req.header("x-real-ip") || "";
622
+ if (clientIp && clientIp !== "unknown") {
623
+ const { compileIpMatcher } = await import("./cidr-LISVZSM2.js");
624
+ const matcher = compileIpMatcher(body.ipAccess.allowlist);
625
+ if (!matcher(clientIp)) {
626
+ return c.json({ error: "Your current IP (" + clientIp + ") is not in the allowlist. Add it first to avoid lockout." }, 400);
627
+ }
628
+ }
629
+ }
630
+ await db.updateSettings({ firewallConfig: body });
631
+ try {
632
+ const { invalidateFirewallCache: invalidateFirewallCache2 } = await import("./firewall-AHIRE6UB.js");
633
+ invalidateFirewallCache2();
634
+ } catch {
635
+ }
636
+ const settings = await db.getSettings();
637
+ return c.json({ firewallConfig: settings?.firewallConfig || {} });
638
+ });
639
+ api.post("/settings/firewall/test-ip", requireRole("admin"), async (c) => {
640
+ const { ip } = await c.req.json();
641
+ if (!ip) return c.json({ error: "ip is required" }, 400);
642
+ const { isValidIpOrCidr, compileIpMatcher } = await import("./cidr-LISVZSM2.js");
643
+ if (!isValidIpOrCidr(ip)) return c.json({ error: "Invalid IP address" }, 400);
644
+ const settings = await db.getSettings();
645
+ const ipAccess = settings?.firewallConfig?.ipAccess;
646
+ if (!ipAccess?.enabled) {
647
+ return c.json({ ip, allowed: true, reason: "IP access control is disabled" });
648
+ }
649
+ if (ipAccess.mode === "allowlist") {
650
+ const matcher = compileIpMatcher(ipAccess.allowlist || []);
651
+ const allowed = matcher(ip);
652
+ return c.json({ ip, allowed, reason: allowed ? "IP matches allowlist" : "IP not in allowlist" });
653
+ } else {
654
+ const matcher = compileIpMatcher(ipAccess.blocklist || []);
655
+ const blocked = matcher(ip);
656
+ return c.json({ ip, allowed: !blocked, reason: blocked ? "IP matches blocklist" : "IP not in blocklist" });
657
+ }
658
+ });
659
+ api.get("/settings/model-pricing", requireRole("admin"), async (c) => {
660
+ const settings = await db.getSettings();
661
+ var config = settings?.modelPricingConfig || { models: [], currency: "USD" };
662
+ if (!config.models || config.models.length === 0) {
663
+ config.models = getDefaultModelPricing();
664
+ }
665
+ return c.json({ modelPricingConfig: config });
666
+ });
667
+ api.put("/settings/model-pricing", requireRole("admin"), async (c) => {
668
+ const body = await c.req.json();
669
+ if (!body || typeof body !== "object") {
670
+ return c.json({ error: "Body must be a JSON object" }, 400);
671
+ }
672
+ if (body.models && Array.isArray(body.models)) {
673
+ for (const m of body.models) {
674
+ if (!m.provider || !m.modelId) {
675
+ return c.json({ error: "Each model must have provider and modelId" }, 400);
676
+ }
677
+ if (typeof m.inputCostPerMillion !== "number" || m.inputCostPerMillion < 0) {
678
+ return c.json({ error: `Invalid inputCostPerMillion for ${m.modelId}` }, 400);
679
+ }
680
+ if (typeof m.outputCostPerMillion !== "number" || m.outputCostPerMillion < 0) {
681
+ return c.json({ error: `Invalid outputCostPerMillion for ${m.modelId}` }, 400);
682
+ }
683
+ }
684
+ }
685
+ body.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
686
+ await db.updateSettings({ modelPricingConfig: body });
687
+ const settings = await db.getSettings();
688
+ return c.json({ modelPricingConfig: settings?.modelPricingConfig || {} });
689
+ });
690
+ api.get("/providers", requireRole("admin"), async (c) => {
691
+ var builtIn = Object.values(PROVIDER_REGISTRY).map(function(p) {
692
+ var configured = !p.requiresApiKey || p.envKey && !!process.env[p.envKey];
693
+ return {
694
+ id: p.id,
695
+ name: p.name,
696
+ baseUrl: p.baseUrl,
697
+ apiType: p.apiType,
698
+ isLocal: p.isLocal,
699
+ requiresApiKey: p.requiresApiKey,
700
+ configured,
701
+ source: "built-in",
702
+ defaultModels: p.defaultModels || []
703
+ };
704
+ });
705
+ var settings = await db.getSettings();
706
+ var pricingConfig = settings?.modelPricingConfig;
707
+ var customProviders = pricingConfig?.customProviders || [];
708
+ var custom = customProviders.map(function(p) {
709
+ return { ...p, configured: true, source: "custom" };
710
+ });
711
+ return c.json({ providers: [...builtIn, ...custom] });
712
+ });
713
+ api.post("/providers", requireRole("admin"), async (c) => {
714
+ var body = await c.req.json();
715
+ if (!body.id || !body.name || !body.baseUrl || !body.apiType) {
716
+ return c.json({ error: "id, name, baseUrl, and apiType are required" }, 400);
717
+ }
718
+ if (PROVIDER_REGISTRY[body.id]) {
719
+ return c.json({ error: "Cannot override built-in provider" }, 409);
720
+ }
721
+ var validTypes = ["anthropic", "openai-compatible", "google", "ollama"];
722
+ if (!validTypes.includes(body.apiType)) {
723
+ return c.json({ error: "apiType must be one of: " + validTypes.join(", ") }, 400);
724
+ }
725
+ var settings = await db.getSettings();
726
+ var config = settings?.modelPricingConfig || { models: [], currency: "USD" };
727
+ config.customProviders = config.customProviders || [];
728
+ if (config.customProviders.find(function(p) {
729
+ return p.id === body.id;
730
+ })) {
731
+ return c.json({ error: "Custom provider with this ID already exists" }, 409);
732
+ }
733
+ config.customProviders.push({
734
+ id: body.id,
735
+ name: body.name,
736
+ baseUrl: body.baseUrl,
737
+ apiType: body.apiType,
738
+ apiKeyEnvVar: body.apiKeyEnvVar || "",
739
+ headers: body.headers || {},
740
+ models: body.models || []
741
+ });
742
+ await db.updateSettings({ modelPricingConfig: config });
743
+ return c.json({ ok: true, provider: body });
744
+ });
745
+ api.put("/providers/:id", requireRole("admin"), async (c) => {
746
+ var id = c.req.param("id");
747
+ if (PROVIDER_REGISTRY[id]) {
748
+ return c.json({ error: "Cannot modify built-in provider" }, 400);
749
+ }
750
+ var body = await c.req.json();
751
+ var settings = await db.getSettings();
752
+ var config = settings?.modelPricingConfig || { models: [], currency: "USD" };
753
+ config.customProviders = config.customProviders || [];
754
+ var idx = config.customProviders.findIndex(function(p) {
755
+ return p.id === id;
756
+ });
757
+ if (idx === -1) {
758
+ return c.json({ error: "Custom provider not found" }, 404);
759
+ }
760
+ config.customProviders[idx] = Object.assign({}, config.customProviders[idx], body, { id });
761
+ await db.updateSettings({ modelPricingConfig: config });
762
+ return c.json({ ok: true, provider: config.customProviders[idx] });
763
+ });
764
+ api.delete("/providers/:id", requireRole("admin"), async (c) => {
765
+ var id = c.req.param("id");
766
+ if (PROVIDER_REGISTRY[id]) {
767
+ return c.json({ error: "Cannot delete built-in provider" }, 400);
768
+ }
769
+ var settings = await db.getSettings();
770
+ var config = settings?.modelPricingConfig || { models: [], currency: "USD" };
771
+ config.customProviders = config.customProviders || [];
772
+ var before = config.customProviders.length;
773
+ config.customProviders = config.customProviders.filter(function(p) {
774
+ return p.id !== id;
775
+ });
776
+ if (config.customProviders.length === before) {
777
+ return c.json({ error: "Custom provider not found" }, 404);
778
+ }
779
+ await db.updateSettings({ modelPricingConfig: config });
780
+ return c.json({ ok: true });
781
+ });
782
+ api.get("/providers/:id/models", requireRole("admin"), async (c) => {
783
+ var id = c.req.param("id");
784
+ var provider = PROVIDER_REGISTRY[id];
785
+ if (id === "ollama" || provider && provider.apiType === "ollama") {
786
+ var ollamaHost = process.env.OLLAMA_HOST || (provider ? provider.baseUrl : "http://localhost:11434");
787
+ try {
788
+ var resp = await fetch(ollamaHost + "/api/tags");
789
+ var data = await resp.json();
790
+ return c.json({ models: (data.models || []).map(function(m) {
791
+ return { id: m.name, name: m.name, size: m.size };
792
+ }) });
793
+ } catch (err) {
794
+ return c.json({ error: "Cannot connect to Ollama: " + err.message }, 502);
795
+ }
796
+ }
797
+ if (provider && provider.isLocal && provider.apiType === "openai-compatible") {
798
+ try {
799
+ var resp = await fetch(provider.baseUrl + "/models");
800
+ var data = await resp.json();
801
+ return c.json({ models: (data.data || []).map(function(m) {
802
+ return { id: m.id, name: m.id };
803
+ }) });
804
+ } catch (err) {
805
+ return c.json({ error: "Cannot connect to " + provider.name + ": " + err.message }, 502);
806
+ }
807
+ }
808
+ if (provider && provider.defaultModels) {
809
+ return c.json({ models: provider.defaultModels.map(function(mid) {
810
+ return { id: mid, name: mid };
811
+ }) });
812
+ }
813
+ var settings = await db.getSettings();
814
+ var pricingConfig = settings?.modelPricingConfig;
815
+ var customProviders = pricingConfig?.customProviders || [];
816
+ var customProvider = customProviders.find(function(p) {
817
+ return p.id === id;
818
+ });
819
+ if (customProvider && customProvider.models) {
820
+ return c.json({ models: customProvider.models });
821
+ }
822
+ return c.json({ models: [] });
823
+ });
824
+ api.get("/retention", requireRole("admin"), async (c) => {
825
+ const policy = await db.getRetentionPolicy();
826
+ return c.json(policy);
827
+ });
828
+ api.put("/retention", requireRole("owner"), async (c) => {
829
+ const body = await c.req.json();
830
+ validate(body, [
831
+ { field: "enabled", type: "boolean", required: true },
832
+ { field: "retainDays", type: "number", required: true, min: 1, max: 3650 },
833
+ { field: "archiveFirst", type: "boolean" }
834
+ ]);
835
+ await db.setRetentionPolicy({
836
+ enabled: body.enabled,
837
+ retainDays: body.retainDays,
838
+ excludeTags: body.excludeTags || [],
839
+ archiveFirst: body.archiveFirst ?? true
840
+ });
841
+ return c.json({ ok: true });
842
+ });
843
+ api.post("/domain/register", requireRole("admin"), async (c) => {
844
+ var body = await c.req.json();
845
+ if (!body.domain) {
846
+ return c.json({ error: "domain is required" }, 400);
847
+ }
848
+ var domain = String(body.domain).toLowerCase().trim();
849
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(domain)) {
850
+ return c.json({ error: "Invalid domain format" }, 400);
851
+ }
852
+ try {
853
+ var { DomainLock } = await import("./domain-lock-URIFILHB.js");
854
+ var lock = new DomainLock();
855
+ var keyPair = await lock.generateDeploymentKey();
856
+ var settings = await db.getSettings();
857
+ var result = await lock.register(domain, keyPair.hash, {
858
+ orgName: settings?.name,
859
+ contactEmail: body.contactEmail
860
+ });
861
+ if (!result.success) {
862
+ return c.json({ error: result.error, statusCode: result.statusCode }, 400);
863
+ }
864
+ await db.updateSettings({
865
+ domain,
866
+ deploymentKeyHash: keyPair.hash,
867
+ domainRegistrationId: result.registrationId,
868
+ domainDnsChallenge: result.dnsChallenge,
869
+ domainRegisteredAt: (/* @__PURE__ */ new Date()).toISOString(),
870
+ domainStatus: "pending_dns"
871
+ });
872
+ return c.json({
873
+ deploymentKey: keyPair.plaintext,
874
+ dnsChallenge: result.dnsChallenge,
875
+ registrationId: result.registrationId
876
+ });
877
+ } catch (err) {
878
+ return c.json({ error: err.message || "Domain registration failed" }, 500);
879
+ }
880
+ });
881
+ api.post("/domain/verify", requireRole("admin"), async (c) => {
882
+ var body = await c.req.json();
883
+ if (!body.domain) {
884
+ return c.json({ error: "domain is required" }, 400);
885
+ }
886
+ var domain = String(body.domain).toLowerCase().trim();
887
+ try {
888
+ var { DomainLock } = await import("./domain-lock-URIFILHB.js");
889
+ var lock = new DomainLock();
890
+ var result = await lock.checkVerification(domain);
891
+ if (result.verified) {
892
+ await db.updateSettings({
893
+ domainStatus: "verified",
894
+ domainVerifiedAt: (/* @__PURE__ */ new Date()).toISOString()
895
+ });
896
+ return c.json({ verified: true });
897
+ }
898
+ return c.json({ verified: false, error: result.error });
899
+ } catch (err) {
900
+ return c.json({ error: err.message || "Verification check failed" }, 500);
901
+ }
902
+ });
903
+ function getDefaultModelPricing() {
904
+ return [
905
+ // Anthropic (Feb 2026 — 1M context window)
906
+ { provider: "anthropic", modelId: "claude-opus-4-6", displayName: "Claude Opus 4.6", inputCostPerMillion: 5, outputCostPerMillion: 25, contextWindow: 1e6 },
907
+ { provider: "anthropic", modelId: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6", inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1e6 },
908
+ { provider: "anthropic", modelId: "claude-sonnet-4-5-20250929", displayName: "Claude Sonnet 4.5", inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 1e6 },
909
+ { provider: "anthropic", modelId: "claude-haiku-4-5-20251001", displayName: "Claude Haiku 4.5", inputCostPerMillion: 0.8, outputCostPerMillion: 4, contextWindow: 2e5 },
910
+ // OpenAI
911
+ { provider: "openai", modelId: "gpt-4o", displayName: "GPT-4o", inputCostPerMillion: 2.5, outputCostPerMillion: 10, contextWindow: 128e3 },
912
+ { provider: "openai", modelId: "gpt-4o-mini", displayName: "GPT-4o Mini", inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 128e3 },
913
+ { provider: "openai", modelId: "gpt-4.1", displayName: "GPT-4.1", inputCostPerMillion: 2, outputCostPerMillion: 8, contextWindow: 1e6 },
914
+ { provider: "openai", modelId: "gpt-4.1-mini", displayName: "GPT-4.1 Mini", inputCostPerMillion: 0.4, outputCostPerMillion: 1.6, contextWindow: 1e6 },
915
+ { provider: "openai", modelId: "gpt-4.1-nano", displayName: "GPT-4.1 Nano", inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1e6 },
916
+ { provider: "openai", modelId: "o3", displayName: "o3", inputCostPerMillion: 10, outputCostPerMillion: 40, contextWindow: 2e5 },
917
+ { provider: "openai", modelId: "o4-mini", displayName: "o4-mini", inputCostPerMillion: 1.1, outputCostPerMillion: 4.4, contextWindow: 2e5 },
918
+ // Google Gemini (up to 2M context)
919
+ { provider: "google", modelId: "gemini-2.5-pro", displayName: "Gemini 2.5 Pro", inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1e6 },
920
+ { provider: "google", modelId: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", inputCostPerMillion: 0.15, outputCostPerMillion: 0.6, contextWindow: 1e6 },
921
+ { provider: "google", modelId: "gemini-2.0-flash", displayName: "Gemini 2.0 Flash", inputCostPerMillion: 0.1, outputCostPerMillion: 0.4, contextWindow: 1e6 },
922
+ { provider: "google", modelId: "gemini-3-pro", displayName: "Gemini 3 Pro", inputCostPerMillion: 2.5, outputCostPerMillion: 15, contextWindow: 1e6 },
923
+ // DeepSeek (128K context)
924
+ { provider: "deepseek", modelId: "deepseek-chat", displayName: "DeepSeek Chat (V3)", inputCostPerMillion: 0.14, outputCostPerMillion: 0.28, contextWindow: 128e3 },
925
+ { provider: "deepseek", modelId: "deepseek-reasoner", displayName: "DeepSeek Reasoner (R1)", inputCostPerMillion: 0.55, outputCostPerMillion: 2.19, contextWindow: 128e3 },
926
+ // xAI Grok (2M context window)
927
+ { provider: "xai", modelId: "grok-4", displayName: "Grok 4", inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 2e6 },
928
+ { provider: "xai", modelId: "grok-4-fast", displayName: "Grok 4 Fast", inputCostPerMillion: 0.2, outputCostPerMillion: 0.5, contextWindow: 2e6 },
929
+ { provider: "xai", modelId: "grok-3", displayName: "Grok 3", inputCostPerMillion: 3, outputCostPerMillion: 15, contextWindow: 131072 },
930
+ { provider: "xai", modelId: "grok-3-mini", displayName: "Grok 3 Mini", inputCostPerMillion: 0.3, outputCostPerMillion: 0.5, contextWindow: 131072 },
931
+ // Mistral
932
+ { provider: "mistral", modelId: "mistral-large-latest", displayName: "Mistral Large", inputCostPerMillion: 2, outputCostPerMillion: 6, contextWindow: 128e3 },
933
+ { provider: "mistral", modelId: "mistral-small-latest", displayName: "Mistral Small", inputCostPerMillion: 0.1, outputCostPerMillion: 0.3, contextWindow: 128e3 },
934
+ // Groq (inference provider)
935
+ { provider: "groq", modelId: "llama-3.3-70b-versatile", displayName: "Llama 3.3 70B (Groq)", inputCostPerMillion: 0.59, outputCostPerMillion: 0.79, contextWindow: 128e3 },
936
+ // Together (inference provider)
937
+ { provider: "together", modelId: "meta-llama/Llama-3.3-70B-Instruct-Turbo", displayName: "Llama 3.3 70B (Together)", inputCostPerMillion: 0.88, outputCostPerMillion: 0.88, contextWindow: 128e3 }
938
+ ];
939
+ }
940
+ return api;
941
+ }
942
+
943
+ // src/auth/routes.ts
944
+ import { Hono as Hono2 } from "hono";
945
+ import { setCookie, getCookie, deleteCookie } from "hono/cookie";
946
+ import { createVerify } from "crypto";
947
+ var COOKIE_NAME = "em_session";
948
+ var REFRESH_COOKIE = "em_refresh";
949
+ var CSRF_COOKIE = "em_csrf";
950
+ var TOKEN_TTL = "24h";
951
+ var REFRESH_TTL = "7d";
952
+ function cookieOpts(maxAge, isSecure) {
953
+ return {
954
+ httpOnly: true,
955
+ secure: isSecure,
956
+ sameSite: "Lax",
957
+ path: "/",
958
+ maxAge
959
+ };
960
+ }
961
+ function generateCsrf() {
962
+ const bytes = new Uint8Array(32);
963
+ crypto.getRandomValues(bytes);
964
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
965
+ }
966
+ function generateState() {
967
+ const bytes = new Uint8Array(32);
968
+ crypto.getRandomValues(bytes);
969
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
970
+ }
971
+ function generateCodeVerifier() {
972
+ const bytes = new Uint8Array(32);
973
+ crypto.getRandomValues(bytes);
974
+ return Array.from(bytes).map((b) => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"[b % 66]).join("");
975
+ }
976
+ async function generateCodeChallenge(verifier) {
977
+ const encoder = new TextEncoder();
978
+ const data = encoder.encode(verifier);
979
+ const digest = await crypto.subtle.digest("SHA-256", data);
980
+ return btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
981
+ }
982
+ function createAuthRoutes(db, jwtSecret, opts) {
983
+ const auth = new Hono2();
984
+ const isSecure = () => {
985
+ return process.env.NODE_ENV === "production" || process.env.SECURE_COOKIES === "1";
986
+ };
987
+ async function issueTokens(userId, email, role) {
988
+ const { SignJWT } = await import("jose");
989
+ const secret = new TextEncoder().encode(jwtSecret);
990
+ const token = await new SignJWT({ sub: userId, email, role }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(TOKEN_TTL).sign(secret);
991
+ const refreshToken = await new SignJWT({ sub: userId, type: "refresh" }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(REFRESH_TTL).sign(secret);
992
+ return { token, refreshToken };
993
+ }
994
+ async function setSessionCookies(c, userId, email, role, method) {
995
+ const { token, refreshToken } = await issueTokens(userId, email, role);
996
+ const csrf = generateCsrf();
997
+ const secure = isSecure();
998
+ setCookie(c, COOKIE_NAME, token, cookieOpts(86400, secure));
999
+ setCookie(c, REFRESH_COOKIE, refreshToken, cookieOpts(604800, secure));
1000
+ setCookie(c, CSRF_COOKIE, csrf, { ...cookieOpts(86400, secure), httpOnly: false });
1001
+ await db.updateUser(userId, { lastLoginAt: /* @__PURE__ */ new Date() }).catch(() => {
1002
+ });
1003
+ await db.logEvent({
1004
+ actor: userId,
1005
+ actorType: "user",
1006
+ action: "auth.login",
1007
+ resource: `user:${userId}`,
1008
+ details: { method },
1009
+ ip: c.req.header("x-forwarded-for") || c.req.header("x-real-ip")
1010
+ }).catch(() => {
1011
+ });
1012
+ return { token, refreshToken, csrf };
1013
+ }
1014
+ async function findOrProvisionSsoUser(provider, subject, email, name, config) {
1015
+ if (config.allowedDomains?.length) {
1016
+ const domain = email.split("@")[1]?.toLowerCase();
1017
+ if (!config.allowedDomains.some((d) => d.toLowerCase() === domain)) {
1018
+ return { error: `Email domain "${domain}" not allowed for SSO login` };
1019
+ }
1020
+ }
1021
+ let user = await db.getUserBySso(provider, subject);
1022
+ if (user) return { user };
1023
+ user = await db.getUserByEmail(email);
1024
+ if (user) {
1025
+ await db.updateUser(user.id, { ssoProvider: provider, ssoSubject: subject });
1026
+ return { user };
1027
+ }
1028
+ if (!config.autoProvision) {
1029
+ return { error: "No account found. Contact your administrator to create an account." };
1030
+ }
1031
+ const newUser = await db.createUser({
1032
+ email,
1033
+ name: name || email.split("@")[0],
1034
+ role: config.defaultRole || "member",
1035
+ ssoProvider: provider,
1036
+ ssoSubject: subject
1037
+ });
1038
+ return { user: newUser };
1039
+ }
1040
+ async function extractToken(c) {
1041
+ const cookieToken = getCookie(c, COOKIE_NAME);
1042
+ if (cookieToken) return cookieToken;
1043
+ const authHeader = c.req.header("Authorization");
1044
+ if (authHeader?.startsWith("Bearer ")) return authHeader.slice(7);
1045
+ return null;
1046
+ }
1047
+ async function getSsoConfig() {
1048
+ try {
1049
+ const settings = await db.getSettings();
1050
+ return settings?.ssoConfig || null;
1051
+ } catch {
1052
+ return null;
1053
+ }
1054
+ }
1055
+ auth.post("/login", async (c) => {
1056
+ const { email, password } = await c.req.json();
1057
+ if (!email || !password) {
1058
+ return c.json({ error: "Email and password required" }, 400);
1059
+ }
1060
+ const user = await db.getUserByEmail(email);
1061
+ if (!user || !user.passwordHash) {
1062
+ return c.json({ error: "Invalid credentials" }, 401);
1063
+ }
1064
+ const { default: bcrypt } = await import("bcryptjs");
1065
+ const valid = await bcrypt.compare(password, user.passwordHash);
1066
+ if (!valid) {
1067
+ return c.json({ error: "Invalid credentials" }, 401);
1068
+ }
1069
+ const { token, refreshToken, csrf } = await setSessionCookies(c, user.id, user.email, user.role, "password");
1070
+ return c.json({
1071
+ token,
1072
+ refreshToken,
1073
+ csrf,
1074
+ user: { id: user.id, email: user.email, name: user.name, role: user.role }
1075
+ });
1076
+ });
1077
+ auth.post("/refresh", async (c) => {
1078
+ const refreshJwt = getCookie(c, REFRESH_COOKIE) || c.req.header("Authorization")?.slice(7);
1079
+ if (!refreshJwt) {
1080
+ return c.json({ error: "Refresh token required" }, 401);
1081
+ }
1082
+ try {
1083
+ const { jwtVerify } = await import("jose");
1084
+ const secret = new TextEncoder().encode(jwtSecret);
1085
+ const { payload } = await jwtVerify(refreshJwt, secret);
1086
+ if (payload.type !== "refresh") return c.json({ error: "Invalid token type" }, 401);
1087
+ const user = await db.getUser(payload.sub);
1088
+ if (!user) return c.json({ error: "User not found" }, 401);
1089
+ const { token, refreshToken } = await issueTokens(user.id, user.email, user.role);
1090
+ const csrf = generateCsrf();
1091
+ const secure = isSecure();
1092
+ setCookie(c, COOKIE_NAME, token, cookieOpts(86400, secure));
1093
+ setCookie(c, REFRESH_COOKIE, refreshToken, cookieOpts(604800, secure));
1094
+ setCookie(c, CSRF_COOKIE, csrf, { ...cookieOpts(86400, secure), httpOnly: false });
1095
+ return c.json({ token, csrf });
1096
+ } catch {
1097
+ return c.json({ error: "Invalid or expired refresh token" }, 401);
1098
+ }
1099
+ });
1100
+ auth.get("/me", async (c) => {
1101
+ const token = await extractToken(c);
1102
+ if (!token) return c.json({ error: "Authentication required" }, 401);
1103
+ try {
1104
+ const { jwtVerify } = await import("jose");
1105
+ const secret = new TextEncoder().encode(jwtSecret);
1106
+ const { payload } = await jwtVerify(token, secret);
1107
+ const user = await db.getUser(payload.sub);
1108
+ if (!user) return c.json({ error: "User not found" }, 404);
1109
+ const { passwordHash, ...safe } = user;
1110
+ return c.json(safe);
1111
+ } catch {
1112
+ return c.json({ error: "Invalid or expired token" }, 401);
1113
+ }
1114
+ });
1115
+ auth.post("/logout", (c) => {
1116
+ deleteCookie(c, COOKIE_NAME, { path: "/" });
1117
+ deleteCookie(c, REFRESH_COOKIE, { path: "/" });
1118
+ deleteCookie(c, CSRF_COOKIE, { path: "/" });
1119
+ return c.json({ ok: true });
1120
+ });
1121
+ auth.get("/sso/providers", async (c) => {
1122
+ const sso = await getSsoConfig();
1123
+ const providers = [];
1124
+ if (sso?.saml?.entityId && sso?.saml?.ssoUrl) {
1125
+ providers.push({ type: "saml", name: "SAML SSO", url: "/auth/saml/login" });
1126
+ }
1127
+ if (sso?.oidc?.clientId && sso?.oidc?.discoveryUrl) {
1128
+ providers.push({ type: "oidc", name: "OpenID Connect", url: "/auth/oidc/authorize" });
1129
+ }
1130
+ return c.json({ providers, ssoEnabled: providers.length > 0 });
1131
+ });
1132
+ auth.get("/setup-status", async (c) => {
1133
+ try {
1134
+ const stats = await db.getStats();
1135
+ const settings = await db.getSettings();
1136
+ const hasUsers = stats.totalUsers > 0;
1137
+ const hasCompanyName = !!(settings?.name && settings.name !== "" && settings.name !== "My Company");
1138
+ const hasSmtp = !!settings?.smtpHost;
1139
+ const hasAgents = stats.totalAgents > 0;
1140
+ return c.json({
1141
+ setupComplete: hasUsers,
1142
+ needsBootstrap: !hasUsers,
1143
+ checklist: {
1144
+ adminCreated: hasUsers,
1145
+ companyConfigured: hasCompanyName,
1146
+ emailConfigured: hasSmtp,
1147
+ agentCreated: hasAgents
1148
+ }
1149
+ });
1150
+ } catch {
1151
+ return c.json({ setupComplete: false, needsBootstrap: true, checklist: { adminCreated: false, companyConfigured: false, emailConfigured: false, agentCreated: false } });
1152
+ }
1153
+ });
1154
+ auth.post("/test-db", async (c) => {
1155
+ const stats = await db.getStats();
1156
+ if (stats.totalUsers > 0) {
1157
+ return c.json({ error: "Setup already complete. Database configuration is disabled." }, 403);
1158
+ }
1159
+ const body = await c.req.json();
1160
+ if (!body.type) {
1161
+ return c.json({ error: "Database type is required" }, 400);
1162
+ }
1163
+ try {
1164
+ const { createAdapter } = await import("./factory-FVJH5RRY.js");
1165
+ const testAdapter = await createAdapter(body);
1166
+ await testAdapter.getStats();
1167
+ await testAdapter.disconnect();
1168
+ return c.json({ success: true });
1169
+ } catch (err) {
1170
+ return c.json({ success: false, error: err.message || "Connection failed" }, 400);
1171
+ }
1172
+ });
1173
+ auth.post("/configure-db", async (c) => {
1174
+ const stats = await db.getStats();
1175
+ if (stats.totalUsers > 0) {
1176
+ return c.json({ error: "Setup already complete. Database configuration is disabled." }, 403);
1177
+ }
1178
+ if (!opts?.onDbConfigure) {
1179
+ return c.json({ error: "Database hot-swap not available" }, 501);
1180
+ }
1181
+ const body = await c.req.json();
1182
+ if (!body.type) {
1183
+ return c.json({ error: "Database type is required" }, 400);
1184
+ }
1185
+ try {
1186
+ const { createAdapter } = await import("./factory-FVJH5RRY.js");
1187
+ const newAdapter = await createAdapter(body);
1188
+ await newAdapter.migrate();
1189
+ const oldAdapter = opts.onDbConfigure(newAdapter);
1190
+ try {
1191
+ await oldAdapter.disconnect();
1192
+ } catch {
1193
+ }
1194
+ try {
1195
+ const { saveDbConfig } = await import("./config-store-CRMKWBON.js");
1196
+ await saveDbConfig(body, jwtSecret);
1197
+ } catch {
1198
+ }
1199
+ return c.json({ success: true, type: body.type });
1200
+ } catch (err) {
1201
+ return c.json({ success: false, error: err.message || "Configuration failed" }, 400);
1202
+ }
1203
+ });
1204
+ const bootstrapAttempts = /* @__PURE__ */ new Map();
1205
+ auth.post("/bootstrap", async (c) => {
1206
+ const clientIp = c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || c.req.header("x-real-ip") || "unknown";
1207
+ const now = Date.now();
1208
+ const attempt = bootstrapAttempts.get(clientIp);
1209
+ if (attempt && attempt.resetAt > now && attempt.count >= 5) {
1210
+ return c.json({ error: "Too many attempts. Try again later." }, 429);
1211
+ }
1212
+ if (!attempt || attempt.resetAt <= now) {
1213
+ bootstrapAttempts.set(clientIp, { count: 1, resetAt: now + 6e4 });
1214
+ } else {
1215
+ attempt.count++;
1216
+ }
1217
+ const stats = await db.getStats();
1218
+ if (stats.totalUsers > 0) {
1219
+ return c.json({ error: "Setup already complete. Bootstrap is disabled." }, 403);
1220
+ }
1221
+ const { name, email, password, companyName, subdomain } = await c.req.json();
1222
+ if (!email || !password || !name) {
1223
+ return c.json({ error: "Name, email, and password are required" }, 400);
1224
+ }
1225
+ if (password.length < 8) {
1226
+ return c.json({ error: "Password must be at least 8 characters" }, 400);
1227
+ }
1228
+ if (!email.includes("@") || !email.includes(".")) {
1229
+ return c.json({ error: "Invalid email address" }, 400);
1230
+ }
1231
+ try {
1232
+ const user = await db.createUser({
1233
+ email,
1234
+ name,
1235
+ role: "owner",
1236
+ password
1237
+ });
1238
+ if (companyName || subdomain) {
1239
+ const updates = {};
1240
+ if (companyName) updates.name = companyName;
1241
+ if (subdomain) {
1242
+ updates.subdomain = subdomain.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 63);
1243
+ }
1244
+ await db.updateSettings(updates);
1245
+ }
1246
+ await db.logEvent({
1247
+ actor: user.id,
1248
+ actorType: "system",
1249
+ action: "setup.bootstrap",
1250
+ resource: `user:${user.id}`,
1251
+ details: { method: "web-wizard", companyName },
1252
+ ip: c.req.header("x-forwarded-for") || c.req.header("x-real-ip")
1253
+ });
1254
+ const { token, refreshToken, csrf } = await setSessionCookies(c, user.id, user.email, user.role, "bootstrap");
1255
+ opts?.onBootstrap?.();
1256
+ return c.json({
1257
+ token,
1258
+ refreshToken,
1259
+ csrf,
1260
+ user: { id: user.id, email: user.email, name: user.name, role: user.role }
1261
+ });
1262
+ } catch (err) {
1263
+ return c.json({ error: err.message || "Bootstrap failed" }, 500);
1264
+ }
1265
+ });
1266
+ auth.get("/oidc/authorize", async (c) => {
1267
+ const sso = await getSsoConfig();
1268
+ if (!sso?.oidc?.clientId || !sso?.oidc?.discoveryUrl) {
1269
+ return c.json({ error: "OIDC not configured. Set up OIDC in Settings > SSO." }, 400);
1270
+ }
1271
+ const oidc = sso.oidc;
1272
+ let discovery;
1273
+ try {
1274
+ const res = await fetch(oidc.discoveryUrl);
1275
+ if (!res.ok) throw new Error(`Discovery fetch failed: ${res.status}`);
1276
+ discovery = await res.json();
1277
+ } catch (e) {
1278
+ return c.json({ error: `Failed to fetch OIDC discovery: ${e.message}` }, 502);
1279
+ }
1280
+ if (!discovery.authorization_endpoint) {
1281
+ return c.json({ error: "Invalid OIDC discovery: missing authorization_endpoint" }, 502);
1282
+ }
1283
+ const state = generateState();
1284
+ const nonce = generateState();
1285
+ const codeVerifier = generateCodeVerifier();
1286
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
1287
+ const protocol = c.req.header("x-forwarded-proto") || "http";
1288
+ const host = c.req.header("host") || "localhost";
1289
+ const redirectUri = `${protocol}://${host}/auth/oidc/callback`;
1290
+ const { SignJWT } = await import("jose");
1291
+ const secret = new TextEncoder().encode(jwtSecret);
1292
+ const stateToken = await new SignJWT({
1293
+ state,
1294
+ nonce,
1295
+ codeVerifier,
1296
+ redirectUri,
1297
+ discoveryUrl: oidc.discoveryUrl
1298
+ }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime("10m").sign(secret);
1299
+ setCookie(c, "em_oidc_state", stateToken, {
1300
+ httpOnly: true,
1301
+ secure: isSecure(),
1302
+ sameSite: "Lax",
1303
+ path: "/auth/oidc",
1304
+ maxAge: 600
1305
+ });
1306
+ const scopes = oidc.scopes?.join(" ") || "openid email profile";
1307
+ const authUrl = new URL(discovery.authorization_endpoint);
1308
+ authUrl.searchParams.set("client_id", oidc.clientId);
1309
+ authUrl.searchParams.set("response_type", "code");
1310
+ authUrl.searchParams.set("scope", scopes);
1311
+ authUrl.searchParams.set("redirect_uri", redirectUri);
1312
+ authUrl.searchParams.set("state", state);
1313
+ authUrl.searchParams.set("nonce", nonce);
1314
+ authUrl.searchParams.set("code_challenge", codeChallenge);
1315
+ authUrl.searchParams.set("code_challenge_method", "S256");
1316
+ return c.redirect(authUrl.toString());
1317
+ });
1318
+ auth.get("/oidc/callback", async (c) => {
1319
+ const code = c.req.query("code");
1320
+ const returnedState = c.req.query("state");
1321
+ const error = c.req.query("error");
1322
+ const errorDesc = c.req.query("error_description");
1323
+ if (error) {
1324
+ return c.html(ssoErrorPage("OIDC Error", errorDesc || error));
1325
+ }
1326
+ if (!code || !returnedState) {
1327
+ return c.html(ssoErrorPage("OIDC Error", "Missing code or state parameter"));
1328
+ }
1329
+ const stateCookie = getCookie(c, "em_oidc_state");
1330
+ if (!stateCookie) {
1331
+ return c.html(ssoErrorPage("OIDC Error", "Session expired. Please try again."));
1332
+ }
1333
+ deleteCookie(c, "em_oidc_state", { path: "/auth/oidc" });
1334
+ let statePayload;
1335
+ try {
1336
+ const { jwtVerify } = await import("jose");
1337
+ const secret = new TextEncoder().encode(jwtSecret);
1338
+ const { payload } = await jwtVerify(stateCookie, secret);
1339
+ statePayload = payload;
1340
+ } catch {
1341
+ return c.html(ssoErrorPage("OIDC Error", "Invalid or expired state. Please try again."));
1342
+ }
1343
+ if (statePayload.state !== returnedState) {
1344
+ return c.html(ssoErrorPage("OIDC Error", "State mismatch. Possible CSRF attack."));
1345
+ }
1346
+ const sso = await getSsoConfig();
1347
+ if (!sso?.oidc) {
1348
+ return c.html(ssoErrorPage("OIDC Error", "OIDC is no longer configured."));
1349
+ }
1350
+ const oidc = sso.oidc;
1351
+ let discovery;
1352
+ try {
1353
+ const res = await fetch(oidc.discoveryUrl);
1354
+ discovery = await res.json();
1355
+ } catch (e) {
1356
+ return c.html(ssoErrorPage("OIDC Error", `Discovery fetch failed: ${e.message}`));
1357
+ }
1358
+ let tokenResponse;
1359
+ try {
1360
+ const tokenRes = await fetch(discovery.token_endpoint, {
1361
+ method: "POST",
1362
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1363
+ body: new URLSearchParams({
1364
+ grant_type: "authorization_code",
1365
+ code,
1366
+ redirect_uri: statePayload.redirectUri,
1367
+ client_id: oidc.clientId,
1368
+ client_secret: oidc.clientSecret,
1369
+ code_verifier: statePayload.codeVerifier
1370
+ }).toString()
1371
+ });
1372
+ if (!tokenRes.ok) {
1373
+ const errBody = await tokenRes.text();
1374
+ throw new Error(`Token exchange failed (${tokenRes.status}): ${errBody}`);
1375
+ }
1376
+ tokenResponse = await tokenRes.json();
1377
+ } catch (e) {
1378
+ return c.html(ssoErrorPage("OIDC Error", e.message));
1379
+ }
1380
+ let email;
1381
+ let name;
1382
+ let sub;
1383
+ if (tokenResponse.id_token) {
1384
+ const parts = tokenResponse.id_token.split(".");
1385
+ if (parts.length !== 3) {
1386
+ return c.html(ssoErrorPage("OIDC Error", "Invalid id_token format"));
1387
+ }
1388
+ try {
1389
+ const { jwtVerify, createRemoteJWKSet } = await import("jose");
1390
+ const jwks = createRemoteJWKSet(new URL(discovery.jwks_uri));
1391
+ const { payload } = await jwtVerify(tokenResponse.id_token, jwks, {
1392
+ issuer: discovery.issuer,
1393
+ audience: oidc.clientId
1394
+ });
1395
+ if (payload.nonce !== statePayload.nonce) {
1396
+ return c.html(ssoErrorPage("OIDC Error", "Nonce mismatch. Possible replay attack."));
1397
+ }
1398
+ sub = payload.sub;
1399
+ email = payload.email || "";
1400
+ name = payload.name || payload.preferred_username || "";
1401
+ } catch (e) {
1402
+ return c.html(ssoErrorPage("OIDC Error", `ID token verification failed: ${e.message}`));
1403
+ }
1404
+ } else if (discovery.userinfo_endpoint) {
1405
+ try {
1406
+ const uiRes = await fetch(discovery.userinfo_endpoint, {
1407
+ headers: { Authorization: `Bearer ${tokenResponse.access_token}` }
1408
+ });
1409
+ const userinfo = await uiRes.json();
1410
+ sub = userinfo.sub;
1411
+ email = userinfo.email || "";
1412
+ name = userinfo.name || userinfo.preferred_username || "";
1413
+ } catch (e) {
1414
+ return c.html(ssoErrorPage("OIDC Error", `Userinfo fetch failed: ${e.message}`));
1415
+ }
1416
+ } else {
1417
+ return c.html(ssoErrorPage("OIDC Error", "No id_token or userinfo endpoint available"));
1418
+ }
1419
+ if (!email) {
1420
+ return c.html(ssoErrorPage("OIDC Error", 'No email claim in the token. Ensure "email" scope is granted.'));
1421
+ }
1422
+ const result = await findOrProvisionSsoUser("oidc", sub, email, name, oidc);
1423
+ if ("error" in result) {
1424
+ return c.html(ssoErrorPage("OIDC Error", result.error ?? "Unknown error"));
1425
+ }
1426
+ await setSessionCookies(c, result.user.id, result.user.email, result.user.role, "oidc");
1427
+ return c.redirect("/dashboard");
1428
+ });
1429
+ auth.get("/saml/login", async (c) => {
1430
+ const sso = await getSsoConfig();
1431
+ if (!sso?.saml?.ssoUrl || !sso?.saml?.entityId) {
1432
+ return c.json({ error: "SAML not configured. Set up SAML in Settings > SSO." }, 400);
1433
+ }
1434
+ const saml = sso.saml;
1435
+ const protocol = c.req.header("x-forwarded-proto") || "http";
1436
+ const host = c.req.header("host") || "localhost";
1437
+ const acsUrl = `${protocol}://${host}/auth/saml/callback`;
1438
+ const requestId2 = "_" + crypto.randomUUID().replace(/-/g, "");
1439
+ const issueInstant = (/* @__PURE__ */ new Date()).toISOString();
1440
+ const authnRequest = `<samlp:AuthnRequest
1441
+ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
1442
+ xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
1443
+ ID="${requestId2}"
1444
+ Version="2.0"
1445
+ IssueInstant="${issueInstant}"
1446
+ Destination="${saml.ssoUrl}"
1447
+ AssertionConsumerServiceURL="${acsUrl}"
1448
+ ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST">
1449
+ <saml:Issuer>${saml.entityId}</saml:Issuer>
1450
+ <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" AllowCreate="true"/>
1451
+ </samlp:AuthnRequest>`;
1452
+ const { deflateRawSync } = await import("zlib");
1453
+ const deflated = deflateRawSync(Buffer.from(authnRequest, "utf-8"));
1454
+ const encoded = deflated.toString("base64");
1455
+ const redirectUrl = new URL(saml.ssoUrl);
1456
+ redirectUrl.searchParams.set("SAMLRequest", encoded);
1457
+ redirectUrl.searchParams.set("RelayState", "/dashboard");
1458
+ return c.redirect(redirectUrl.toString());
1459
+ });
1460
+ auth.get("/saml/metadata", async (c) => {
1461
+ const sso = await getSsoConfig();
1462
+ const entityId = sso?.saml?.entityId || "agenticmail-enterprise";
1463
+ const protocol = c.req.header("x-forwarded-proto") || "http";
1464
+ const host = c.req.header("host") || "localhost";
1465
+ const acsUrl = `${protocol}://${host}/auth/saml/callback`;
1466
+ const sloUrl = `${protocol}://${host}/auth/saml/logout`;
1467
+ const metadata = `<?xml version="1.0" encoding="UTF-8"?>
1468
+ <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
1469
+ entityID="${entityId}">
1470
+ <md:SPSSODescriptor
1471
+ AuthnRequestsSigned="false"
1472
+ WantAssertionsSigned="true"
1473
+ protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
1474
+ <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
1475
+ <md:AssertionConsumerService
1476
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
1477
+ Location="${acsUrl}"
1478
+ index="0"
1479
+ isDefault="true"/>
1480
+ <md:SingleLogoutService
1481
+ Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
1482
+ Location="${sloUrl}"/>
1483
+ </md:SPSSODescriptor>
1484
+ </md:EntityDescriptor>`;
1485
+ return c.body(metadata, 200, {
1486
+ "Content-Type": "application/xml"
1487
+ });
1488
+ });
1489
+ auth.post("/saml/callback", async (c) => {
1490
+ const sso = await getSsoConfig();
1491
+ if (!sso?.saml?.certificate) {
1492
+ return c.html(ssoErrorPage("SAML Error", "SAML not configured."));
1493
+ }
1494
+ const saml = sso.saml;
1495
+ let samlResponse;
1496
+ const contentType = c.req.header("content-type") || "";
1497
+ if (contentType.includes("application/x-www-form-urlencoded")) {
1498
+ const body = await c.req.parseBody();
1499
+ samlResponse = body["SAMLResponse"];
1500
+ } else {
1501
+ const body = await c.req.json().catch(() => ({}));
1502
+ samlResponse = body.SAMLResponse;
1503
+ }
1504
+ if (!samlResponse) {
1505
+ return c.html(ssoErrorPage("SAML Error", "Missing SAMLResponse"));
1506
+ }
1507
+ let xml;
1508
+ try {
1509
+ xml = Buffer.from(samlResponse, "base64").toString("utf-8");
1510
+ } catch {
1511
+ return c.html(ssoErrorPage("SAML Error", "Invalid base64 encoding"));
1512
+ }
1513
+ const assertion = parseSamlAssertion(xml, saml.certificate);
1514
+ if (assertion.error) {
1515
+ return c.html(ssoErrorPage("SAML Error", assertion.error));
1516
+ }
1517
+ if (!assertion.email) {
1518
+ return c.html(ssoErrorPage("SAML Error", "No email found in SAML assertion. Check your IdP attribute mapping."));
1519
+ }
1520
+ if (assertion.notBefore && new Date(assertion.notBefore) > /* @__PURE__ */ new Date()) {
1521
+ return c.html(ssoErrorPage("SAML Error", "Assertion not yet valid"));
1522
+ }
1523
+ if (assertion.notOnOrAfter && new Date(assertion.notOnOrAfter) <= /* @__PURE__ */ new Date()) {
1524
+ return c.html(ssoErrorPage("SAML Error", "Assertion has expired"));
1525
+ }
1526
+ const subject = assertion.nameId || assertion.email;
1527
+ const result = await findOrProvisionSsoUser("saml", subject, assertion.email, assertion.name || "", saml);
1528
+ if ("error" in result) {
1529
+ return c.html(ssoErrorPage("SAML Error", result.error ?? "Unknown error"));
1530
+ }
1531
+ await setSessionCookies(c, result.user.id, result.user.email, result.user.role, "saml");
1532
+ return c.redirect("/dashboard");
1533
+ });
1534
+ return auth;
1535
+ }
1536
+ function parseSamlAssertion(xml, certificate) {
1537
+ const result = {};
1538
+ try {
1539
+ const statusMatch = xml.match(/<samlp?:StatusCode[^>]*Value="([^"]+)"/);
1540
+ if (statusMatch) {
1541
+ const statusValue = statusMatch[1];
1542
+ if (!statusValue.includes(":Success")) {
1543
+ result.error = `SAML authentication failed with status: ${statusValue}`;
1544
+ return result;
1545
+ }
1546
+ }
1547
+ const nameIdMatch = xml.match(/<(?:saml2?:)?NameID[^>]*>([^<]+)<\/(?:saml2?:)?NameID>/);
1548
+ if (nameIdMatch) {
1549
+ result.nameId = nameIdMatch[1].trim();
1550
+ }
1551
+ const issuerMatch = xml.match(/<(?:saml2?:)?Issuer[^>]*>([^<]+)<\/(?:saml2?:)?Issuer>/);
1552
+ if (issuerMatch) {
1553
+ result.issuer = issuerMatch[1].trim();
1554
+ }
1555
+ const condMatch = xml.match(/<(?:saml2?:)?Conditions\s+NotBefore="([^"]+)"\s+NotOnOrAfter="([^"]+)"/);
1556
+ if (condMatch) {
1557
+ result.notBefore = condMatch[1];
1558
+ result.notOnOrAfter = condMatch[2];
1559
+ }
1560
+ const sessionMatch = xml.match(/SessionIndex="([^"]+)"/);
1561
+ if (sessionMatch) {
1562
+ result.sessionIndex = sessionMatch[1];
1563
+ }
1564
+ const attrRegex = /<(?:saml2?:)?Attribute\s+Name="([^"]+)"[^>]*>[\s\S]*?<(?:saml2?:)?AttributeValue[^>]*>([^<]*)<\/(?:saml2?:)?AttributeValue>/g;
1565
+ let match;
1566
+ while ((match = attrRegex.exec(xml)) !== null) {
1567
+ const attrName = match[1].toLowerCase();
1568
+ const attrValue = match[2].trim();
1569
+ if (attrName.includes("emailaddress") || attrName.includes("email") || attrName === "mail") {
1570
+ result.email = attrValue;
1571
+ } else if (attrName.includes("displayname") || attrName === "name") {
1572
+ result.name = attrValue;
1573
+ } else if (attrName.includes("givenname") || attrName.includes("firstname")) {
1574
+ result.firstName = attrValue;
1575
+ } else if (attrName.includes("surname") || attrName.includes("lastname")) {
1576
+ result.lastName = attrValue;
1577
+ }
1578
+ }
1579
+ if (!result.email && result.nameId?.includes("@")) {
1580
+ result.email = result.nameId;
1581
+ }
1582
+ if (!result.name && (result.firstName || result.lastName)) {
1583
+ result.name = [result.firstName, result.lastName].filter(Boolean).join(" ");
1584
+ }
1585
+ result.signatureValid = verifySamlSignature(xml, certificate);
1586
+ if (!result.signatureValid) {
1587
+ result.error = "SAML assertion signature verification failed. Check IdP certificate.";
1588
+ return result;
1589
+ }
1590
+ } catch (e) {
1591
+ result.error = `Failed to parse SAML assertion: ${e.message}`;
1592
+ }
1593
+ return result;
1594
+ }
1595
+ function verifySamlSignature(xml, certPem) {
1596
+ try {
1597
+ const sigMatch = xml.match(/<(?:ds:)?SignatureValue[^>]*>([\s\S]*?)<\/(?:ds:)?SignatureValue>/);
1598
+ if (!sigMatch) return true;
1599
+ const signedInfoMatch = xml.match(/<(?:ds:)?SignedInfo[^>]*>[\s\S]*?<\/(?:ds:)?SignedInfo>/);
1600
+ if (!signedInfoMatch) return false;
1601
+ let cert = certPem.trim();
1602
+ if (!cert.startsWith("-----BEGIN CERTIFICATE-----")) {
1603
+ cert = cert.replace(/\s/g, "");
1604
+ cert = `-----BEGIN CERTIFICATE-----
1605
+ ${cert.match(/.{1,64}/g)?.join("\n")}
1606
+ -----END CERTIFICATE-----`;
1607
+ }
1608
+ const algMatch = xml.match(/SignatureMethod\s+Algorithm="([^"]+)"/);
1609
+ const algorithm = algMatch?.[1]?.includes("rsa-sha256") ? "RSA-SHA256" : "RSA-SHA1";
1610
+ const signature = Buffer.from(sigMatch[1].replace(/\s/g, ""), "base64");
1611
+ const signedInfo = signedInfoMatch[0];
1612
+ const canonicalized = signedInfo.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
1613
+ const verifier = createVerify(algorithm);
1614
+ verifier.update(canonicalized);
1615
+ return verifier.verify(cert, signature);
1616
+ } catch {
1617
+ return false;
1618
+ }
1619
+ }
1620
+ function ssoErrorPage(title, message) {
1621
+ return `<!DOCTYPE html>
1622
+ <html>
1623
+ <head><title>${title}</title>
1624
+ <style>
1625
+ body { font-family: system-ui, -apple-system, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f8f9fa; }
1626
+ .card { background: white; border-radius: 12px; padding: 40px; max-width: 480px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); text-align: center; }
1627
+ h1 { color: #dc2626; font-size: 1.5rem; margin: 0 0 16px; }
1628
+ p { color: #4b5563; margin: 0 0 24px; line-height: 1.5; }
1629
+ a { display: inline-block; padding: 10px 24px; background: #6366f1; color: white; border-radius: 8px; text-decoration: none; }
1630
+ a:hover { background: #4f46e5; }
1631
+ </style></head>
1632
+ <body>
1633
+ <div class="card">
1634
+ <h1>${title}</h1>
1635
+ <p>${message}</p>
1636
+ <a href="/dashboard">Back to Dashboard</a>
1637
+ </div>
1638
+ </body></html>`;
1639
+ }
1640
+
1641
+ // src/server.ts
1642
+ function createServer(config) {
1643
+ const app = new Hono3();
1644
+ const dbProxy = createDbProxy(config.db);
1645
+ config.db = dbProxy;
1646
+ const dbBreaker = new CircuitBreaker({
1647
+ failureThreshold: 5,
1648
+ recoveryTimeMs: 3e4,
1649
+ timeout: 1e4
1650
+ });
1651
+ const healthMonitor = new HealthMonitor(
1652
+ async () => {
1653
+ await config.db.getStats();
1654
+ },
1655
+ { intervalMs: 3e4, timeoutMs: 5e3, unhealthyThreshold: 3 }
1656
+ );
1657
+ healthMonitor.onStatusChange((healthy) => {
1658
+ console.log(
1659
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] ${healthy ? "\u2705" : "\u274C"} Database health: ${healthy ? "healthy" : "unhealthy"}`
1660
+ );
1661
+ });
1662
+ app.use("*", requestIdMiddleware());
1663
+ app.use("*", errorHandler());
1664
+ app.use("*", securityHeaders());
1665
+ app.use("*", ipAccessControl(() => config.db));
1666
+ app.use("*", cors({
1667
+ origin: config.corsOrigins || "*",
1668
+ credentials: true,
1669
+ allowHeaders: ["Content-Type", "Authorization", "X-API-Key", "X-Request-Id", "X-CSRF-Token"],
1670
+ exposeHeaders: ["X-Request-Id", "X-RateLimit-Limit", "X-RateLimit-Remaining", "Retry-After"]
1671
+ }));
1672
+ app.use("*", rateLimiter({
1673
+ limit: config.rateLimit ?? 120,
1674
+ windowSec: 60,
1675
+ skipPaths: ["/health", "/ready"]
1676
+ }));
1677
+ if (config.logging !== false) {
1678
+ app.use("*", requestLogger());
1679
+ }
1680
+ app.get("/health", (c) => c.json({
1681
+ status: "ok",
1682
+ version: "0.4.0",
1683
+ uptime: process.uptime()
1684
+ }));
1685
+ app.get("/ready", async (c) => {
1686
+ const dbHealthy = healthMonitor.isHealthy();
1687
+ const status = dbHealthy ? 200 : 503;
1688
+ return c.json({
1689
+ ready: dbHealthy,
1690
+ checks: {
1691
+ database: dbHealthy ? "ok" : "unhealthy",
1692
+ circuitBreaker: dbBreaker.getState()
1693
+ }
1694
+ }, status);
1695
+ });
1696
+ let _setupComplete = false;
1697
+ (async () => {
1698
+ try {
1699
+ const stats = await config.db.getStats();
1700
+ if (stats.totalUsers > 0) _setupComplete = true;
1701
+ } catch {
1702
+ }
1703
+ })();
1704
+ const authRoutes = createAuthRoutes(config.db, config.jwtSecret, {
1705
+ onBootstrap: () => {
1706
+ _setupComplete = true;
1707
+ },
1708
+ onDbConfigure: (newAdapter) => {
1709
+ const old = dbProxy.__swap(newAdapter);
1710
+ engineInitialized = false;
1711
+ return old;
1712
+ }
1713
+ });
1714
+ app.route("/auth", authRoutes);
1715
+ const api = new Hono3();
1716
+ api.use("*", async (c, next) => {
1717
+ const apiKeyHeader = c.req.header("X-API-Key");
1718
+ if (apiKeyHeader) {
1719
+ const key = await dbBreaker.execute(() => config.db.validateApiKey(apiKeyHeader));
1720
+ if (!key) return c.json({ error: "Invalid API key" }, 401);
1721
+ c.set("userId", key.createdBy);
1722
+ c.set("authType", "api-key");
1723
+ c.set("apiKeyScopes", key.scopes);
1724
+ return next();
1725
+ }
1726
+ const { getCookie: getCookie2 } = await import("hono/cookie");
1727
+ const cookieToken = getCookie2(c, "em_session");
1728
+ const authHeader = c.req.header("Authorization");
1729
+ const jwt = cookieToken || (authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null);
1730
+ if (!jwt) {
1731
+ return c.json({ error: "Authentication required" }, 401);
1732
+ }
1733
+ try {
1734
+ const { jwtVerify } = await import("jose");
1735
+ const secret = new TextEncoder().encode(config.jwtSecret);
1736
+ const { payload } = await jwtVerify(jwt, secret);
1737
+ c.set("userId", payload.sub);
1738
+ c.set("userRole", payload.role || "");
1739
+ c.set("userEmail", payload.email || "");
1740
+ c.set("authType", cookieToken ? "cookie" : "jwt");
1741
+ return next();
1742
+ } catch {
1743
+ return c.json({ error: "Invalid or expired token" }, 401);
1744
+ }
1745
+ });
1746
+ api.use("*", auditLogger(config.db));
1747
+ const adminRoutes = createAdminRoutes(config.db);
1748
+ api.route("/", adminRoutes);
1749
+ let engineInitialized = false;
1750
+ api.all("/engine/*", async (c, next) => {
1751
+ try {
1752
+ const { engineRoutes, setEngineDb } = await import("./routes-ALTC4I2R.js");
1753
+ const { EngineDatabase } = await import("./db-adapter-5PWMLY67.js");
1754
+ if (!engineInitialized) {
1755
+ const engineDbInterface = config.db.getEngineDB();
1756
+ if (!engineDbInterface) {
1757
+ return c.json({
1758
+ error: "Engine not available",
1759
+ detail: `Engine requires a SQL-compatible database. "${config.db.type}" does not support raw SQL queries. Use postgres, mysql, sqlite, or turso.`
1760
+ }, 501);
1761
+ }
1762
+ const adapterDialect = config.db.getDialect();
1763
+ const dialectMap = {
1764
+ sqlite: "sqlite",
1765
+ postgres: "postgres",
1766
+ supabase: "postgres",
1767
+ neon: "postgres",
1768
+ cockroachdb: "postgres",
1769
+ mysql: "mysql",
1770
+ planetscale: "mysql",
1771
+ turso: "turso"
1772
+ };
1773
+ const engineDialect = dialectMap[adapterDialect] || adapterDialect;
1774
+ const engineDb = new EngineDatabase(engineDbInterface, engineDialect);
1775
+ const migrationResult = await engineDb.migrate();
1776
+ console.log(`[engine] Migrations: ${migrationResult.applied} applied, ${migrationResult.total} total`);
1777
+ await setEngineDb(engineDb, config.db);
1778
+ engineInitialized = true;
1779
+ if (config.runtime?.enabled) {
1780
+ try {
1781
+ const { createAgentRuntime } = await import("./runtime-JLFTHMIT.js");
1782
+ const { mountRuntimeApp } = await import("./routes-ALTC4I2R.js");
1783
+ const runtime = createAgentRuntime({
1784
+ engineDb,
1785
+ adminDb: config.db,
1786
+ defaultModel: config.runtime.defaultModel,
1787
+ apiKeys: config.runtime.apiKeys,
1788
+ gatewayEnabled: true
1789
+ });
1790
+ await runtime.start();
1791
+ const runtimeApp = runtime.getApp();
1792
+ if (runtimeApp) {
1793
+ mountRuntimeApp(runtimeApp);
1794
+ }
1795
+ console.log("[runtime] Agent runtime started and mounted at /api/engine/runtime/*");
1796
+ } catch (runtimeErr) {
1797
+ console.warn(`[runtime] Failed to start agent runtime: ${runtimeErr.message}`);
1798
+ }
1799
+ }
1800
+ }
1801
+ const originalUrl = new URL(c.req.url);
1802
+ const subPath = (c.req.path.replace(/^\/api\/engine/, "") || "/") + originalUrl.search;
1803
+ const headers = new Headers(c.req.raw.headers);
1804
+ const userId = c.get("userId");
1805
+ const userRole = c.get("userRole");
1806
+ const userEmail = c.get("userEmail");
1807
+ const authType = c.get("authType");
1808
+ const requestId2 = c.get("requestId");
1809
+ if (userId) headers.set("X-User-Id", String(userId));
1810
+ if (userRole) headers.set("X-User-Role", String(userRole));
1811
+ if (userEmail) headers.set("X-User-Email", String(userEmail));
1812
+ if (authType) headers.set("X-Auth-Type", String(authType));
1813
+ if (requestId2) headers.set("X-Request-Id", String(requestId2));
1814
+ const subReq = new Request(new URL(subPath, "http://localhost"), {
1815
+ method: c.req.method,
1816
+ headers,
1817
+ body: c.req.method !== "GET" && c.req.method !== "HEAD" ? c.req.raw.body : void 0
1818
+ });
1819
+ return engineRoutes.fetch(subReq);
1820
+ } catch (e) {
1821
+ console.error("[engine] Error:", e.message);
1822
+ return c.json({ error: "Engine module not available", detail: e.message }, 501);
1823
+ }
1824
+ });
1825
+ app.route("/api", api);
1826
+ let dashboardHtml = null;
1827
+ function getDashboardHtml() {
1828
+ if (!dashboardHtml) {
1829
+ try {
1830
+ const dir = dirname(fileURLToPath(import.meta.url));
1831
+ dashboardHtml = readFileSync(join(dir, "dashboard", "index.html"), "utf-8");
1832
+ } catch {
1833
+ try {
1834
+ dashboardHtml = readFileSync(join(process.cwd(), "node_modules", "@agenticmail", "enterprise", "dist", "dashboard", "index.html"), "utf-8");
1835
+ } catch {
1836
+ dashboardHtml = "<html><body><h1>Dashboard not found</h1><p>The dashboard HTML file could not be located.</p></body></html>";
1837
+ }
1838
+ }
1839
+ }
1840
+ return dashboardHtml;
1841
+ }
1842
+ async function serveDashboard(c) {
1843
+ let html = getDashboardHtml();
1844
+ if (!_setupComplete) {
1845
+ const injection = `<script>window.__EM_SETUP_STATE__=${JSON.stringify({ needsBootstrap: true })};</script>`;
1846
+ html = html.replace("</head>", injection + "</head>");
1847
+ }
1848
+ try {
1849
+ const settings = await config.db.getSettings();
1850
+ if (settings.domain && settings.domainStatus) {
1851
+ const domainState = {
1852
+ domain: settings.domain,
1853
+ status: settings.domainStatus,
1854
+ verifiedAt: settings.domainVerifiedAt,
1855
+ dnsChallenge: settings.domainDnsChallenge
1856
+ };
1857
+ const domainScript = `<script>window.__EM_DOMAIN_STATE__=${JSON.stringify(domainState)};</script>`;
1858
+ html = html.replace("</head>", domainScript + "</head>");
1859
+ }
1860
+ } catch {
1861
+ }
1862
+ return c.html(html);
1863
+ }
1864
+ app.get("/", (c) => c.redirect("/dashboard"));
1865
+ app.get("/dashboard", serveDashboard);
1866
+ const STATIC_MIME = { ".js": "application/javascript; charset=utf-8", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".svg": "image/svg+xml", ".ico": "image/x-icon", ".gif": "image/gif", ".webp": "image/webp", ".css": "text/css; charset=utf-8" };
1867
+ app.get("/dashboard/*", (c) => {
1868
+ const reqPath = c.req.path.replace("/dashboard/", "");
1869
+ const ext = reqPath.substring(reqPath.lastIndexOf("."));
1870
+ const mime = STATIC_MIME[ext];
1871
+ if (mime) {
1872
+ const dir = dirname(fileURLToPath(import.meta.url));
1873
+ const filePath = join(dir, "dashboard", reqPath);
1874
+ if (!filePath.startsWith(join(dir, "dashboard"))) {
1875
+ return c.json({ error: "Forbidden" }, 403);
1876
+ }
1877
+ if (existsSync(filePath)) {
1878
+ const content = readFileSync(filePath);
1879
+ return new Response(content, { status: 200, headers: { "Content-Type": mime, "Cache-Control": ext === ".js" ? "no-cache, no-store, must-revalidate" : "public, max-age=86400" } });
1880
+ }
1881
+ }
1882
+ return serveDashboard(c);
1883
+ });
1884
+ app.notFound((c) => {
1885
+ return c.json({ error: "Not found", path: c.req.path }, 404);
1886
+ });
1887
+ return {
1888
+ app,
1889
+ healthMonitor,
1890
+ start: () => {
1891
+ return new Promise((resolve) => {
1892
+ const server = serve(
1893
+ { fetch: app.fetch, port: config.port },
1894
+ (info) => {
1895
+ console.log(`
1896
+ \u{1F3E2} AgenticMail Enterprise`);
1897
+ console.log(` API: http://localhost:${info.port}/api`);
1898
+ console.log(` Auth: http://localhost:${info.port}/auth`);
1899
+ console.log(` Health: http://localhost:${info.port}/health`);
1900
+ console.log("");
1901
+ healthMonitor.start();
1902
+ const shutdown = () => {
1903
+ console.log("\n\u23F3 Shutting down gracefully...");
1904
+ healthMonitor.stop();
1905
+ server.close(() => {
1906
+ config.db.disconnect().then(() => {
1907
+ console.log("\u2705 Shutdown complete");
1908
+ process.exit(0);
1909
+ });
1910
+ });
1911
+ setTimeout(() => {
1912
+ process.exit(1);
1913
+ }, 1e4).unref();
1914
+ };
1915
+ process.on("SIGINT", shutdown);
1916
+ process.on("SIGTERM", shutdown);
1917
+ resolve({
1918
+ close: () => {
1919
+ healthMonitor.stop();
1920
+ server.close();
1921
+ }
1922
+ });
1923
+ }
1924
+ );
1925
+ });
1926
+ }
1927
+ };
1928
+ }
1929
+
1930
+ export {
1931
+ requestIdMiddleware,
1932
+ requestLogger,
1933
+ rateLimiter,
1934
+ securityHeaders,
1935
+ errorHandler,
1936
+ ValidationError,
1937
+ validate,
1938
+ auditLogger,
1939
+ requireRole,
1940
+ createAdminRoutes,
1941
+ createAuthRoutes,
1942
+ createServer
1943
+ };