@bernierllc/email-mitm-masking 1.0.0
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/.eslintrc.js +21 -0
- package/README.md +405 -0
- package/TESTING.md +237 -0
- package/__tests__/EmailMaskingService.test.ts +470 -0
- package/__tests__/__fixtures__/test-config.ts +24 -0
- package/__tests__/__mocks__/database.ts +252 -0
- package/__tests__/setup.ts +16 -0
- package/dist/EmailMaskingService.d.ts +90 -0
- package/dist/EmailMaskingService.js +316 -0
- package/dist/database.d.ts +5 -0
- package/dist/database.js +69 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -0
- package/dist/types.d.ts +159 -0
- package/dist/types.js +9 -0
- package/jest.config.cjs +28 -0
- package/package.json +56 -0
- package/src/EmailMaskingService.ts +425 -0
- package/src/database.ts +69 -0
- package/src/index.ts +20 -0
- package/src/types.ts +200 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Pool } from 'pg';
|
|
10
|
+
import type {
|
|
11
|
+
EmailMaskingConfig,
|
|
12
|
+
ProxyAddress,
|
|
13
|
+
RoutingResult,
|
|
14
|
+
RoutingAudit,
|
|
15
|
+
CreateProxyOptions,
|
|
16
|
+
ProxyAddressRow
|
|
17
|
+
} from './types.js';
|
|
18
|
+
import { initializeDatabase } from './database.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Email masking and man-in-the-middle routing service
|
|
22
|
+
*/
|
|
23
|
+
export class EmailMaskingService {
|
|
24
|
+
private config: EmailMaskingConfig;
|
|
25
|
+
private db: Pool;
|
|
26
|
+
private cleanupInterval?: NodeJS.Timeout;
|
|
27
|
+
|
|
28
|
+
constructor(config: EmailMaskingConfig) {
|
|
29
|
+
this.config = config;
|
|
30
|
+
this.db = new Pool({
|
|
31
|
+
host: config.database.host,
|
|
32
|
+
port: config.database.port,
|
|
33
|
+
database: config.database.database,
|
|
34
|
+
user: config.database.user,
|
|
35
|
+
password: config.database.password,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Initialize the masking service
|
|
41
|
+
*/
|
|
42
|
+
async initialize(): Promise<void> {
|
|
43
|
+
// Initialize database schema
|
|
44
|
+
await initializeDatabase(this.db);
|
|
45
|
+
|
|
46
|
+
// Start cleanup job for expired proxies
|
|
47
|
+
this.startCleanupJob();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Cleanup resources
|
|
52
|
+
*/
|
|
53
|
+
async shutdown(): Promise<void> {
|
|
54
|
+
if (this.cleanupInterval) {
|
|
55
|
+
clearInterval(this.cleanupInterval);
|
|
56
|
+
}
|
|
57
|
+
await this.db.end();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a new proxy email address
|
|
62
|
+
*/
|
|
63
|
+
async createProxy(
|
|
64
|
+
userId: string,
|
|
65
|
+
realEmail: string,
|
|
66
|
+
options?: CreateProxyOptions
|
|
67
|
+
): Promise<ProxyAddress> {
|
|
68
|
+
// Validate user's proxy quota
|
|
69
|
+
const userProxies = await this.getUserProxyCount(userId);
|
|
70
|
+
const maxProxies = this.config.maxProxiesPerUser || 0;
|
|
71
|
+
|
|
72
|
+
if (maxProxies > 0 && userProxies >= maxProxies) {
|
|
73
|
+
throw new Error(`User ${userId} has reached maximum proxy limit (${maxProxies})`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Generate secure proxy token
|
|
77
|
+
const proxyToken = this.generateProxyToken();
|
|
78
|
+
const proxyEmail = `${userId}-${proxyToken}@${this.config.proxyDomain}`;
|
|
79
|
+
|
|
80
|
+
// Calculate expiration
|
|
81
|
+
const ttl = options?.ttlDays !== undefined ? options.ttlDays : (this.config.defaultTTL || 0);
|
|
82
|
+
const expiresAt = ttl !== 0
|
|
83
|
+
? new Date(Date.now() + ttl * 24 * 60 * 60 * 1000)
|
|
84
|
+
: null;
|
|
85
|
+
|
|
86
|
+
// Insert proxy record
|
|
87
|
+
const result = await this.db.query<ProxyAddressRow>(
|
|
88
|
+
`INSERT INTO proxy_addresses
|
|
89
|
+
(proxy_email, real_email, user_id, external_email, conversation_id, status, expires_at, metadata)
|
|
90
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
91
|
+
RETURNING *`,
|
|
92
|
+
[
|
|
93
|
+
proxyEmail,
|
|
94
|
+
realEmail,
|
|
95
|
+
userId,
|
|
96
|
+
options?.externalEmail || null,
|
|
97
|
+
options?.conversationId || null,
|
|
98
|
+
'active',
|
|
99
|
+
expiresAt,
|
|
100
|
+
JSON.stringify(options?.metadata || {})
|
|
101
|
+
]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return this.mapProxyRow(result.rows[0]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Route inbound email (external → real address)
|
|
109
|
+
*/
|
|
110
|
+
async routeInbound(
|
|
111
|
+
from: string,
|
|
112
|
+
to: string,
|
|
113
|
+
_subject: string,
|
|
114
|
+
_text?: string,
|
|
115
|
+
_html?: string
|
|
116
|
+
): Promise<RoutingResult> {
|
|
117
|
+
const toAddress = this.extractProxyAddress(to);
|
|
118
|
+
|
|
119
|
+
if (!toAddress) {
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
direction: 'inbound',
|
|
123
|
+
error: 'No proxy address found in To field'
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Look up proxy
|
|
128
|
+
const proxy = await this.getProxyByEmail(toAddress);
|
|
129
|
+
|
|
130
|
+
if (!proxy) {
|
|
131
|
+
return {
|
|
132
|
+
success: false,
|
|
133
|
+
direction: 'inbound',
|
|
134
|
+
error: `Proxy address not found: ${toAddress}`
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Validate proxy status
|
|
139
|
+
if (proxy.status !== 'active') {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
direction: 'inbound',
|
|
143
|
+
error: `Proxy is ${proxy.status}: ${toAddress}`
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check expiration
|
|
148
|
+
if (proxy.expiresAt && proxy.expiresAt < new Date()) {
|
|
149
|
+
await this.expireProxy(proxy.id);
|
|
150
|
+
return {
|
|
151
|
+
success: false,
|
|
152
|
+
direction: 'inbound',
|
|
153
|
+
error: `Proxy expired: ${toAddress}`
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Update proxy usage
|
|
158
|
+
await this.updateProxyUsage(proxy.id);
|
|
159
|
+
|
|
160
|
+
// Audit the routing
|
|
161
|
+
const auditId = await this.auditRouting({
|
|
162
|
+
proxyId: proxy.id,
|
|
163
|
+
direction: 'inbound',
|
|
164
|
+
fromAddress: from,
|
|
165
|
+
toAddress: proxy.realEmail,
|
|
166
|
+
success: true
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
success: true,
|
|
171
|
+
routedTo: proxy.realEmail,
|
|
172
|
+
proxyUsed: toAddress,
|
|
173
|
+
direction: 'inbound',
|
|
174
|
+
auditId
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Route outbound email (real address → external via proxy)
|
|
180
|
+
*/
|
|
181
|
+
async routeOutbound(
|
|
182
|
+
userId: string,
|
|
183
|
+
realEmail: string,
|
|
184
|
+
externalEmail: string,
|
|
185
|
+
_emailContent: { subject: string; text?: string; html?: string }
|
|
186
|
+
): Promise<RoutingResult> {
|
|
187
|
+
// Find or create proxy for this user-external pair
|
|
188
|
+
let proxy = await this.getProxyForContact(userId, realEmail, externalEmail);
|
|
189
|
+
|
|
190
|
+
if (!proxy) {
|
|
191
|
+
// Auto-create proxy for outbound routing
|
|
192
|
+
proxy = await this.createProxy(userId, realEmail, {
|
|
193
|
+
externalEmail,
|
|
194
|
+
metadata: { autoCreated: true, direction: 'outbound' }
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Validate proxy status
|
|
199
|
+
if (proxy.status !== 'active') {
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
direction: 'outbound',
|
|
203
|
+
error: `Proxy is ${proxy.status}`
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Update proxy usage
|
|
208
|
+
await this.updateProxyUsage(proxy.id);
|
|
209
|
+
|
|
210
|
+
// Audit the routing
|
|
211
|
+
const auditId = await this.auditRouting({
|
|
212
|
+
proxyId: proxy.id,
|
|
213
|
+
direction: 'outbound',
|
|
214
|
+
fromAddress: realEmail,
|
|
215
|
+
toAddress: externalEmail,
|
|
216
|
+
success: true
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
success: true,
|
|
221
|
+
routedTo: externalEmail,
|
|
222
|
+
proxyUsed: proxy.proxyEmail,
|
|
223
|
+
direction: 'outbound',
|
|
224
|
+
auditId
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Deactivate a proxy address
|
|
230
|
+
*/
|
|
231
|
+
async deactivateProxy(proxyId: string): Promise<void> {
|
|
232
|
+
await this.db.query(
|
|
233
|
+
'UPDATE proxy_addresses SET status = $1 WHERE id = $2',
|
|
234
|
+
['inactive', proxyId]
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Revoke a proxy address permanently
|
|
240
|
+
*/
|
|
241
|
+
async revokeProxy(proxyId: string): Promise<void> {
|
|
242
|
+
await this.db.query(
|
|
243
|
+
'UPDATE proxy_addresses SET status = $1 WHERE id = $2',
|
|
244
|
+
['revoked', proxyId]
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get proxy by ID
|
|
250
|
+
*/
|
|
251
|
+
async getProxyById(proxyId: string): Promise<ProxyAddress | null> {
|
|
252
|
+
const result = await this.db.query<ProxyAddressRow>(
|
|
253
|
+
'SELECT * FROM proxy_addresses WHERE id = $1',
|
|
254
|
+
[proxyId]
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
return result.rows.length > 0 ? this.mapProxyRow(result.rows[0]) : null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get proxy by email address
|
|
262
|
+
*/
|
|
263
|
+
private async getProxyByEmail(proxyEmail: string): Promise<ProxyAddress | null> {
|
|
264
|
+
const result = await this.db.query<ProxyAddressRow>(
|
|
265
|
+
'SELECT * FROM proxy_addresses WHERE proxy_email = $1',
|
|
266
|
+
[proxyEmail]
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
return result.rows.length > 0 ? this.mapProxyRow(result.rows[0]) : null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get proxy for specific user-contact pair
|
|
274
|
+
*/
|
|
275
|
+
private async getProxyForContact(
|
|
276
|
+
userId: string,
|
|
277
|
+
realEmail: string,
|
|
278
|
+
externalEmail: string
|
|
279
|
+
): Promise<ProxyAddress | null> {
|
|
280
|
+
const result = await this.db.query<ProxyAddressRow>(
|
|
281
|
+
`SELECT * FROM proxy_addresses
|
|
282
|
+
WHERE user_id = $1
|
|
283
|
+
AND real_email = $2
|
|
284
|
+
AND external_email = $3
|
|
285
|
+
ORDER BY created_at DESC
|
|
286
|
+
LIMIT 1`,
|
|
287
|
+
[userId, realEmail, externalEmail]
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return result.rows.length > 0 ? this.mapProxyRow(result.rows[0]) : null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Update proxy usage statistics
|
|
295
|
+
*/
|
|
296
|
+
private async updateProxyUsage(proxyId: string): Promise<void> {
|
|
297
|
+
await this.db.query(
|
|
298
|
+
`UPDATE proxy_addresses
|
|
299
|
+
SET routing_count = routing_count + 1, last_used_at = NOW()
|
|
300
|
+
WHERE id = $1`,
|
|
301
|
+
[proxyId]
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Audit a routing decision
|
|
307
|
+
*/
|
|
308
|
+
private async auditRouting(
|
|
309
|
+
audit: Omit<RoutingAudit, 'id' | 'routedAt'>
|
|
310
|
+
): Promise<string> {
|
|
311
|
+
if (!this.config.auditEnabled) {
|
|
312
|
+
return '';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const result = await this.db.query<{ id: string }>(
|
|
316
|
+
`INSERT INTO routing_audit
|
|
317
|
+
(proxy_id, direction, from_address, to_address, success, error, metadata)
|
|
318
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
319
|
+
RETURNING id`,
|
|
320
|
+
[
|
|
321
|
+
audit.proxyId,
|
|
322
|
+
audit.direction,
|
|
323
|
+
audit.fromAddress,
|
|
324
|
+
audit.toAddress,
|
|
325
|
+
audit.success,
|
|
326
|
+
audit.error || null,
|
|
327
|
+
JSON.stringify(audit.metadata || {})
|
|
328
|
+
]
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
return result.rows[0].id;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Map database row to ProxyAddress
|
|
336
|
+
*/
|
|
337
|
+
private mapProxyRow(row: ProxyAddressRow): ProxyAddress {
|
|
338
|
+
return {
|
|
339
|
+
id: row.id,
|
|
340
|
+
proxyEmail: row.proxy_email,
|
|
341
|
+
realEmail: row.real_email,
|
|
342
|
+
userId: row.user_id,
|
|
343
|
+
externalEmail: row.external_email || undefined,
|
|
344
|
+
conversationId: row.conversation_id || undefined,
|
|
345
|
+
status: row.status as 'active' | 'inactive' | 'expired' | 'revoked',
|
|
346
|
+
createdAt: new Date(row.created_at),
|
|
347
|
+
activatedAt: row.activated_at ? new Date(row.activated_at) : undefined,
|
|
348
|
+
expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
|
|
349
|
+
lastUsedAt: row.last_used_at ? new Date(row.last_used_at) : undefined,
|
|
350
|
+
routingCount: parseInt(row.routing_count) || 0,
|
|
351
|
+
metadata: row.metadata ? JSON.parse(row.metadata) as Record<string, unknown> : {}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Get user's proxy count
|
|
357
|
+
*/
|
|
358
|
+
private async getUserProxyCount(userId: string): Promise<number> {
|
|
359
|
+
const result = await this.db.query<{ count: string }>(
|
|
360
|
+
`SELECT COUNT(*) as count FROM proxy_addresses
|
|
361
|
+
WHERE user_id = $1 AND status = 'active'`,
|
|
362
|
+
[userId]
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
return parseInt(result.rows[0].count) || 0;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Expire a proxy
|
|
370
|
+
*/
|
|
371
|
+
private async expireProxy(proxyId: string): Promise<void> {
|
|
372
|
+
await this.db.query(
|
|
373
|
+
'UPDATE proxy_addresses SET status = $1 WHERE id = $2',
|
|
374
|
+
['expired', proxyId]
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Start cleanup job for expired proxies
|
|
380
|
+
*/
|
|
381
|
+
private startCleanupJob(): void {
|
|
382
|
+
// Run every hour
|
|
383
|
+
this.cleanupInterval = setInterval(() => {
|
|
384
|
+
void this.cleanupExpiredProxies();
|
|
385
|
+
}, 60 * 60 * 1000); // 1 hour
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Cleanup expired proxies
|
|
390
|
+
*/
|
|
391
|
+
private async cleanupExpiredProxies(): Promise<void> {
|
|
392
|
+
await this.db.query(
|
|
393
|
+
`UPDATE proxy_addresses
|
|
394
|
+
SET status = 'expired'
|
|
395
|
+
WHERE expires_at < NOW() AND status = 'active'`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Extract proxy address from To field
|
|
401
|
+
*/
|
|
402
|
+
private extractProxyAddress(to: string): string | null {
|
|
403
|
+
const proxyDomain = this.config.proxyDomain;
|
|
404
|
+
|
|
405
|
+
if (to.includes(`@${proxyDomain}`)) {
|
|
406
|
+
// Extract email from "Name <email>" format
|
|
407
|
+
const match = to.match(/<(.+?)>/) || [null, to];
|
|
408
|
+
return match[1];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Generate secure proxy token
|
|
416
|
+
*/
|
|
417
|
+
private generateProxyToken(): string {
|
|
418
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
419
|
+
let token = '';
|
|
420
|
+
for (let i = 0; i < 16; i++) {
|
|
421
|
+
token += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
422
|
+
}
|
|
423
|
+
return token;
|
|
424
|
+
}
|
|
425
|
+
}
|
package/src/database.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Pool } from 'pg';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initialize database schema for email masking
|
|
13
|
+
*/
|
|
14
|
+
export async function initializeDatabase(pool: Pool): Promise<void> {
|
|
15
|
+
await pool.query(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS proxy_addresses (
|
|
17
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
18
|
+
proxy_email TEXT UNIQUE NOT NULL,
|
|
19
|
+
real_email TEXT NOT NULL,
|
|
20
|
+
user_id TEXT NOT NULL,
|
|
21
|
+
external_email TEXT,
|
|
22
|
+
conversation_id TEXT,
|
|
23
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
24
|
+
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
25
|
+
activated_at TIMESTAMP,
|
|
26
|
+
expires_at TIMESTAMP,
|
|
27
|
+
last_used_at TIMESTAMP,
|
|
28
|
+
routing_count INTEGER NOT NULL DEFAULT 0,
|
|
29
|
+
metadata JSONB
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_proxy_email ON proxy_addresses(proxy_email);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_user_id ON proxy_addresses(user_id);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_real_email ON proxy_addresses(real_email);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_status ON proxy_addresses(status);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_expires_at ON proxy_addresses(expires_at) WHERE status = 'active';
|
|
37
|
+
|
|
38
|
+
CREATE TABLE IF NOT EXISTS routing_audit (
|
|
39
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
40
|
+
proxy_id UUID NOT NULL,
|
|
41
|
+
direction TEXT NOT NULL,
|
|
42
|
+
from_address TEXT NOT NULL,
|
|
43
|
+
to_address TEXT NOT NULL,
|
|
44
|
+
routed_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
45
|
+
success BOOLEAN NOT NULL,
|
|
46
|
+
error TEXT,
|
|
47
|
+
metadata JSONB
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_audit_proxy_id ON routing_audit(proxy_id);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_routed_at ON routing_audit(routed_at);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_direction ON routing_audit(direction);
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS masking_rules (
|
|
55
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
56
|
+
user_id TEXT NOT NULL,
|
|
57
|
+
type TEXT NOT NULL,
|
|
58
|
+
criteria TEXT NOT NULL,
|
|
59
|
+
action TEXT NOT NULL,
|
|
60
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
61
|
+
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
62
|
+
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_rules_user_id ON masking_rules(user_id);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_priority ON masking_rules(priority);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_enabled ON masking_rules(enabled);
|
|
68
|
+
`);
|
|
69
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { EmailMaskingService } from './EmailMaskingService.js';
|
|
10
|
+
export type {
|
|
11
|
+
EmailMaskingConfig,
|
|
12
|
+
DatabaseConfig,
|
|
13
|
+
ProxyAddress,
|
|
14
|
+
MaskingRule,
|
|
15
|
+
RoutingResult,
|
|
16
|
+
RoutingAudit,
|
|
17
|
+
MaskingResult,
|
|
18
|
+
CreateProxyOptions,
|
|
19
|
+
EmailContent
|
|
20
|
+
} from './types.js';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Configuration for email masking service
|
|
11
|
+
*/
|
|
12
|
+
export interface EmailMaskingConfig {
|
|
13
|
+
/** Proxy domain for masked addresses (e.g., proxy.example.com) */
|
|
14
|
+
proxyDomain: string;
|
|
15
|
+
|
|
16
|
+
/** Database connection for proxy storage */
|
|
17
|
+
database: DatabaseConfig;
|
|
18
|
+
|
|
19
|
+
/** Default TTL for proxy addresses (in days, 0 = unlimited) */
|
|
20
|
+
defaultTTL?: number;
|
|
21
|
+
|
|
22
|
+
/** Maximum proxies per user (0 = unlimited) */
|
|
23
|
+
maxProxiesPerUser?: number;
|
|
24
|
+
|
|
25
|
+
/** Enable audit logging */
|
|
26
|
+
auditEnabled?: boolean;
|
|
27
|
+
|
|
28
|
+
/** Masking strategy */
|
|
29
|
+
strategy?: 'per-user' | 'per-contact' | 'per-conversation';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Database configuration
|
|
34
|
+
*/
|
|
35
|
+
export interface DatabaseConfig {
|
|
36
|
+
host: string;
|
|
37
|
+
port: number;
|
|
38
|
+
database: string;
|
|
39
|
+
user: string;
|
|
40
|
+
password: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Proxy email address record
|
|
45
|
+
*/
|
|
46
|
+
export interface ProxyAddress {
|
|
47
|
+
/** Unique proxy identifier */
|
|
48
|
+
id: string;
|
|
49
|
+
|
|
50
|
+
/** Proxy email address (e.g., user-abc123@proxy.domain.com) */
|
|
51
|
+
proxyEmail: string;
|
|
52
|
+
|
|
53
|
+
/** Real user email address */
|
|
54
|
+
realEmail: string;
|
|
55
|
+
|
|
56
|
+
/** User identifier */
|
|
57
|
+
userId: string;
|
|
58
|
+
|
|
59
|
+
/** Optional external contact email (for per-contact masking) */
|
|
60
|
+
externalEmail?: string;
|
|
61
|
+
|
|
62
|
+
/** Optional conversation identifier (for per-conversation masking) */
|
|
63
|
+
conversationId?: string;
|
|
64
|
+
|
|
65
|
+
/** Proxy status */
|
|
66
|
+
status: 'active' | 'inactive' | 'expired' | 'revoked';
|
|
67
|
+
|
|
68
|
+
/** Creation timestamp */
|
|
69
|
+
createdAt: Date;
|
|
70
|
+
|
|
71
|
+
/** Activation timestamp */
|
|
72
|
+
activatedAt?: Date;
|
|
73
|
+
|
|
74
|
+
/** Expiration timestamp (null = no expiration) */
|
|
75
|
+
expiresAt?: Date;
|
|
76
|
+
|
|
77
|
+
/** Last used timestamp */
|
|
78
|
+
lastUsedAt?: Date;
|
|
79
|
+
|
|
80
|
+
/** Routing count */
|
|
81
|
+
routingCount: number;
|
|
82
|
+
|
|
83
|
+
/** Metadata */
|
|
84
|
+
metadata?: Record<string, unknown>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Masking rule configuration
|
|
89
|
+
*/
|
|
90
|
+
export interface MaskingRule {
|
|
91
|
+
/** Rule identifier */
|
|
92
|
+
id: string;
|
|
93
|
+
|
|
94
|
+
/** User identifier */
|
|
95
|
+
userId: string;
|
|
96
|
+
|
|
97
|
+
/** Rule type */
|
|
98
|
+
type: 'domain' | 'address' | 'pattern';
|
|
99
|
+
|
|
100
|
+
/** Match criteria */
|
|
101
|
+
criteria: string;
|
|
102
|
+
|
|
103
|
+
/** Action (mask or allow) */
|
|
104
|
+
action: 'mask' | 'allow';
|
|
105
|
+
|
|
106
|
+
/** Priority (higher = evaluated first) */
|
|
107
|
+
priority: number;
|
|
108
|
+
|
|
109
|
+
/** Enabled flag */
|
|
110
|
+
enabled: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Routing result
|
|
115
|
+
*/
|
|
116
|
+
export interface RoutingResult {
|
|
117
|
+
success: boolean;
|
|
118
|
+
routedTo?: string;
|
|
119
|
+
proxyUsed?: string;
|
|
120
|
+
direction: 'inbound' | 'outbound';
|
|
121
|
+
error?: string;
|
|
122
|
+
auditId?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Audit entry
|
|
127
|
+
*/
|
|
128
|
+
export interface RoutingAudit {
|
|
129
|
+
id: string;
|
|
130
|
+
proxyId: string;
|
|
131
|
+
direction: 'inbound' | 'outbound';
|
|
132
|
+
fromAddress: string;
|
|
133
|
+
toAddress: string;
|
|
134
|
+
routedAt: Date;
|
|
135
|
+
success: boolean;
|
|
136
|
+
error?: string;
|
|
137
|
+
metadata?: Record<string, unknown>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Result wrapper for masking operations
|
|
142
|
+
*/
|
|
143
|
+
export interface MaskingResult<T = unknown> {
|
|
144
|
+
success: boolean;
|
|
145
|
+
data?: T;
|
|
146
|
+
error?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Options for creating a proxy
|
|
151
|
+
*/
|
|
152
|
+
export interface CreateProxyOptions {
|
|
153
|
+
externalEmail?: string;
|
|
154
|
+
conversationId?: string;
|
|
155
|
+
ttlDays?: number;
|
|
156
|
+
metadata?: Record<string, unknown>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Email content for outbound routing
|
|
161
|
+
*/
|
|
162
|
+
export interface EmailContent {
|
|
163
|
+
subject: string;
|
|
164
|
+
text?: string;
|
|
165
|
+
html?: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Database row type for proxy addresses
|
|
170
|
+
*/
|
|
171
|
+
export interface ProxyAddressRow {
|
|
172
|
+
id: string;
|
|
173
|
+
proxy_email: string;
|
|
174
|
+
real_email: string;
|
|
175
|
+
user_id: string;
|
|
176
|
+
external_email: string | null;
|
|
177
|
+
conversation_id: string | null;
|
|
178
|
+
status: string;
|
|
179
|
+
created_at: string;
|
|
180
|
+
activated_at: string | null;
|
|
181
|
+
expires_at: string | null;
|
|
182
|
+
last_used_at: string | null;
|
|
183
|
+
routing_count: string;
|
|
184
|
+
metadata: string | null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Database row type for routing audit
|
|
189
|
+
*/
|
|
190
|
+
export interface RoutingAuditRow {
|
|
191
|
+
id: string;
|
|
192
|
+
proxy_id: string;
|
|
193
|
+
direction: string;
|
|
194
|
+
from_address: string;
|
|
195
|
+
to_address: string;
|
|
196
|
+
routed_at: string;
|
|
197
|
+
success: boolean;
|
|
198
|
+
error: string | null;
|
|
199
|
+
metadata: string | null;
|
|
200
|
+
}
|