@agenticmail/enterprise 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +183 -0
- package/agenticmail-enterprise.db +0 -0
- package/dashboards/README.md +120 -0
- package/dashboards/dotnet/Program.cs +261 -0
- package/dashboards/express/app.js +146 -0
- package/dashboards/go/main.go +513 -0
- package/dashboards/html/index.html +535 -0
- package/dashboards/java/AgenticMailDashboard.java +376 -0
- package/dashboards/php/index.php +414 -0
- package/dashboards/python/app.py +273 -0
- package/dashboards/ruby/app.rb +195 -0
- package/dist/chunk-77IDQJL3.js +7 -0
- package/dist/chunk-7RGCCHIT.js +115 -0
- package/dist/chunk-DXNKR3TG.js +1355 -0
- package/dist/chunk-IQWA44WT.js +970 -0
- package/dist/chunk-LCUZGIDH.js +965 -0
- package/dist/chunk-N2JVTNNJ.js +2553 -0
- package/dist/chunk-O462UJBH.js +363 -0
- package/dist/chunk-PNKVD2UK.js +26 -0
- package/dist/cli.js +218 -0
- package/dist/dashboard/index.html +558 -0
- package/dist/db-adapter-DEWEFNIV.js +7 -0
- package/dist/dynamodb-CCGL2E77.js +426 -0
- package/dist/engine/index.js +1261 -0
- package/dist/index.js +522 -0
- package/dist/mongodb-ODTXIVPV.js +319 -0
- package/dist/mysql-RM3S2FV5.js +521 -0
- package/dist/postgres-LN7A6MGQ.js +518 -0
- package/dist/routes-2JEPIIKC.js +441 -0
- package/dist/routes-74ZLKJKP.js +399 -0
- package/dist/server.js +7 -0
- package/dist/sqlite-3K5YOZ4K.js +439 -0
- package/dist/turso-LDWODSDI.js +442 -0
- package/package.json +49 -0
- package/src/admin/routes.ts +331 -0
- package/src/auth/routes.ts +130 -0
- package/src/cli.ts +260 -0
- package/src/dashboard/index.html +558 -0
- package/src/db/adapter.ts +230 -0
- package/src/db/dynamodb.ts +456 -0
- package/src/db/factory.ts +51 -0
- package/src/db/mongodb.ts +360 -0
- package/src/db/mysql.ts +472 -0
- package/src/db/postgres.ts +479 -0
- package/src/db/sql-schema.ts +123 -0
- package/src/db/sqlite.ts +391 -0
- package/src/db/turso.ts +411 -0
- package/src/deploy/fly.ts +368 -0
- package/src/deploy/managed.ts +213 -0
- package/src/engine/activity.ts +474 -0
- package/src/engine/agent-config.ts +429 -0
- package/src/engine/agenticmail-bridge.ts +296 -0
- package/src/engine/approvals.ts +278 -0
- package/src/engine/db-adapter.ts +682 -0
- package/src/engine/db-schema.ts +335 -0
- package/src/engine/deployer.ts +595 -0
- package/src/engine/index.ts +134 -0
- package/src/engine/knowledge.ts +486 -0
- package/src/engine/lifecycle.ts +635 -0
- package/src/engine/openclaw-hook.ts +371 -0
- package/src/engine/routes.ts +528 -0
- package/src/engine/skills.ts +473 -0
- package/src/engine/tenant.ts +345 -0
- package/src/engine/tool-catalog.ts +189 -0
- package/src/index.ts +64 -0
- package/src/lib/resilience.ts +326 -0
- package/src/middleware/index.ts +286 -0
- package/src/server.ts +310 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Database Adapter
|
|
3
|
+
*
|
|
4
|
+
* Also works with Supabase, Neon, CockroachDB (all PG-compatible).
|
|
5
|
+
* Uses 'pg' driver — user must install: npm install pg
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID, createHash } from 'crypto';
|
|
9
|
+
import {
|
|
10
|
+
DatabaseAdapter, DatabaseConfig,
|
|
11
|
+
Agent, AgentInput, User, UserInput,
|
|
12
|
+
AuditEvent, AuditFilters, ApiKey, ApiKeyInput,
|
|
13
|
+
EmailRule, RetentionPolicy, CompanySettings,
|
|
14
|
+
} from './adapter.js';
|
|
15
|
+
import { getAllCreateStatements } from './sql-schema.js';
|
|
16
|
+
|
|
17
|
+
let pg: any;
|
|
18
|
+
|
|
19
|
+
async function getPg() {
|
|
20
|
+
if (!pg) {
|
|
21
|
+
try {
|
|
22
|
+
pg = await import('pg');
|
|
23
|
+
} catch {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'PostgreSQL driver not found. Install it: npm install pg\n' +
|
|
26
|
+
'For Supabase/Neon/CockroachDB, the same pg driver works.'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return pg;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class PostgresAdapter extends DatabaseAdapter {
|
|
34
|
+
readonly type = 'postgres' as const;
|
|
35
|
+
private pool: any = null;
|
|
36
|
+
|
|
37
|
+
async connect(config: DatabaseConfig): Promise<void> {
|
|
38
|
+
const { Pool } = await getPg();
|
|
39
|
+
this.pool = new Pool({
|
|
40
|
+
connectionString: config.connectionString,
|
|
41
|
+
host: config.host,
|
|
42
|
+
port: config.port,
|
|
43
|
+
database: config.database,
|
|
44
|
+
user: config.username,
|
|
45
|
+
password: config.password,
|
|
46
|
+
ssl: config.ssl ? { rejectUnauthorized: false } : undefined,
|
|
47
|
+
max: 20,
|
|
48
|
+
idleTimeoutMillis: 30000,
|
|
49
|
+
});
|
|
50
|
+
// Test connection
|
|
51
|
+
const client = await this.pool.connect();
|
|
52
|
+
client.release();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async disconnect(): Promise<void> {
|
|
56
|
+
if (this.pool) await this.pool.end();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
isConnected(): boolean {
|
|
60
|
+
return this.pool !== null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async migrate(): Promise<void> {
|
|
64
|
+
const stmts = getAllCreateStatements();
|
|
65
|
+
const client = await this.pool.connect();
|
|
66
|
+
try {
|
|
67
|
+
await client.query('BEGIN');
|
|
68
|
+
for (const stmt of stmts) {
|
|
69
|
+
await client.query(stmt);
|
|
70
|
+
}
|
|
71
|
+
// Seed retention policy
|
|
72
|
+
await client.query(`
|
|
73
|
+
INSERT INTO retention_policy (id) VALUES ('default')
|
|
74
|
+
ON CONFLICT (id) DO NOTHING
|
|
75
|
+
`);
|
|
76
|
+
await client.query('COMMIT');
|
|
77
|
+
} catch (err) {
|
|
78
|
+
await client.query('ROLLBACK');
|
|
79
|
+
throw err;
|
|
80
|
+
} finally {
|
|
81
|
+
client.release();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Company ─────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
async getSettings(): Promise<CompanySettings> {
|
|
88
|
+
const { rows } = await this.pool.query(
|
|
89
|
+
'SELECT * FROM company_settings WHERE id = $1', ['default']
|
|
90
|
+
);
|
|
91
|
+
return rows[0] ? this.mapSettings(rows[0]) : null!;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async updateSettings(updates: Partial<CompanySettings>): Promise<CompanySettings> {
|
|
95
|
+
const fields: string[] = [];
|
|
96
|
+
const values: any[] = [];
|
|
97
|
+
let i = 1;
|
|
98
|
+
const map: Record<string, string> = {
|
|
99
|
+
name: 'name', domain: 'domain', subdomain: 'subdomain',
|
|
100
|
+
smtpHost: 'smtp_host', smtpPort: 'smtp_port', smtpUser: 'smtp_user',
|
|
101
|
+
smtpPass: 'smtp_pass', dkimPrivateKey: 'dkim_private_key',
|
|
102
|
+
logoUrl: 'logo_url', primaryColor: 'primary_color', plan: 'plan',
|
|
103
|
+
};
|
|
104
|
+
for (const [key, col] of Object.entries(map)) {
|
|
105
|
+
if ((updates as any)[key] !== undefined) {
|
|
106
|
+
fields.push(`${col} = $${i}`);
|
|
107
|
+
values.push((updates as any)[key]);
|
|
108
|
+
i++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
fields.push(`updated_at = NOW()`);
|
|
112
|
+
values.push('default');
|
|
113
|
+
const { rows } = await this.pool.query(
|
|
114
|
+
`UPDATE company_settings SET ${fields.join(', ')} WHERE id = $${i} RETURNING *`,
|
|
115
|
+
values
|
|
116
|
+
);
|
|
117
|
+
return this.mapSettings(rows[0]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Agents ──────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
async createAgent(input: AgentInput): Promise<Agent> {
|
|
123
|
+
const id = randomUUID();
|
|
124
|
+
const email = input.email || `${input.name.toLowerCase().replace(/\s+/g, '-')}@localhost`;
|
|
125
|
+
const { rows } = await this.pool.query(
|
|
126
|
+
`INSERT INTO agents (id, name, email, role, metadata, created_by)
|
|
127
|
+
VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
|
|
128
|
+
[id, input.name, email, input.role || 'assistant', JSON.stringify(input.metadata || {}), input.createdBy]
|
|
129
|
+
);
|
|
130
|
+
return this.mapAgent(rows[0]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async getAgent(id: string): Promise<Agent | null> {
|
|
134
|
+
const { rows } = await this.pool.query('SELECT * FROM agents WHERE id = $1', [id]);
|
|
135
|
+
return rows[0] ? this.mapAgent(rows[0]) : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async getAgentByName(name: string): Promise<Agent | null> {
|
|
139
|
+
const { rows } = await this.pool.query('SELECT * FROM agents WHERE name = $1', [name]);
|
|
140
|
+
return rows[0] ? this.mapAgent(rows[0]) : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async listAgents(opts?: { status?: string; limit?: number; offset?: number }): Promise<Agent[]> {
|
|
144
|
+
let q = 'SELECT * FROM agents';
|
|
145
|
+
const params: any[] = [];
|
|
146
|
+
if (opts?.status) { q += ' WHERE status = $1'; params.push(opts.status); }
|
|
147
|
+
q += ' ORDER BY created_at DESC';
|
|
148
|
+
if (opts?.limit) { q += ` LIMIT ${opts.limit}`; }
|
|
149
|
+
if (opts?.offset) { q += ` OFFSET ${opts.offset}`; }
|
|
150
|
+
const { rows } = await this.pool.query(q, params);
|
|
151
|
+
return rows.map((r: any) => this.mapAgent(r));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async updateAgent(id: string, updates: Partial<Agent>): Promise<Agent> {
|
|
155
|
+
const fields: string[] = [];
|
|
156
|
+
const values: any[] = [];
|
|
157
|
+
let i = 1;
|
|
158
|
+
for (const [key, col] of Object.entries({ name: 'name', email: 'email', role: 'role', status: 'status' })) {
|
|
159
|
+
if ((updates as any)[key] !== undefined) {
|
|
160
|
+
fields.push(`${col} = $${i}`);
|
|
161
|
+
values.push((updates as any)[key]);
|
|
162
|
+
i++;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (updates.metadata) {
|
|
166
|
+
fields.push(`metadata = $${i}`);
|
|
167
|
+
values.push(JSON.stringify(updates.metadata));
|
|
168
|
+
i++;
|
|
169
|
+
}
|
|
170
|
+
fields.push('updated_at = NOW()');
|
|
171
|
+
values.push(id);
|
|
172
|
+
const { rows } = await this.pool.query(
|
|
173
|
+
`UPDATE agents SET ${fields.join(', ')} WHERE id = $${i} RETURNING *`,
|
|
174
|
+
values
|
|
175
|
+
);
|
|
176
|
+
return this.mapAgent(rows[0]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async archiveAgent(id: string): Promise<void> {
|
|
180
|
+
await this.pool.query("UPDATE agents SET status = 'archived', updated_at = NOW() WHERE id = $1", [id]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async deleteAgent(id: string): Promise<void> {
|
|
184
|
+
await this.pool.query('DELETE FROM agents WHERE id = $1', [id]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async countAgents(status?: string): Promise<number> {
|
|
188
|
+
const q = status
|
|
189
|
+
? await this.pool.query('SELECT COUNT(*) FROM agents WHERE status = $1', [status])
|
|
190
|
+
: await this.pool.query('SELECT COUNT(*) FROM agents');
|
|
191
|
+
return parseInt(q.rows[0].count, 10);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─── Users ───────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
async createUser(input: UserInput): Promise<User> {
|
|
197
|
+
const id = randomUUID();
|
|
198
|
+
let passwordHash: string | null = null;
|
|
199
|
+
if (input.password) {
|
|
200
|
+
const { default: bcrypt } = await import('bcryptjs');
|
|
201
|
+
passwordHash = await bcrypt.hash(input.password, 12);
|
|
202
|
+
}
|
|
203
|
+
const { rows } = await this.pool.query(
|
|
204
|
+
`INSERT INTO users (id, email, name, role, password_hash, sso_provider, sso_subject)
|
|
205
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
|
|
206
|
+
[id, input.email, input.name, input.role, passwordHash, input.ssoProvider || null, input.ssoSubject || null]
|
|
207
|
+
);
|
|
208
|
+
return this.mapUser(rows[0]);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async getUser(id: string): Promise<User | null> {
|
|
212
|
+
const { rows } = await this.pool.query('SELECT * FROM users WHERE id = $1', [id]);
|
|
213
|
+
return rows[0] ? this.mapUser(rows[0]) : null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async getUserByEmail(email: string): Promise<User | null> {
|
|
217
|
+
const { rows } = await this.pool.query('SELECT * FROM users WHERE email = $1', [email]);
|
|
218
|
+
return rows[0] ? this.mapUser(rows[0]) : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async getUserBySso(provider: string, subject: string): Promise<User | null> {
|
|
222
|
+
const { rows } = await this.pool.query(
|
|
223
|
+
'SELECT * FROM users WHERE sso_provider = $1 AND sso_subject = $2',
|
|
224
|
+
[provider, subject]
|
|
225
|
+
);
|
|
226
|
+
return rows[0] ? this.mapUser(rows[0]) : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async listUsers(opts?: { limit?: number; offset?: number }): Promise<User[]> {
|
|
230
|
+
let q = 'SELECT * FROM users ORDER BY created_at DESC';
|
|
231
|
+
if (opts?.limit) q += ` LIMIT ${opts.limit}`;
|
|
232
|
+
if (opts?.offset) q += ` OFFSET ${opts.offset}`;
|
|
233
|
+
const { rows } = await this.pool.query(q);
|
|
234
|
+
return rows.map((r: any) => this.mapUser(r));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async updateUser(id: string, updates: Partial<User>): Promise<User> {
|
|
238
|
+
const fields: string[] = [];
|
|
239
|
+
const values: any[] = [];
|
|
240
|
+
let i = 1;
|
|
241
|
+
for (const key of ['email', 'name', 'role', 'sso_provider', 'sso_subject'] as const) {
|
|
242
|
+
const camelKey = key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
243
|
+
if ((updates as any)[camelKey] !== undefined) {
|
|
244
|
+
fields.push(`${key} = $${i}`);
|
|
245
|
+
values.push((updates as any)[camelKey]);
|
|
246
|
+
i++;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
fields.push('updated_at = NOW()');
|
|
250
|
+
values.push(id);
|
|
251
|
+
const { rows } = await this.pool.query(
|
|
252
|
+
`UPDATE users SET ${fields.join(', ')} WHERE id = $${i} RETURNING *`,
|
|
253
|
+
values
|
|
254
|
+
);
|
|
255
|
+
return this.mapUser(rows[0]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async deleteUser(id: string): Promise<void> {
|
|
259
|
+
await this.pool.query('DELETE FROM users WHERE id = $1', [id]);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ─── Audit ───────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
async logEvent(event: Omit<AuditEvent, 'id' | 'timestamp'>): Promise<void> {
|
|
265
|
+
await this.pool.query(
|
|
266
|
+
`INSERT INTO audit_log (id, actor, actor_type, action, resource, details, ip)
|
|
267
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
268
|
+
[randomUUID(), event.actor, event.actorType, event.action, event.resource,
|
|
269
|
+
JSON.stringify(event.details || {}), event.ip || null]
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async queryAudit(filters: AuditFilters): Promise<{ events: AuditEvent[]; total: number }> {
|
|
274
|
+
const where: string[] = [];
|
|
275
|
+
const params: any[] = [];
|
|
276
|
+
let i = 1;
|
|
277
|
+
if (filters.actor) { where.push(`actor = $${i++}`); params.push(filters.actor); }
|
|
278
|
+
if (filters.action) { where.push(`action = $${i++}`); params.push(filters.action); }
|
|
279
|
+
if (filters.resource) { where.push(`resource LIKE $${i++}`); params.push(`%${filters.resource}%`); }
|
|
280
|
+
if (filters.from) { where.push(`timestamp >= $${i++}`); params.push(filters.from); }
|
|
281
|
+
if (filters.to) { where.push(`timestamp <= $${i++}`); params.push(filters.to); }
|
|
282
|
+
|
|
283
|
+
const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
|
284
|
+
const countResult = await this.pool.query(`SELECT COUNT(*) FROM audit_log ${whereClause}`, params);
|
|
285
|
+
const total = parseInt(countResult.rows[0].count, 10);
|
|
286
|
+
|
|
287
|
+
let q = `SELECT * FROM audit_log ${whereClause} ORDER BY timestamp DESC`;
|
|
288
|
+
if (filters.limit) q += ` LIMIT ${filters.limit}`;
|
|
289
|
+
if (filters.offset) q += ` OFFSET ${filters.offset}`;
|
|
290
|
+
const { rows } = await this.pool.query(q, params);
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
events: rows.map((r: any) => ({
|
|
294
|
+
id: r.id, timestamp: r.timestamp, actor: r.actor, actorType: r.actor_type,
|
|
295
|
+
action: r.action, resource: r.resource, details: JSON.parse(r.details || '{}'), ip: r.ip,
|
|
296
|
+
})),
|
|
297
|
+
total,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ─── API Keys ────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
async createApiKey(input: ApiKeyInput): Promise<{ key: ApiKey; plaintext: string }> {
|
|
304
|
+
const id = randomUUID();
|
|
305
|
+
const plaintext = `ek_${randomUUID().replace(/-/g, '')}`;
|
|
306
|
+
const keyHash = createHash('sha256').update(plaintext).digest('hex');
|
|
307
|
+
const keyPrefix = plaintext.substring(0, 11); // "ek_" + 8 chars
|
|
308
|
+
const { rows } = await this.pool.query(
|
|
309
|
+
`INSERT INTO api_keys (id, name, key_hash, key_prefix, scopes, created_by, expires_at)
|
|
310
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
|
|
311
|
+
[id, input.name, keyHash, keyPrefix, JSON.stringify(input.scopes), input.createdBy, input.expiresAt || null]
|
|
312
|
+
);
|
|
313
|
+
return { key: this.mapApiKey(rows[0]), plaintext };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async getApiKey(id: string): Promise<ApiKey | null> {
|
|
317
|
+
const { rows } = await this.pool.query('SELECT * FROM api_keys WHERE id = $1', [id]);
|
|
318
|
+
return rows[0] ? this.mapApiKey(rows[0]) : null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async validateApiKey(plaintext: string): Promise<ApiKey | null> {
|
|
322
|
+
const keyHash = createHash('sha256').update(plaintext).digest('hex');
|
|
323
|
+
const { rows } = await this.pool.query(
|
|
324
|
+
'SELECT * FROM api_keys WHERE key_hash = $1 AND revoked = 0',
|
|
325
|
+
[keyHash]
|
|
326
|
+
);
|
|
327
|
+
if (!rows[0]) return null;
|
|
328
|
+
const key = this.mapApiKey(rows[0]);
|
|
329
|
+
if (key.expiresAt && new Date() > key.expiresAt) return null;
|
|
330
|
+
// Update last used
|
|
331
|
+
await this.pool.query('UPDATE api_keys SET last_used_at = NOW() WHERE id = $1', [key.id]);
|
|
332
|
+
return key;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async listApiKeys(opts?: { createdBy?: string }): Promise<ApiKey[]> {
|
|
336
|
+
let q = 'SELECT * FROM api_keys';
|
|
337
|
+
const params: any[] = [];
|
|
338
|
+
if (opts?.createdBy) { q += ' WHERE created_by = $1'; params.push(opts.createdBy); }
|
|
339
|
+
q += ' ORDER BY created_at DESC';
|
|
340
|
+
const { rows } = await this.pool.query(q, params);
|
|
341
|
+
return rows.map((r: any) => this.mapApiKey(r));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async revokeApiKey(id: string): Promise<void> {
|
|
345
|
+
await this.pool.query('UPDATE api_keys SET revoked = 1 WHERE id = $1', [id]);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ─── Rules ───────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
async createRule(rule: Omit<EmailRule, 'id' | 'createdAt' | 'updatedAt'>): Promise<EmailRule> {
|
|
351
|
+
const id = randomUUID();
|
|
352
|
+
const { rows } = await this.pool.query(
|
|
353
|
+
`INSERT INTO email_rules (id, name, agent_id, conditions, actions, priority, enabled)
|
|
354
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
|
|
355
|
+
[id, rule.name, rule.agentId || null, JSON.stringify(rule.conditions), JSON.stringify(rule.actions),
|
|
356
|
+
rule.priority, rule.enabled ? 1 : 0]
|
|
357
|
+
);
|
|
358
|
+
return this.mapRule(rows[0]);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async getRules(agentId?: string): Promise<EmailRule[]> {
|
|
362
|
+
let q = 'SELECT * FROM email_rules';
|
|
363
|
+
const params: any[] = [];
|
|
364
|
+
if (agentId) { q += ' WHERE agent_id = $1 OR agent_id IS NULL'; params.push(agentId); }
|
|
365
|
+
q += ' ORDER BY priority DESC';
|
|
366
|
+
const { rows } = await this.pool.query(q, params);
|
|
367
|
+
return rows.map((r: any) => this.mapRule(r));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async updateRule(id: string, updates: Partial<EmailRule>): Promise<EmailRule> {
|
|
371
|
+
const fields: string[] = [];
|
|
372
|
+
const values: any[] = [];
|
|
373
|
+
let i = 1;
|
|
374
|
+
if (updates.name !== undefined) { fields.push(`name = $${i++}`); values.push(updates.name); }
|
|
375
|
+
if (updates.conditions) { fields.push(`conditions = $${i++}`); values.push(JSON.stringify(updates.conditions)); }
|
|
376
|
+
if (updates.actions) { fields.push(`actions = $${i++}`); values.push(JSON.stringify(updates.actions)); }
|
|
377
|
+
if (updates.priority !== undefined) { fields.push(`priority = $${i++}`); values.push(updates.priority); }
|
|
378
|
+
if (updates.enabled !== undefined) { fields.push(`enabled = $${i++}`); values.push(updates.enabled ? 1 : 0); }
|
|
379
|
+
fields.push('updated_at = NOW()');
|
|
380
|
+
values.push(id);
|
|
381
|
+
const { rows } = await this.pool.query(
|
|
382
|
+
`UPDATE email_rules SET ${fields.join(', ')} WHERE id = $${i} RETURNING *`,
|
|
383
|
+
values
|
|
384
|
+
);
|
|
385
|
+
return this.mapRule(rows[0]);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async deleteRule(id: string): Promise<void> {
|
|
389
|
+
await this.pool.query('DELETE FROM email_rules WHERE id = $1', [id]);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ─── Retention ───────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
async getRetentionPolicy(): Promise<RetentionPolicy> {
|
|
395
|
+
const { rows } = await this.pool.query('SELECT * FROM retention_policy WHERE id = $1', ['default']);
|
|
396
|
+
if (!rows[0]) return { enabled: false, retainDays: 365, archiveFirst: true };
|
|
397
|
+
return {
|
|
398
|
+
enabled: !!rows[0].enabled,
|
|
399
|
+
retainDays: rows[0].retain_days,
|
|
400
|
+
excludeTags: JSON.parse(rows[0].exclude_tags || '[]'),
|
|
401
|
+
archiveFirst: !!rows[0].archive_first,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async setRetentionPolicy(policy: RetentionPolicy): Promise<void> {
|
|
406
|
+
await this.pool.query(
|
|
407
|
+
`UPDATE retention_policy SET enabled = $1, retain_days = $2, exclude_tags = $3, archive_first = $4
|
|
408
|
+
WHERE id = 'default'`,
|
|
409
|
+
[policy.enabled ? 1 : 0, policy.retainDays, JSON.stringify(policy.excludeTags || []), policy.archiveFirst ? 1 : 0]
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ─── Stats ───────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
async getStats() {
|
|
416
|
+
const [agents, active, users, audit] = await Promise.all([
|
|
417
|
+
this.pool.query('SELECT COUNT(*) FROM agents'),
|
|
418
|
+
this.pool.query("SELECT COUNT(*) FROM agents WHERE status = 'active'"),
|
|
419
|
+
this.pool.query('SELECT COUNT(*) FROM users'),
|
|
420
|
+
this.pool.query('SELECT COUNT(*) FROM audit_log'),
|
|
421
|
+
]);
|
|
422
|
+
return {
|
|
423
|
+
totalAgents: parseInt(agents.rows[0].count, 10),
|
|
424
|
+
activeAgents: parseInt(active.rows[0].count, 10),
|
|
425
|
+
totalUsers: parseInt(users.rows[0].count, 10),
|
|
426
|
+
totalEmails: 0, // TODO: wire to email storage
|
|
427
|
+
totalAuditEvents: parseInt(audit.rows[0].count, 10),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ─── Mappers ─────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
private mapAgent(r: any): Agent {
|
|
434
|
+
return {
|
|
435
|
+
id: r.id, name: r.name, email: r.email, role: r.role, status: r.status,
|
|
436
|
+
metadata: typeof r.metadata === 'string' ? JSON.parse(r.metadata) : r.metadata,
|
|
437
|
+
createdAt: new Date(r.created_at), updatedAt: new Date(r.updated_at), createdBy: r.created_by,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private mapUser(r: any): User {
|
|
442
|
+
return {
|
|
443
|
+
id: r.id, email: r.email, name: r.name, role: r.role,
|
|
444
|
+
passwordHash: r.password_hash, ssoProvider: r.sso_provider, ssoSubject: r.sso_subject,
|
|
445
|
+
createdAt: new Date(r.created_at), updatedAt: new Date(r.updated_at),
|
|
446
|
+
lastLoginAt: r.last_login_at ? new Date(r.last_login_at) : undefined,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private mapApiKey(r: any): ApiKey {
|
|
451
|
+
return {
|
|
452
|
+
id: r.id, name: r.name, keyHash: r.key_hash, keyPrefix: r.key_prefix,
|
|
453
|
+
scopes: typeof r.scopes === 'string' ? JSON.parse(r.scopes) : r.scopes,
|
|
454
|
+
createdBy: r.created_by, createdAt: new Date(r.created_at),
|
|
455
|
+
lastUsedAt: r.last_used_at ? new Date(r.last_used_at) : undefined,
|
|
456
|
+
expiresAt: r.expires_at ? new Date(r.expires_at) : undefined,
|
|
457
|
+
revoked: !!r.revoked,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private mapRule(r: any): EmailRule {
|
|
462
|
+
return {
|
|
463
|
+
id: r.id, name: r.name, agentId: r.agent_id,
|
|
464
|
+
conditions: typeof r.conditions === 'string' ? JSON.parse(r.conditions) : r.conditions,
|
|
465
|
+
actions: typeof r.actions === 'string' ? JSON.parse(r.actions) : r.actions,
|
|
466
|
+
priority: r.priority, enabled: !!r.enabled,
|
|
467
|
+
createdAt: new Date(r.created_at), updatedAt: new Date(r.updated_at),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private mapSettings(r: any): CompanySettings {
|
|
472
|
+
return {
|
|
473
|
+
id: r.id, name: r.name, domain: r.domain, subdomain: r.subdomain,
|
|
474
|
+
smtpHost: r.smtp_host, smtpPort: r.smtp_port, smtpUser: r.smtp_user, smtpPass: r.smtp_pass,
|
|
475
|
+
dkimPrivateKey: r.dkim_private_key, logoUrl: r.logo_url, primaryColor: r.primary_color,
|
|
476
|
+
plan: r.plan, createdAt: new Date(r.created_at), updatedAt: new Date(r.updated_at),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared SQL schema for Postgres, MySQL, SQLite, CockroachDB, etc.
|
|
3
|
+
* Each adapter translates these to dialect-specific DDL.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const TABLES = {
|
|
7
|
+
company: `
|
|
8
|
+
CREATE TABLE IF NOT EXISTS company_settings (
|
|
9
|
+
id TEXT PRIMARY KEY DEFAULT 'default',
|
|
10
|
+
name TEXT NOT NULL,
|
|
11
|
+
domain TEXT,
|
|
12
|
+
subdomain TEXT NOT NULL UNIQUE,
|
|
13
|
+
smtp_host TEXT,
|
|
14
|
+
smtp_port INTEGER,
|
|
15
|
+
smtp_user TEXT,
|
|
16
|
+
smtp_pass TEXT,
|
|
17
|
+
dkim_private_key TEXT,
|
|
18
|
+
logo_url TEXT,
|
|
19
|
+
primary_color TEXT DEFAULT '#6366f1',
|
|
20
|
+
plan TEXT NOT NULL DEFAULT 'free',
|
|
21
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
22
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
23
|
+
)`,
|
|
24
|
+
|
|
25
|
+
agents: `
|
|
26
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
27
|
+
id TEXT PRIMARY KEY,
|
|
28
|
+
name TEXT NOT NULL UNIQUE,
|
|
29
|
+
email TEXT NOT NULL UNIQUE,
|
|
30
|
+
role TEXT NOT NULL DEFAULT 'assistant',
|
|
31
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
32
|
+
metadata TEXT DEFAULT '{}',
|
|
33
|
+
created_by TEXT NOT NULL,
|
|
34
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
35
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
36
|
+
)`,
|
|
37
|
+
|
|
38
|
+
users: `
|
|
39
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
email TEXT NOT NULL UNIQUE,
|
|
42
|
+
name TEXT NOT NULL,
|
|
43
|
+
role TEXT NOT NULL DEFAULT 'member',
|
|
44
|
+
password_hash TEXT,
|
|
45
|
+
sso_provider TEXT,
|
|
46
|
+
sso_subject TEXT,
|
|
47
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
48
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
49
|
+
last_login_at TIMESTAMP
|
|
50
|
+
)`,
|
|
51
|
+
|
|
52
|
+
audit_log: `
|
|
53
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
54
|
+
id TEXT PRIMARY KEY,
|
|
55
|
+
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
56
|
+
actor TEXT NOT NULL,
|
|
57
|
+
actor_type TEXT NOT NULL DEFAULT 'user',
|
|
58
|
+
action TEXT NOT NULL,
|
|
59
|
+
resource TEXT NOT NULL,
|
|
60
|
+
details TEXT DEFAULT '{}',
|
|
61
|
+
ip TEXT
|
|
62
|
+
)`,
|
|
63
|
+
|
|
64
|
+
api_keys: `
|
|
65
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
66
|
+
id TEXT PRIMARY KEY,
|
|
67
|
+
name TEXT NOT NULL,
|
|
68
|
+
key_hash TEXT NOT NULL,
|
|
69
|
+
key_prefix TEXT NOT NULL,
|
|
70
|
+
scopes TEXT NOT NULL DEFAULT '[]',
|
|
71
|
+
created_by TEXT NOT NULL,
|
|
72
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
73
|
+
last_used_at TIMESTAMP,
|
|
74
|
+
expires_at TIMESTAMP,
|
|
75
|
+
revoked INTEGER NOT NULL DEFAULT 0
|
|
76
|
+
)`,
|
|
77
|
+
|
|
78
|
+
email_rules: `
|
|
79
|
+
CREATE TABLE IF NOT EXISTS email_rules (
|
|
80
|
+
id TEXT PRIMARY KEY,
|
|
81
|
+
name TEXT NOT NULL,
|
|
82
|
+
agent_id TEXT,
|
|
83
|
+
conditions TEXT NOT NULL DEFAULT '{}',
|
|
84
|
+
actions TEXT NOT NULL DEFAULT '{}',
|
|
85
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
86
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
87
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
88
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
89
|
+
)`,
|
|
90
|
+
|
|
91
|
+
retention_policy: `
|
|
92
|
+
CREATE TABLE IF NOT EXISTS retention_policy (
|
|
93
|
+
id TEXT PRIMARY KEY DEFAULT 'default',
|
|
94
|
+
enabled INTEGER NOT NULL DEFAULT 0,
|
|
95
|
+
retain_days INTEGER NOT NULL DEFAULT 365,
|
|
96
|
+
exclude_tags TEXT DEFAULT '[]',
|
|
97
|
+
archive_first INTEGER NOT NULL DEFAULT 1
|
|
98
|
+
)`,
|
|
99
|
+
|
|
100
|
+
// Indexes
|
|
101
|
+
indexes: [
|
|
102
|
+
'CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status)',
|
|
103
|
+
'CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name)',
|
|
104
|
+
'CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)',
|
|
105
|
+
'CREATE INDEX IF NOT EXISTS idx_users_sso ON users(sso_provider, sso_subject)',
|
|
106
|
+
'CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp)',
|
|
107
|
+
'CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit_log(actor)',
|
|
108
|
+
'CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log(action)',
|
|
109
|
+
'CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash)',
|
|
110
|
+
'CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(key_prefix)',
|
|
111
|
+
'CREATE INDEX IF NOT EXISTS idx_email_rules_agent ON email_rules(agent_id)',
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export function getAllCreateStatements(): string[] {
|
|
116
|
+
const stmts: string[] = [];
|
|
117
|
+
for (const [key, value] of Object.entries(TABLES)) {
|
|
118
|
+
if (key === 'indexes') continue;
|
|
119
|
+
stmts.push(value as string);
|
|
120
|
+
}
|
|
121
|
+
stmts.push(...TABLES.indexes);
|
|
122
|
+
return stmts;
|
|
123
|
+
}
|