@agenticmail/enterprise 0.5.336 → 0.5.337

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,879 @@
1
+ import {
2
+ getAllCreateStatements
3
+ } from "./chunk-UDEOGAGB.js";
4
+ import {
5
+ DatabaseAdapter
6
+ } from "./chunk-FLRYMSKY.js";
7
+ import "./chunk-KFQGP6VL.js";
8
+
9
+ // src/db/postgres.ts
10
+ import { randomUUID, createHash } from "crypto";
11
+ var pg;
12
+ async function getPg() {
13
+ if (!pg) {
14
+ const { resolveDriver } = await import("./resolve-driver-VQXMFKLJ.js");
15
+ pg = await resolveDriver(
16
+ "pg",
17
+ "PostgreSQL driver not found. Install it: npm install pg\nFor Supabase/Neon/CockroachDB, the same pg driver works."
18
+ );
19
+ }
20
+ return pg;
21
+ }
22
+ var PostgresAdapter = class extends DatabaseAdapter {
23
+ type = "postgres";
24
+ pool = null;
25
+ ended = false;
26
+ _directUrl;
27
+ _connectionString;
28
+ _isPgBouncer = false;
29
+ async connect(config) {
30
+ this.ended = false;
31
+ const { Pool } = await getPg();
32
+ const connUrl = config.connectionString ? new URL(config.connectionString) : null;
33
+ const port = config.port || (connUrl ? connUrl.port : "5432");
34
+ const hostname = connUrl?.hostname || config.host || "";
35
+ const isPgBouncer = String(port) === "6543" || String(port) === "5433" || hostname.includes(".pooler.supabase.") || hostname.includes(".pooler.") || connUrl?.searchParams.get("pgbouncer") === "true";
36
+ this._directUrl = config.directUrl || void 0;
37
+ this._connectionString = config.connectionString;
38
+ this._isPgBouncer = isPgBouncer;
39
+ const envMax = process.env.DB_POOL_MAX ? parseInt(process.env.DB_POOL_MAX, 10) : 0;
40
+ const defaultMax = isPgBouncer ? 3 : 10;
41
+ const poolMax = envMax || defaultMax;
42
+ const idleTimeout = isPgBouncer ? 2e3 : 3e4;
43
+ this.pool = new Pool({
44
+ connectionString: config.connectionString,
45
+ host: config.host,
46
+ port: config.port,
47
+ database: config.database,
48
+ user: config.username,
49
+ password: config.password,
50
+ ssl: config.ssl ? { rejectUnauthorized: false } : void 0,
51
+ max: poolMax,
52
+ idleTimeoutMillis: idleTimeout,
53
+ connectionTimeoutMillis: 15e3,
54
+ // PgBouncer doesn't support prepared statements in transaction mode
55
+ ...isPgBouncer ? { allowExitOnIdle: true } : {}
56
+ });
57
+ this.pool.on("error", (err) => {
58
+ if (this.ended) return;
59
+ if (err.code === "XX000" || err.code === "53300" || err.code === "57P01") {
60
+ console.warn(`[postgres] Pool connection error (${err.code}): ${err.message?.slice(0, 100) || "unknown"} \u2014 will retry`);
61
+ } else {
62
+ console.error(`[postgres] Unexpected pool error:`, err.message?.slice(0, 200));
63
+ }
64
+ });
65
+ const client = await this.pool.connect();
66
+ client.release();
67
+ console.log(`[postgres] Connected (pool: max=${poolMax}, idle=${idleTimeout}ms, pgbouncer=${isPgBouncer})`);
68
+ }
69
+ async disconnect() {
70
+ this.ended = true;
71
+ if (this.pool) {
72
+ try {
73
+ await this.pool.end();
74
+ } catch {
75
+ }
76
+ }
77
+ }
78
+ isConnected() {
79
+ return this.pool !== null && !this.ended;
80
+ }
81
+ /**
82
+ * Execute a query with automatic retry on transient connection errors.
83
+ * Retries once after a 500ms delay for: connection limit exceeded (XX000, 53300),
84
+ * admin shutdown (57P01), connection refused, client already released.
85
+ */
86
+ async _query(sql, params) {
87
+ const RETRYABLE = /* @__PURE__ */ new Set(["XX000", "53300", "57P01", "ECONNREFUSED", "ECONNRESET", "57P03", "25P02"]);
88
+ for (let attempt = 0; attempt < 3; attempt++) {
89
+ try {
90
+ return await this.pool.query(sql, params);
91
+ } catch (err) {
92
+ const code = err.code || "";
93
+ const msg = err.message || "";
94
+ const isAbortedTx = code === "25P02" || msg.includes("current transaction is aborted");
95
+ const isRetryable = RETRYABLE.has(code) || msg.includes("remaining connection") || msg.includes("terminating connection") || msg.includes("Connection terminated") || msg.includes("MaxClientsInSessionMode");
96
+ if ((isRetryable || isAbortedTx) && attempt < 2) {
97
+ if (isAbortedTx) {
98
+ try {
99
+ await this.pool.query("ROLLBACK");
100
+ } catch {
101
+ }
102
+ }
103
+ await new Promise((r) => setTimeout(r, 500 + Math.random() * 1e3));
104
+ continue;
105
+ }
106
+ throw err;
107
+ }
108
+ }
109
+ }
110
+ // ─── Engine Integration ──────────────────────────────────
111
+ getEngineDB() {
112
+ if (!this.pool || this.ended) return null;
113
+ const _pool = this.pool;
114
+ const self = this;
115
+ const pgSql = (sql) => {
116
+ let i = 0;
117
+ return sql.replace(/\?/g, () => `$${++i}`);
118
+ };
119
+ return {
120
+ run: async (sql, params) => {
121
+ if (self.ended) return;
122
+ await self._query(pgSql(sql), params);
123
+ },
124
+ get: async (sql, params) => {
125
+ if (self.ended) return void 0;
126
+ const result = await self._query(pgSql(sql), params);
127
+ return result.rows[0];
128
+ },
129
+ all: async (sql, params) => {
130
+ if (self.ended) return [];
131
+ const result = await self._query(pgSql(sql), params);
132
+ return result.rows;
133
+ }
134
+ };
135
+ }
136
+ getDialect() {
137
+ return "postgres";
138
+ }
139
+ async migrate() {
140
+ const stmts = getAllCreateStatements();
141
+ let directPool = null;
142
+ let client;
143
+ if (this._isPgBouncer && this._directUrl) {
144
+ try {
145
+ const { Pool } = await getPg();
146
+ directPool = new Pool({
147
+ connectionString: this._directUrl,
148
+ ssl: { rejectUnauthorized: false },
149
+ max: 1,
150
+ idleTimeoutMillis: 5e3,
151
+ connectionTimeoutMillis: 8e3
152
+ });
153
+ directPool.on("error", () => {
154
+ });
155
+ client = await directPool.connect();
156
+ console.log("[postgres] Using direct connection for migrations (bypassing PgBouncer)");
157
+ } catch (err) {
158
+ if (process.env.DEBUG_DB) console.warn(`[postgres] Direct connection unavailable (${err.message?.slice(0, 80)}), using pooler for migrations`);
159
+ if (directPool) {
160
+ try {
161
+ await directPool.end();
162
+ } catch {
163
+ }
164
+ }
165
+ directPool = null;
166
+ client = await this.pool.connect();
167
+ }
168
+ } else {
169
+ client = await this.pool.connect();
170
+ }
171
+ try {
172
+ await client.query("BEGIN");
173
+ for (const stmt of stmts) {
174
+ await client.query(stmt);
175
+ }
176
+ await client.query(`
177
+ INSERT INTO retention_policy (id) VALUES ('default')
178
+ ON CONFLICT (id) DO NOTHING
179
+ `);
180
+ await client.query(`
181
+ ALTER TABLE company_settings ADD COLUMN IF NOT EXISTS org_id TEXT
182
+ `).catch(() => {
183
+ });
184
+ await client.query(`
185
+ ALTER TABLE company_settings ADD COLUMN IF NOT EXISTS cf_api_token TEXT;
186
+ ALTER TABLE company_settings ADD COLUMN IF NOT EXISTS cf_account_id TEXT;
187
+ `).catch(() => {
188
+ });
189
+ await client.query(`
190
+ ALTER TABLE company_settings ADD COLUMN IF NOT EXISTS org_email_config JSONB;
191
+ `).catch(() => {
192
+ });
193
+ await client.query(`
194
+ ALTER TABLE company_settings ADD COLUMN IF NOT EXISTS platform_capabilities JSONB;
195
+ `).catch(() => {
196
+ });
197
+ await client.query(`
198
+ ALTER TABLE company_settings ADD COLUMN IF NOT EXISTS branding JSONB;
199
+ `).catch(() => {
200
+ });
201
+ await client.query(`
202
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_secret TEXT;
203
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_enabled BOOLEAN DEFAULT FALSE;
204
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_backup_codes TEXT;
205
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS permissions JSONB DEFAULT '"*"';
206
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS must_reset_password BOOLEAN DEFAULT FALSE;
207
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE;
208
+ ALTER TABLE users ADD COLUMN IF NOT EXISTS client_org_id TEXT;
209
+ ALTER TABLE agents ADD COLUMN IF NOT EXISTS billing_rate NUMERIC(10,2) DEFAULT 0;
210
+ `).catch(() => {
211
+ });
212
+ await client.query(`
213
+ CREATE TABLE IF NOT EXISTS client_organizations (
214
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
215
+ name TEXT NOT NULL,
216
+ slug TEXT NOT NULL UNIQUE,
217
+ contact_name TEXT,
218
+ contact_email TEXT,
219
+ description TEXT,
220
+ is_active BOOLEAN DEFAULT TRUE,
221
+ settings JSONB DEFAULT '{}',
222
+ created_at TIMESTAMP DEFAULT NOW(),
223
+ updated_at TIMESTAMP DEFAULT NOW()
224
+ );
225
+ `);
226
+ await client.query(`
227
+ ALTER TABLE client_organizations ADD COLUMN IF NOT EXISTS billing_rate_per_agent NUMERIC(10,2) DEFAULT 0;
228
+ ALTER TABLE client_organizations ADD COLUMN IF NOT EXISTS currency TEXT DEFAULT 'USD';
229
+ ALTER TABLE agents ADD COLUMN IF NOT EXISTS client_org_id TEXT REFERENCES client_organizations(id);
230
+
231
+ CREATE TABLE IF NOT EXISTS org_billing_records (
232
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
233
+ org_id TEXT NOT NULL REFERENCES client_organizations(id) ON DELETE CASCADE,
234
+ agent_id TEXT,
235
+ month TEXT NOT NULL,
236
+ revenue NUMERIC(10,2) DEFAULT 0,
237
+ token_cost NUMERIC(10,4) DEFAULT 0,
238
+ input_tokens BIGINT DEFAULT 0,
239
+ output_tokens BIGINT DEFAULT 0,
240
+ notes TEXT,
241
+ created_at TIMESTAMP DEFAULT NOW(),
242
+ updated_at TIMESTAMP DEFAULT NOW(),
243
+ UNIQUE(org_id, agent_id, month)
244
+ );
245
+ `).catch(() => {
246
+ });
247
+ await client.query(`
248
+ ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS org_id TEXT;
249
+ `).catch(() => {
250
+ });
251
+ await client.query(`
252
+ CREATE TABLE IF NOT EXISTS agent_knowledge_access (
253
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
254
+ agent_id TEXT NOT NULL,
255
+ knowledge_base_id TEXT NOT NULL,
256
+ access_type TEXT NOT NULL DEFAULT 'read',
257
+ created_at TIMESTAMP DEFAULT NOW(),
258
+ UNIQUE(agent_id, knowledge_base_id)
259
+ );
260
+ `);
261
+ await client.query("COMMIT");
262
+ } catch (err) {
263
+ await client.query("ROLLBACK");
264
+ throw err;
265
+ } finally {
266
+ client.release();
267
+ if (directPool) {
268
+ try {
269
+ await directPool.end();
270
+ } catch {
271
+ }
272
+ }
273
+ }
274
+ }
275
+ // ─── Company ─────────────────────────────────────────────
276
+ async getSettings() {
277
+ const { rows } = await this._query(
278
+ "SELECT * FROM company_settings WHERE id = $1",
279
+ ["default"]
280
+ );
281
+ return rows[0] ? this.mapSettings(rows[0]) : null;
282
+ }
283
+ async updateSettings(updates) {
284
+ await this.pool.query(
285
+ `INSERT INTO company_settings (id, name, subdomain) VALUES ('default', '', '')
286
+ ON CONFLICT (id) DO NOTHING`
287
+ );
288
+ const fields = [];
289
+ const values = [];
290
+ let i = 1;
291
+ const map = {
292
+ name: "name",
293
+ orgId: "org_id",
294
+ domain: "domain",
295
+ subdomain: "subdomain",
296
+ smtpHost: "smtp_host",
297
+ smtpPort: "smtp_port",
298
+ smtpUser: "smtp_user",
299
+ smtpPass: "smtp_pass",
300
+ dkimPrivateKey: "dkim_private_key",
301
+ logoUrl: "logo_url",
302
+ primaryColor: "primary_color",
303
+ plan: "plan",
304
+ deploymentKeyHash: "deployment_key_hash",
305
+ domainRegistrationId: "domain_registration_id",
306
+ domainDnsChallenge: "domain_dns_challenge",
307
+ domainVerifiedAt: "domain_verified_at",
308
+ domainRegisteredAt: "domain_registered_at",
309
+ domainStatus: "domain_status",
310
+ useRootDomain: "use_root_domain",
311
+ cfApiToken: "cf_api_token",
312
+ cfAccountId: "cf_account_id",
313
+ signatureTemplate: "signature_template"
314
+ };
315
+ for (const [key, col] of Object.entries(map)) {
316
+ if (updates[key] !== void 0) {
317
+ fields.push(`${col} = $${i}`);
318
+ values.push(updates[key]);
319
+ i++;
320
+ }
321
+ }
322
+ if (updates.ssoConfig !== void 0) {
323
+ fields.push(`sso_config = $${i}`);
324
+ values.push(JSON.stringify(updates.ssoConfig));
325
+ i++;
326
+ }
327
+ if (updates.toolSecurityConfig !== void 0) {
328
+ fields.push(`tool_security_config = $${i}`);
329
+ values.push(JSON.stringify(updates.toolSecurityConfig));
330
+ i++;
331
+ }
332
+ if (updates.firewallConfig !== void 0) {
333
+ fields.push(`firewall_config = $${i}`);
334
+ values.push(JSON.stringify(updates.firewallConfig));
335
+ i++;
336
+ }
337
+ if (updates.modelPricingConfig !== void 0) {
338
+ fields.push(`model_pricing_config = $${i}`);
339
+ values.push(JSON.stringify(updates.modelPricingConfig));
340
+ i++;
341
+ }
342
+ if (updates.securityConfig !== void 0) {
343
+ fields.push(`security_config = $${i}`);
344
+ values.push(JSON.stringify(updates.securityConfig));
345
+ i++;
346
+ }
347
+ if (updates.orgEmailConfig !== void 0) {
348
+ fields.push(`org_email_config = $${i}`);
349
+ values.push(JSON.stringify(updates.orgEmailConfig));
350
+ i++;
351
+ }
352
+ if (updates.platformCapabilities !== void 0) {
353
+ fields.push(`platform_capabilities = $${i}`);
354
+ values.push(JSON.stringify(updates.platformCapabilities));
355
+ i++;
356
+ }
357
+ if (updates.branding !== void 0) {
358
+ fields.push(`branding = $${i}`);
359
+ values.push(JSON.stringify(updates.branding));
360
+ i++;
361
+ }
362
+ fields.push(`updated_at = NOW()`);
363
+ values.push("default");
364
+ const { rows } = await this.pool.query(
365
+ `UPDATE company_settings SET ${fields.join(", ")} WHERE id = $${i} RETURNING *`,
366
+ values
367
+ );
368
+ return this.mapSettings(rows[0]);
369
+ }
370
+ // ─── Agents ──────────────────────────────────────────────
371
+ async createAgent(input) {
372
+ const id = input.id || randomUUID();
373
+ const email = input.email || `${input.name.toLowerCase().replace(/\s+/g, "-")}@localhost`;
374
+ const { rows } = await this.pool.query(
375
+ `INSERT INTO agents (id, name, email, role, metadata, created_by)
376
+ VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
377
+ [id, input.name, email, input.role || "assistant", JSON.stringify(input.metadata || {}), input.createdBy]
378
+ );
379
+ return this.mapAgent(rows[0]);
380
+ }
381
+ async getAgent(id) {
382
+ const { rows } = await this.pool.query("SELECT * FROM agents WHERE id = $1", [id]);
383
+ return rows[0] ? this.mapAgent(rows[0]) : null;
384
+ }
385
+ async getAgentByName(name) {
386
+ const { rows } = await this.pool.query("SELECT * FROM agents WHERE name = $1", [name]);
387
+ return rows[0] ? this.mapAgent(rows[0]) : null;
388
+ }
389
+ async listAgents(opts) {
390
+ let q = "SELECT * FROM agents";
391
+ const params = [];
392
+ if (opts?.status) {
393
+ q += " WHERE status = $1";
394
+ params.push(opts.status);
395
+ }
396
+ q += " ORDER BY created_at DESC";
397
+ if (opts?.limit) {
398
+ q += ` LIMIT ${opts.limit}`;
399
+ }
400
+ if (opts?.offset) {
401
+ q += ` OFFSET ${opts.offset}`;
402
+ }
403
+ const { rows } = await this.pool.query(q, params);
404
+ return rows.map((r) => this.mapAgent(r));
405
+ }
406
+ async updateAgent(id, updates) {
407
+ const fields = [];
408
+ const values = [];
409
+ let i = 1;
410
+ for (const [key, col] of Object.entries({ name: "name", email: "email", role: "role", status: "status" })) {
411
+ if (updates[key] !== void 0) {
412
+ fields.push(`${col} = $${i}`);
413
+ values.push(updates[key]);
414
+ i++;
415
+ }
416
+ }
417
+ if (updates.metadata) {
418
+ fields.push(`metadata = $${i}`);
419
+ values.push(JSON.stringify(updates.metadata));
420
+ i++;
421
+ }
422
+ if (updates.securityOverrides !== void 0) {
423
+ fields.push(`security_overrides = $${i}`);
424
+ values.push(JSON.stringify(updates.securityOverrides));
425
+ i++;
426
+ }
427
+ fields.push("updated_at = NOW()");
428
+ values.push(id);
429
+ const { rows } = await this.pool.query(
430
+ `UPDATE agents SET ${fields.join(", ")} WHERE id = $${i} RETURNING *`,
431
+ values
432
+ );
433
+ return this.mapAgent(rows[0]);
434
+ }
435
+ async archiveAgent(id) {
436
+ await this.pool.query("UPDATE agents SET status = 'archived', updated_at = NOW() WHERE id = $1", [id]);
437
+ }
438
+ async deleteAgent(id) {
439
+ await this.pool.query("DELETE FROM agents WHERE id = $1", [id]);
440
+ }
441
+ async countAgents(status) {
442
+ const q = status ? await this.pool.query("SELECT COUNT(*) FROM agents WHERE status = $1", [status]) : await this.pool.query("SELECT COUNT(*) FROM agents");
443
+ return parseInt(q.rows[0].count, 10);
444
+ }
445
+ // ─── Users ───────────────────────────────────────────────
446
+ async createUser(input) {
447
+ const id = randomUUID();
448
+ let passwordHash = null;
449
+ if (input.password) {
450
+ const { default: bcrypt } = await import("bcryptjs");
451
+ passwordHash = await bcrypt.hash(input.password, 12);
452
+ }
453
+ const { rows } = await this.pool.query(
454
+ `INSERT INTO users (id, email, name, role, password_hash, sso_provider, sso_subject)
455
+ VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
456
+ [id, input.email, input.name, input.role, passwordHash, input.ssoProvider || null, input.ssoSubject || null]
457
+ );
458
+ return this.mapUser(rows[0]);
459
+ }
460
+ async getUser(id) {
461
+ const { rows } = await this.pool.query("SELECT * FROM users WHERE id = $1", [id]);
462
+ return rows[0] ? this.mapUser(rows[0]) : null;
463
+ }
464
+ async getUserByEmail(email) {
465
+ const { rows } = await this.pool.query("SELECT * FROM users WHERE email = $1", [email]);
466
+ return rows[0] ? this.mapUser(rows[0]) : null;
467
+ }
468
+ async getUserBySso(provider, subject) {
469
+ const { rows } = await this.pool.query(
470
+ "SELECT * FROM users WHERE sso_provider = $1 AND sso_subject = $2",
471
+ [provider, subject]
472
+ );
473
+ return rows[0] ? this.mapUser(rows[0]) : null;
474
+ }
475
+ async listUsers(opts) {
476
+ let q = "SELECT * FROM users ORDER BY created_at DESC";
477
+ if (opts?.limit) q += ` LIMIT ${opts.limit}`;
478
+ if (opts?.offset) q += ` OFFSET ${opts.offset}`;
479
+ const { rows } = await this.pool.query(q);
480
+ return rows.map((r) => this.mapUser(r));
481
+ }
482
+ async updateUser(id, updates) {
483
+ const fields = [];
484
+ const values = [];
485
+ let i = 1;
486
+ for (const key of ["email", "name", "role", "sso_provider", "sso_subject", "totp_secret", "totp_enabled", "totp_backup_codes", "last_login_at"]) {
487
+ const camelKey = key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
488
+ if (updates[camelKey] !== void 0) {
489
+ fields.push(`${key} = $${i}`);
490
+ values.push(updates[camelKey]);
491
+ i++;
492
+ }
493
+ }
494
+ fields.push("updated_at = NOW()");
495
+ values.push(id);
496
+ const { rows } = await this.pool.query(
497
+ `UPDATE users SET ${fields.join(", ")} WHERE id = $${i} RETURNING *`,
498
+ values
499
+ );
500
+ return this.mapUser(rows[0]);
501
+ }
502
+ async deleteUser(id) {
503
+ await this.pool.query("DELETE FROM users WHERE id = $1", [id]);
504
+ }
505
+ // ─── Audit ───────────────────────────────────────────────
506
+ async logEvent(event) {
507
+ await this.pool.query(
508
+ `INSERT INTO audit_log (id, actor, actor_type, action, resource, details, ip, org_id)
509
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
510
+ [
511
+ randomUUID(),
512
+ event.actor,
513
+ event.actorType,
514
+ event.action,
515
+ event.resource,
516
+ JSON.stringify(event.details || {}),
517
+ event.ip || null,
518
+ event.orgId || null
519
+ ]
520
+ );
521
+ }
522
+ async queryAudit(filters) {
523
+ const where = [];
524
+ const params = [];
525
+ let i = 1;
526
+ if (filters.actor) {
527
+ where.push(`actor = $${i++}`);
528
+ params.push(filters.actor);
529
+ }
530
+ if (filters.action) {
531
+ where.push(`action = $${i++}`);
532
+ params.push(filters.action);
533
+ }
534
+ if (filters.resource) {
535
+ where.push(`resource LIKE $${i++}`);
536
+ params.push(`%${filters.resource}%`);
537
+ }
538
+ if (filters.orgId) {
539
+ where.push(`org_id = $${i++}`);
540
+ params.push(filters.orgId);
541
+ }
542
+ if (filters.from) {
543
+ where.push(`timestamp >= $${i++}`);
544
+ params.push(filters.from);
545
+ }
546
+ if (filters.to) {
547
+ where.push(`timestamp <= $${i++}`);
548
+ params.push(filters.to);
549
+ }
550
+ const whereClause = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
551
+ const countResult = await this.pool.query(`SELECT COUNT(*) FROM audit_log ${whereClause}`, params);
552
+ const total = parseInt(countResult.rows[0].count, 10);
553
+ let q = `SELECT * FROM audit_log ${whereClause} ORDER BY timestamp DESC`;
554
+ if (filters.limit) q += ` LIMIT ${filters.limit}`;
555
+ if (filters.offset) q += ` OFFSET ${filters.offset}`;
556
+ const { rows } = await this.pool.query(q, params);
557
+ return {
558
+ events: rows.map((r) => ({
559
+ id: r.id,
560
+ timestamp: r.timestamp,
561
+ actor: r.actor,
562
+ actorType: r.actor_type,
563
+ action: r.action,
564
+ resource: r.resource,
565
+ details: typeof r.details === "string" ? JSON.parse(r.details || "{}") : r.details || {},
566
+ ip: r.ip
567
+ })),
568
+ total
569
+ };
570
+ }
571
+ // ─── API Keys ────────────────────────────────────────────
572
+ async createApiKey(input) {
573
+ const id = randomUUID();
574
+ const plaintext = `ek_${randomUUID().replace(/-/g, "")}`;
575
+ const keyHash = createHash("sha256").update(plaintext).digest("hex");
576
+ const keyPrefix = plaintext.substring(0, 11);
577
+ const { rows } = await this.pool.query(
578
+ `INSERT INTO api_keys (id, name, key_hash, key_prefix, scopes, created_by, expires_at)
579
+ VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
580
+ [id, input.name, keyHash, keyPrefix, JSON.stringify(input.scopes), input.createdBy, input.expiresAt || null]
581
+ );
582
+ return { key: this.mapApiKey(rows[0]), plaintext };
583
+ }
584
+ async getApiKey(id) {
585
+ const { rows } = await this.pool.query("SELECT * FROM api_keys WHERE id = $1", [id]);
586
+ return rows[0] ? this.mapApiKey(rows[0]) : null;
587
+ }
588
+ async validateApiKey(plaintext) {
589
+ const keyHash = createHash("sha256").update(plaintext).digest("hex");
590
+ const { rows } = await this.pool.query(
591
+ "SELECT * FROM api_keys WHERE key_hash = $1 AND (revoked IS NULL OR revoked = 0)",
592
+ [keyHash]
593
+ );
594
+ if (!rows[0]) return null;
595
+ const key = this.mapApiKey(rows[0]);
596
+ if (key.expiresAt && /* @__PURE__ */ new Date() > key.expiresAt) return null;
597
+ await this.pool.query("UPDATE api_keys SET last_used_at = NOW() WHERE id = $1", [key.id]);
598
+ return key;
599
+ }
600
+ async listApiKeys(opts) {
601
+ let q = "SELECT * FROM api_keys WHERE (revoked IS NULL OR revoked = 0)";
602
+ const params = [];
603
+ if (opts?.createdBy) {
604
+ q += " AND created_by = $" + (params.length + 1);
605
+ params.push(opts.createdBy);
606
+ }
607
+ q += " ORDER BY created_at DESC";
608
+ const { rows } = await this.pool.query(q, params);
609
+ return rows.map((r) => this.mapApiKey(r));
610
+ }
611
+ async revokeApiKey(id) {
612
+ await this.pool.query("UPDATE api_keys SET revoked = 1 WHERE id = $1", [id]);
613
+ }
614
+ // ─── Rules ───────────────────────────────────────────────
615
+ async createRule(rule) {
616
+ const id = randomUUID();
617
+ const { rows } = await this.pool.query(
618
+ `INSERT INTO email_rules (id, name, agent_id, conditions, actions, priority, enabled)
619
+ VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
620
+ [
621
+ id,
622
+ rule.name,
623
+ rule.agentId || null,
624
+ JSON.stringify(rule.conditions),
625
+ JSON.stringify(rule.actions),
626
+ rule.priority,
627
+ rule.enabled ? 1 : 0
628
+ ]
629
+ );
630
+ return this.mapRule(rows[0]);
631
+ }
632
+ async getRules(agentId) {
633
+ let q = "SELECT * FROM email_rules";
634
+ const params = [];
635
+ if (agentId) {
636
+ q += " WHERE agent_id = $1 OR agent_id IS NULL";
637
+ params.push(agentId);
638
+ }
639
+ q += " ORDER BY priority DESC";
640
+ const { rows } = await this.pool.query(q, params);
641
+ return rows.map((r) => this.mapRule(r));
642
+ }
643
+ async updateRule(id, updates) {
644
+ const fields = [];
645
+ const values = [];
646
+ let i = 1;
647
+ if (updates.name !== void 0) {
648
+ fields.push(`name = $${i++}`);
649
+ values.push(updates.name);
650
+ }
651
+ if (updates.conditions) {
652
+ fields.push(`conditions = $${i++}`);
653
+ values.push(JSON.stringify(updates.conditions));
654
+ }
655
+ if (updates.actions) {
656
+ fields.push(`actions = $${i++}`);
657
+ values.push(JSON.stringify(updates.actions));
658
+ }
659
+ if (updates.priority !== void 0) {
660
+ fields.push(`priority = $${i++}`);
661
+ values.push(updates.priority);
662
+ }
663
+ if (updates.enabled !== void 0) {
664
+ fields.push(`enabled = $${i++}`);
665
+ values.push(updates.enabled ? 1 : 0);
666
+ }
667
+ fields.push("updated_at = NOW()");
668
+ values.push(id);
669
+ const { rows } = await this.pool.query(
670
+ `UPDATE email_rules SET ${fields.join(", ")} WHERE id = $${i} RETURNING *`,
671
+ values
672
+ );
673
+ return this.mapRule(rows[0]);
674
+ }
675
+ async deleteRule(id) {
676
+ await this.pool.query("DELETE FROM email_rules WHERE id = $1", [id]);
677
+ }
678
+ // ─── Retention ───────────────────────────────────────────
679
+ async getRetentionPolicy() {
680
+ const { rows } = await this.pool.query("SELECT * FROM retention_policy WHERE id = $1", ["default"]);
681
+ if (!rows[0]) return { enabled: false, retainDays: 365, archiveFirst: true };
682
+ return {
683
+ enabled: !!rows[0].enabled,
684
+ retainDays: rows[0].retain_days,
685
+ excludeTags: typeof rows[0].exclude_tags === "string" ? JSON.parse(rows[0].exclude_tags || "[]") : rows[0].exclude_tags || [],
686
+ archiveFirst: !!rows[0].archive_first
687
+ };
688
+ }
689
+ async setRetentionPolicy(policy) {
690
+ await this.pool.query(
691
+ `UPDATE retention_policy SET enabled = $1, retain_days = $2, exclude_tags = $3, archive_first = $4
692
+ WHERE id = 'default'`,
693
+ [policy.enabled ? 1 : 0, policy.retainDays, JSON.stringify(policy.excludeTags || []), policy.archiveFirst ? 1 : 0]
694
+ );
695
+ }
696
+ // ─── Stats ───────────────────────────────────────────────
697
+ async getStats() {
698
+ const [agents, active, users, audit] = await Promise.all([
699
+ this.pool.query("SELECT COUNT(*) FROM agents"),
700
+ this.pool.query("SELECT COUNT(*) FROM agents WHERE status = 'active'"),
701
+ this.pool.query("SELECT COUNT(*) FROM users"),
702
+ this.pool.query("SELECT COUNT(*) FROM audit_log")
703
+ ]);
704
+ return {
705
+ totalAgents: parseInt(agents.rows[0].count, 10),
706
+ activeAgents: parseInt(active.rows[0].count, 10),
707
+ totalUsers: parseInt(users.rows[0].count, 10),
708
+ totalEmails: 0,
709
+ // Email count tracked externally via AgenticMail
710
+ totalAuditEvents: parseInt(audit.rows[0].count, 10)
711
+ };
712
+ }
713
+ // ─── Security Events ─────────────────────────────────────
714
+ async logSecurityEvent(event) {
715
+ const id = randomUUID();
716
+ await this.pool.query(
717
+ `INSERT INTO security_events (id, event_type, severity, agent_id, details, source_ip)
718
+ VALUES ($1, $2, $3, $4, $5, $6)`,
719
+ [id, event.eventType, event.severity, event.agentId, JSON.stringify(event.details), event.sourceIp]
720
+ );
721
+ }
722
+ async getSecurityEvents(filter = {}) {
723
+ let query = "SELECT * FROM security_events WHERE 1=1";
724
+ const params = [];
725
+ let paramIndex = 1;
726
+ if (filter.eventType && filter.eventType.length > 0) {
727
+ query += ` AND event_type = ANY($${paramIndex++})`;
728
+ params.push(filter.eventType);
729
+ }
730
+ if (filter.severity && filter.severity.length > 0) {
731
+ query += ` AND severity = ANY($${paramIndex++})`;
732
+ params.push(filter.severity);
733
+ }
734
+ if (filter.agentId) {
735
+ query += ` AND agent_id = $${paramIndex++}`;
736
+ params.push(filter.agentId);
737
+ }
738
+ if (filter.sourceIp) {
739
+ query += ` AND source_ip = $${paramIndex++}`;
740
+ params.push(filter.sourceIp);
741
+ }
742
+ if (filter.fromDate) {
743
+ query += ` AND created_at >= $${paramIndex++}`;
744
+ params.push(filter.fromDate);
745
+ }
746
+ if (filter.toDate) {
747
+ query += ` AND created_at <= $${paramIndex++}`;
748
+ params.push(filter.toDate);
749
+ }
750
+ query += " ORDER BY created_at DESC";
751
+ if (filter.limit) {
752
+ query += ` LIMIT ${filter.limit}`;
753
+ }
754
+ if (filter.offset) {
755
+ query += ` OFFSET ${filter.offset}`;
756
+ }
757
+ const { rows } = await this.pool.query(query, params);
758
+ return rows.map((row) => ({
759
+ id: row.id,
760
+ eventType: row.event_type,
761
+ severity: row.severity,
762
+ agentId: row.agent_id,
763
+ details: typeof row.details === "string" ? JSON.parse(row.details) : row.details,
764
+ sourceIp: row.source_ip,
765
+ timestamp: row.created_at
766
+ }));
767
+ }
768
+ // ─── Mappers ─────────────────────────────────────────────
769
+ mapAgent(r) {
770
+ return {
771
+ id: r.id,
772
+ name: r.name,
773
+ email: r.email,
774
+ role: r.role,
775
+ status: r.status,
776
+ metadata: typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata,
777
+ securityOverrides: r.security_overrides ? typeof r.security_overrides === "string" ? JSON.parse(r.security_overrides) : r.security_overrides : void 0,
778
+ createdAt: new Date(r.created_at),
779
+ updatedAt: new Date(r.updated_at),
780
+ createdBy: r.created_by,
781
+ client_org_id: r.client_org_id || null
782
+ };
783
+ }
784
+ mapUser(r) {
785
+ return {
786
+ id: r.id,
787
+ email: r.email,
788
+ name: r.name,
789
+ role: r.role,
790
+ passwordHash: r.password_hash,
791
+ ssoProvider: r.sso_provider,
792
+ ssoSubject: r.sso_subject,
793
+ totpSecret: r.totp_secret,
794
+ totpEnabled: !!r.totp_enabled,
795
+ totpBackupCodes: r.totp_backup_codes,
796
+ permissions: r.permissions != null ? typeof r.permissions === "string" ? (() => {
797
+ try {
798
+ return JSON.parse(r.permissions);
799
+ } catch {
800
+ return "*";
801
+ }
802
+ })() : r.permissions : "*",
803
+ mustResetPassword: !!r.must_reset_password,
804
+ isActive: r.is_active !== false && r.is_active !== 0,
805
+ // default true
806
+ clientOrgId: r.client_org_id || null,
807
+ createdAt: new Date(r.created_at),
808
+ updatedAt: new Date(r.updated_at),
809
+ lastLoginAt: r.last_login_at ? new Date(r.last_login_at) : void 0
810
+ };
811
+ }
812
+ mapApiKey(r) {
813
+ return {
814
+ id: r.id,
815
+ name: r.name,
816
+ keyHash: r.key_hash,
817
+ keyPrefix: r.key_prefix,
818
+ scopes: typeof r.scopes === "string" ? JSON.parse(r.scopes) : r.scopes,
819
+ createdBy: r.created_by,
820
+ createdAt: new Date(r.created_at),
821
+ lastUsedAt: r.last_used_at ? new Date(r.last_used_at) : void 0,
822
+ expiresAt: r.expires_at ? new Date(r.expires_at) : void 0,
823
+ revoked: !!r.revoked
824
+ };
825
+ }
826
+ mapRule(r) {
827
+ return {
828
+ id: r.id,
829
+ name: r.name,
830
+ agentId: r.agent_id,
831
+ conditions: typeof r.conditions === "string" ? JSON.parse(r.conditions) : r.conditions,
832
+ actions: typeof r.actions === "string" ? JSON.parse(r.actions) : r.actions,
833
+ priority: r.priority,
834
+ enabled: !!r.enabled,
835
+ createdAt: new Date(r.created_at),
836
+ updatedAt: new Date(r.updated_at)
837
+ };
838
+ }
839
+ mapSettings(r) {
840
+ return {
841
+ id: r.id,
842
+ orgId: r.org_id || void 0,
843
+ name: r.name,
844
+ domain: r.domain,
845
+ subdomain: r.subdomain,
846
+ smtpHost: r.smtp_host,
847
+ smtpPort: r.smtp_port,
848
+ smtpUser: r.smtp_user,
849
+ smtpPass: r.smtp_pass,
850
+ dkimPrivateKey: r.dkim_private_key,
851
+ logoUrl: r.logo_url,
852
+ primaryColor: r.primary_color,
853
+ ssoConfig: r.sso_config ? typeof r.sso_config === "string" ? JSON.parse(r.sso_config) : r.sso_config : void 0,
854
+ toolSecurityConfig: r.tool_security_config ? typeof r.tool_security_config === "string" ? JSON.parse(r.tool_security_config) : r.tool_security_config : {},
855
+ firewallConfig: r.firewall_config ? typeof r.firewall_config === "string" ? JSON.parse(r.firewall_config) : r.firewall_config : {},
856
+ securityConfig: r.security_config ? typeof r.security_config === "string" ? JSON.parse(r.security_config) : r.security_config : {},
857
+ modelPricingConfig: r.model_pricing_config ? typeof r.model_pricing_config === "string" ? JSON.parse(r.model_pricing_config) : r.model_pricing_config : {},
858
+ plan: r.plan,
859
+ createdAt: new Date(r.created_at),
860
+ updatedAt: new Date(r.updated_at),
861
+ deploymentKeyHash: r.deployment_key_hash,
862
+ domainRegistrationId: r.domain_registration_id,
863
+ domainDnsChallenge: r.domain_dns_challenge,
864
+ domainVerifiedAt: r.domain_verified_at || void 0,
865
+ domainRegisteredAt: r.domain_registered_at || void 0,
866
+ domainStatus: r.domain_status || "unregistered",
867
+ useRootDomain: r.use_root_domain || false,
868
+ cfApiToken: r.cf_api_token || void 0,
869
+ cfAccountId: r.cf_account_id || void 0,
870
+ orgEmailConfig: r.org_email_config ? typeof r.org_email_config === "string" ? JSON.parse(r.org_email_config) : r.org_email_config : void 0,
871
+ platformCapabilities: r.platform_capabilities ? typeof r.platform_capabilities === "string" ? JSON.parse(r.platform_capabilities) : r.platform_capabilities : void 0,
872
+ signatureTemplate: r.signature_template || void 0,
873
+ branding: r.branding ? typeof r.branding === "string" ? JSON.parse(r.branding) : r.branding : void 0
874
+ };
875
+ }
876
+ };
877
+ export {
878
+ PostgresAdapter
879
+ };