expxagents 0.11.2 → 0.12.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.
Files changed (94) hide show
  1. package/dist/dashboard/assets/{BufferResource-CR4DczHL.js → BufferResource-BcVsF5HP.js} +1 -1
  2. package/dist/dashboard/assets/{CanvasRenderer-DdWLm2t4.js → CanvasRenderer-kA1Maw0x.js} +1 -1
  3. package/dist/dashboard/assets/{JarvisView-yQv964kf.js → JarvisView-DBrCWArD.js} +1 -1
  4. package/dist/dashboard/assets/{RenderTargetSystem-DdTH8Un8.js → RenderTargetSystem-Bp9B4iP8.js} +1 -1
  5. package/dist/dashboard/assets/{WebGLRenderer-C6BYo_WV.js → WebGLRenderer-CNJyeb_W.js} +1 -1
  6. package/dist/dashboard/assets/{WebGPURenderer-C1UxCrJq.js → WebGPURenderer-DCbozzdc.js} +1 -1
  7. package/dist/dashboard/assets/{browserAll-BC0ycs7y.js → browserAll-Bu4cXbCn.js} +1 -1
  8. package/dist/dashboard/assets/index-zfHiMrG2.js +344 -0
  9. package/dist/dashboard/assets/{webworkerAll-D7SccyuO.js → webworkerAll-BFb9bpT-.js} +1 -1
  10. package/dist/dashboard/index.html +1 -1
  11. package/dist/data/opensquad.db +0 -0
  12. package/dist/server/app.d.ts.map +1 -1
  13. package/dist/server/app.js +30 -0
  14. package/dist/server/app.js.map +1 -1
  15. package/dist/server/config.d.ts +6 -0
  16. package/dist/server/config.d.ts.map +1 -1
  17. package/dist/server/config.js +14 -1
  18. package/dist/server/config.js.map +1 -1
  19. package/dist/server/db/__tests__/email-schema.test.d.ts +2 -0
  20. package/dist/server/db/__tests__/email-schema.test.d.ts.map +1 -0
  21. package/dist/server/db/__tests__/email-schema.test.js +53 -0
  22. package/dist/server/db/__tests__/email-schema.test.js.map +1 -0
  23. package/dist/server/db/schema.d.ts +1 -1
  24. package/dist/server/db/schema.d.ts.map +1 -1
  25. package/dist/server/db/schema.js +70 -0
  26. package/dist/server/db/schema.js.map +1 -1
  27. package/dist/server/email/__tests__/campaign-routes.test.d.ts +2 -0
  28. package/dist/server/email/__tests__/campaign-routes.test.d.ts.map +1 -0
  29. package/dist/server/email/__tests__/campaign-routes.test.js +216 -0
  30. package/dist/server/email/__tests__/campaign-routes.test.js.map +1 -0
  31. package/dist/server/email/__tests__/campaign-service.test.d.ts +2 -0
  32. package/dist/server/email/__tests__/campaign-service.test.d.ts.map +1 -0
  33. package/dist/server/email/__tests__/campaign-service.test.js +79 -0
  34. package/dist/server/email/__tests__/campaign-service.test.js.map +1 -0
  35. package/dist/server/email/__tests__/email-queue-worker.test.d.ts +2 -0
  36. package/dist/server/email/__tests__/email-queue-worker.test.d.ts.map +1 -0
  37. package/dist/server/email/__tests__/email-queue-worker.test.js +93 -0
  38. package/dist/server/email/__tests__/email-queue-worker.test.js.map +1 -0
  39. package/dist/server/email/__tests__/email-utils.test.d.ts +2 -0
  40. package/dist/server/email/__tests__/email-utils.test.d.ts.map +1 -0
  41. package/dist/server/email/__tests__/email-utils.test.js +36 -0
  42. package/dist/server/email/__tests__/email-utils.test.js.map +1 -0
  43. package/dist/server/email/__tests__/lead-routes.test.d.ts +2 -0
  44. package/dist/server/email/__tests__/lead-routes.test.d.ts.map +1 -0
  45. package/dist/server/email/__tests__/lead-routes.test.js +180 -0
  46. package/dist/server/email/__tests__/lead-routes.test.js.map +1 -0
  47. package/dist/server/email/__tests__/lead-service.test.d.ts +2 -0
  48. package/dist/server/email/__tests__/lead-service.test.d.ts.map +1 -0
  49. package/dist/server/email/__tests__/lead-service.test.js +113 -0
  50. package/dist/server/email/__tests__/lead-service.test.js.map +1 -0
  51. package/dist/server/email/__tests__/ses-client.test.d.ts +2 -0
  52. package/dist/server/email/__tests__/ses-client.test.d.ts.map +1 -0
  53. package/dist/server/email/__tests__/ses-client.test.js +48 -0
  54. package/dist/server/email/__tests__/ses-client.test.js.map +1 -0
  55. package/dist/server/email/__tests__/sns-webhook.test.d.ts +2 -0
  56. package/dist/server/email/__tests__/sns-webhook.test.d.ts.map +1 -0
  57. package/dist/server/email/__tests__/sns-webhook.test.js +40 -0
  58. package/dist/server/email/__tests__/sns-webhook.test.js.map +1 -0
  59. package/dist/server/email/campaign-routes.d.ts +8 -0
  60. package/dist/server/email/campaign-routes.d.ts.map +1 -0
  61. package/dist/server/email/campaign-routes.js +65 -0
  62. package/dist/server/email/campaign-routes.js.map +1 -0
  63. package/dist/server/email/campaign-service.d.ts +55 -0
  64. package/dist/server/email/campaign-service.d.ts.map +1 -0
  65. package/dist/server/email/campaign-service.js +89 -0
  66. package/dist/server/email/campaign-service.js.map +1 -0
  67. package/dist/server/email/email-queue-worker.d.ts +27 -0
  68. package/dist/server/email/email-queue-worker.d.ts.map +1 -0
  69. package/dist/server/email/email-queue-worker.js +119 -0
  70. package/dist/server/email/email-queue-worker.js.map +1 -0
  71. package/dist/server/email/email-utils.d.ts +5 -0
  72. package/dist/server/email/email-utils.d.ts.map +1 -0
  73. package/dist/server/email/email-utils.js +24 -0
  74. package/dist/server/email/email-utils.js.map +1 -0
  75. package/dist/server/email/lead-routes.d.ts +8 -0
  76. package/dist/server/email/lead-routes.d.ts.map +1 -0
  77. package/dist/server/email/lead-routes.js +56 -0
  78. package/dist/server/email/lead-routes.js.map +1 -0
  79. package/dist/server/email/lead-service.d.ts +66 -0
  80. package/dist/server/email/lead-service.d.ts.map +1 -0
  81. package/dist/server/email/lead-service.js +138 -0
  82. package/dist/server/email/lead-service.js.map +1 -0
  83. package/dist/server/email/ses-client.d.ts +27 -0
  84. package/dist/server/email/ses-client.d.ts.map +1 -0
  85. package/dist/server/email/ses-client.js +44 -0
  86. package/dist/server/email/ses-client.js.map +1 -0
  87. package/dist/server/email/sns-webhook.d.ts +10 -0
  88. package/dist/server/email/sns-webhook.d.ts.map +1 -0
  89. package/dist/server/email/sns-webhook.js +73 -0
  90. package/dist/server/email/sns-webhook.js.map +1 -0
  91. package/package.json +6 -2
  92. package/dist/dashboard/assets/index-3Noclaww.js +0 -344
  93. package/dist/data/opensquad.db-shm +0 -0
  94. package/dist/data/opensquad.db-wal +0 -0
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ vi.mock('@aws-sdk/client-sesv2', () => {
3
+ const sendMock = vi.fn().mockImplementation((cmd) => {
4
+ if (cmd && cmd.__type === 'GetAccount') {
5
+ return Promise.resolve({
6
+ SendQuota: { MaxSendRate: 14, Max24HourSend: 50000, SentLast24Hours: 100 },
7
+ });
8
+ }
9
+ return Promise.resolve({ MessageId: 'mock-msg-id-123' });
10
+ });
11
+ const quotaMock = vi.fn().mockResolvedValue({
12
+ SendQuota: { MaxSendRate: 14, Max24HourSend: 50000, SentLast24Hours: 100 },
13
+ });
14
+ return {
15
+ SESv2Client: vi.fn().mockImplementation(() => ({ send: sendMock })),
16
+ SendEmailCommand: vi.fn().mockImplementation((params) => params),
17
+ GetAccountCommand: vi.fn().mockImplementation(() => ({ __type: 'GetAccount' })),
18
+ __sendMock: sendMock,
19
+ __quotaMock: quotaMock,
20
+ };
21
+ });
22
+ import { createSesClient } from '../ses-client.js';
23
+ describe('ses-client', () => {
24
+ beforeEach(() => { vi.clearAllMocks(); });
25
+ it('should send an email and return messageId', async () => {
26
+ const client = createSesClient({ region: 'us-east-1', accessKeyId: 'test', secretAccessKey: 'test' });
27
+ const result = await client.sendEmail({
28
+ to: 'user@example.com',
29
+ from: 'noreply@test.com',
30
+ fromName: 'Test',
31
+ subject: 'Hello',
32
+ htmlBody: '<p>Hi</p>',
33
+ textBody: 'Hi',
34
+ });
35
+ expect(result.messageId).toBe('mock-msg-id-123');
36
+ });
37
+ it('should return quota info', async () => {
38
+ const client = createSesClient({ region: 'us-east-1', accessKeyId: 'test', secretAccessKey: 'test' });
39
+ const quota = await client.getSendQuota();
40
+ expect(quota.maxSendRate).toBe(14);
41
+ expect(quota.max24HourSend).toBe(50000);
42
+ });
43
+ it('should return null client when no credentials', () => {
44
+ const client = createSesClient({ region: 'us-east-1' });
45
+ expect(client.isConfigured()).toBe(false);
46
+ });
47
+ });
48
+ //# sourceMappingURL=ses-client.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ses-client.test.js","sourceRoot":"","sources":["../../../src/email/__tests__/ses-client.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sns-webhook.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sns-webhook.test.d.ts","sourceRoot":"","sources":["../../../src/email/__tests__/sns-webhook.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import Database from 'better-sqlite3';
3
+ import { runMigrations } from '../../db/migrations.js';
4
+ import { processSnsNotification } from '../sns-webhook.js';
5
+ describe('SNS webhook processing', () => {
6
+ let db;
7
+ beforeEach(() => {
8
+ db = new Database(':memory:');
9
+ runMigrations(db);
10
+ db.prepare("INSERT INTO leads (id, email) VALUES ('l1', 'user@test.com')").run();
11
+ db.prepare("INSERT INTO email_campaigns (id, name, subject, body_html, from_email, status, total_recipients) VALUES ('c1', 'Test', 'Hi', '<p>Hi</p>', 'a@b.com', 'sending', 1)").run();
12
+ db.prepare("INSERT INTO email_queue (id, campaign_id, lead_id, to_email, status, ses_message_id) VALUES ('q1', 'c1', 'l1', 'user@test.com', 'sent', 'ses-123')").run();
13
+ });
14
+ afterEach(() => { db.close(); });
15
+ it('should process bounce notification', () => {
16
+ processSnsNotification(db, { notificationType: 'Bounce', mail: { messageId: 'ses-123' }, bounce: { bounceType: 'Permanent' } });
17
+ const row = db.prepare("SELECT status FROM email_queue WHERE id = 'q1'").get();
18
+ expect(row.status).toBe('bounced');
19
+ const lead = db.prepare("SELECT status FROM leads WHERE id = 'l1'").get();
20
+ expect(lead.status).toBe('bounced');
21
+ const campaign = db.prepare("SELECT bounce_count FROM email_campaigns WHERE id = 'c1'").get();
22
+ expect(campaign.bounce_count).toBe(1);
23
+ });
24
+ it('should process complaint notification', () => {
25
+ processSnsNotification(db, { notificationType: 'Complaint', mail: { messageId: 'ses-123' } });
26
+ const row = db.prepare("SELECT status FROM email_queue WHERE id = 'q1'").get();
27
+ expect(row.status).toBe('complained');
28
+ });
29
+ it('should be idempotent — skip if already bounced', () => {
30
+ db.prepare("UPDATE email_queue SET status = 'bounced' WHERE id = 'q1'").run();
31
+ db.prepare("UPDATE email_campaigns SET bounce_count = 1 WHERE id = 'c1'").run();
32
+ processSnsNotification(db, { notificationType: 'Bounce', mail: { messageId: 'ses-123' }, bounce: { bounceType: 'Permanent' } });
33
+ const campaign = db.prepare("SELECT bounce_count FROM email_campaigns WHERE id = 'c1'").get();
34
+ expect(campaign.bounce_count).toBe(1); // not incremented again
35
+ });
36
+ it('should ignore unknown message IDs', () => {
37
+ expect(() => processSnsNotification(db, { notificationType: 'Bounce', mail: { messageId: 'unknown-id' }, bounce: { bounceType: 'Permanent' } })).not.toThrow();
38
+ });
39
+ });
40
+ //# sourceMappingURL=sns-webhook.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sns-webhook.test.js","sourceRoot":"","sources":["../../../src/email/__tests__/sns-webhook.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
2
+ import type Database from 'better-sqlite3';
3
+ interface CampaignRoutesOptions extends FastifyPluginOptions {
4
+ db: Database.Database;
5
+ }
6
+ export declare function campaignRoutes(app: FastifyInstance, opts: CampaignRoutesOptions): Promise<void>;
7
+ export {};
8
+ //# sourceMappingURL=campaign-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"campaign-routes.d.ts","sourceRoot":"","sources":["../../src/email/campaign-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AACrE,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAG3C,UAAU,qBAAsB,SAAQ,oBAAoB;IAC1D,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAC;CACvB;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyFrG"}
@@ -0,0 +1,65 @@
1
+ import { CampaignService } from './campaign-service.js';
2
+ export async function campaignRoutes(app, opts) {
3
+ const service = new CampaignService(opts.db);
4
+ app.post('/api/email/campaigns', { preHandler: [app.requireAuth] }, async (request, reply) => {
5
+ const campaign = service.createCampaign(request.body);
6
+ return reply.send({ campaign });
7
+ });
8
+ app.get('/api/email/campaigns', { preHandler: [app.requireAuth] }, async (request, reply) => {
9
+ const { page, limit, status } = request.query;
10
+ const result = service.listCampaigns({ page: page ? parseInt(page) : undefined, limit: limit ? parseInt(limit) : undefined, status });
11
+ return reply.send(result);
12
+ });
13
+ app.get('/api/email/campaigns/:id', { preHandler: [app.requireAuth] }, async (request, reply) => {
14
+ const campaign = service.getCampaign(request.params.id);
15
+ if (!campaign)
16
+ return reply.status(404).send({ error: 'Campaign not found' });
17
+ return reply.send({ campaign });
18
+ });
19
+ app.post('/api/email/campaigns/:id/enqueue', { preHandler: [app.requireAuth] }, async (request, reply) => {
20
+ try {
21
+ service.enqueueCampaign(request.params.id, request.body.filters);
22
+ const campaign = service.getCampaign(request.params.id);
23
+ return reply.send({ campaign });
24
+ }
25
+ catch (err) {
26
+ return reply.status(400).send({ error: err.message });
27
+ }
28
+ });
29
+ app.post('/api/email/campaigns/:id/start', { preHandler: [app.requireAuth] }, async (request, reply) => {
30
+ try {
31
+ service.startCampaign(request.params.id);
32
+ return reply.send({ campaign: service.getCampaign(request.params.id) });
33
+ }
34
+ catch (err) {
35
+ return reply.status(400).send({ error: err.message });
36
+ }
37
+ });
38
+ app.post('/api/email/campaigns/:id/pause', { preHandler: [app.requireAuth] }, async (request, reply) => {
39
+ try {
40
+ service.pauseCampaign(request.params.id);
41
+ return reply.send({ campaign: service.getCampaign(request.params.id) });
42
+ }
43
+ catch (err) {
44
+ return reply.status(400).send({ error: err.message });
45
+ }
46
+ });
47
+ app.post('/api/email/campaigns/:id/cancel', { preHandler: [app.requireAuth] }, async (request, reply) => {
48
+ try {
49
+ service.cancelCampaign(request.params.id);
50
+ return reply.send({ campaign: service.getCampaign(request.params.id) });
51
+ }
52
+ catch (err) {
53
+ return reply.status(400).send({ error: err.message });
54
+ }
55
+ });
56
+ app.get('/api/email/campaigns/:id/queue', { preHandler: [app.requireAuth] }, async (request, reply) => {
57
+ const page = request.query.page ? parseInt(request.query.page) : 1;
58
+ const limit = request.query.limit ? parseInt(request.query.limit) : 50;
59
+ const offset = (page - 1) * limit;
60
+ const total = opts.db.prepare('SELECT COUNT(*) as count FROM email_queue WHERE campaign_id = ?').get(request.params.id).count;
61
+ const data = opts.db.prepare('SELECT * FROM email_queue WHERE campaign_id = ? ORDER BY created_at LIMIT ? OFFSET ?').all(request.params.id, limit, offset);
62
+ return reply.send({ data, total });
63
+ });
64
+ }
65
+ //# sourceMappingURL=campaign-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"campaign-routes.js","sourceRoot":"","sources":["../../src/email/campaign-routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import type Database from 'better-sqlite3';
2
+ export interface CreateCampaignData {
3
+ name: string;
4
+ squad_name?: string;
5
+ subject: string;
6
+ body_html: string;
7
+ body_text?: string;
8
+ from_email: string;
9
+ from_name?: string;
10
+ }
11
+ export interface EnqueueFilters {
12
+ tags?: string[];
13
+ lists?: string[];
14
+ status?: string;
15
+ }
16
+ export interface Campaign {
17
+ id: string;
18
+ name: string;
19
+ squad_name: string | null;
20
+ subject: string;
21
+ body_html: string;
22
+ body_text: string | null;
23
+ from_email: string;
24
+ from_name: string | null;
25
+ status: string;
26
+ total_recipients: number;
27
+ sent_count: number;
28
+ failed_count: number;
29
+ bounce_count: number;
30
+ complaint_count: number;
31
+ created_at: string;
32
+ started_at: string | null;
33
+ completed_at: string | null;
34
+ }
35
+ export declare class CampaignService {
36
+ private db;
37
+ constructor(db: Database.Database);
38
+ createCampaign(data: CreateCampaignData): Campaign;
39
+ getCampaign(id: string): Campaign | undefined;
40
+ listCampaigns(filters?: {
41
+ page?: number;
42
+ limit?: number;
43
+ status?: string;
44
+ }): {
45
+ data: Campaign[];
46
+ total: number;
47
+ };
48
+ enqueueCampaign(campaignId: string, filters: EnqueueFilters): void;
49
+ startCampaign(campaignId: string): void;
50
+ pauseCampaign(campaignId: string): void;
51
+ resumeCampaign(campaignId: string): void;
52
+ cancelCampaign(campaignId: string): void;
53
+ getCampaignStats(campaignId: string): Campaign | undefined;
54
+ }
55
+ //# sourceMappingURL=campaign-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"campaign-service.d.ts","sourceRoot":"","sources":["../../src/email/campaign-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,qBAAa,eAAe;IACd,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,QAAQ,CAAC,QAAQ;IAEzC,cAAc,CAAC,IAAI,EAAE,kBAAkB,GAAG,QAAQ;IASlD,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAI7C,aAAa,CAAC,OAAO,GAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG;QAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;IAapH,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI;IA2BlE,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAOvC,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAMvC,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAMxC,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAOxC,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;CAG3D"}
@@ -0,0 +1,89 @@
1
+ import crypto from 'crypto';
2
+ export class CampaignService {
3
+ db;
4
+ constructor(db) {
5
+ this.db = db;
6
+ }
7
+ createCampaign(data) {
8
+ const id = crypto.randomUUID();
9
+ this.db.prepare(`
10
+ INSERT INTO email_campaigns (id, name, squad_name, subject, body_html, body_text, from_email, from_name)
11
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
12
+ `).run(id, data.name, data.squad_name ?? null, data.subject, data.body_html, data.body_text ?? null, data.from_email, data.from_name ?? null);
13
+ return this.getCampaign(id);
14
+ }
15
+ getCampaign(id) {
16
+ return this.db.prepare('SELECT * FROM email_campaigns WHERE id = ?').get(id);
17
+ }
18
+ listCampaigns(filters = {}) {
19
+ const page = filters.page ?? 1;
20
+ const limit = filters.limit ?? 50;
21
+ const offset = (page - 1) * limit;
22
+ const conditions = [];
23
+ const params = [];
24
+ if (filters.status) {
25
+ conditions.push('status = ?');
26
+ params.push(filters.status);
27
+ }
28
+ const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
29
+ const total = this.db.prepare(`SELECT COUNT(*) as count FROM email_campaigns ${where}`).get(...params).count;
30
+ const data = this.db.prepare(`SELECT * FROM email_campaigns ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
31
+ return { data, total };
32
+ }
33
+ enqueueCampaign(campaignId, filters) {
34
+ const campaign = this.getCampaign(campaignId);
35
+ if (!campaign || campaign.status !== 'draft')
36
+ throw new Error('Campaign must be in draft status to enqueue');
37
+ const conditions = ["l.status = 'active'"];
38
+ const params = [];
39
+ if (filters.tags?.length) {
40
+ conditions.push(`EXISTS (SELECT 1 FROM lead_tags lt WHERE lt.lead_id = l.id AND lt.tag IN (${filters.tags.map(() => '?').join(',')}))`);
41
+ params.push(...filters.tags);
42
+ }
43
+ if (filters.lists?.length) {
44
+ conditions.push(`EXISTS (SELECT 1 FROM lead_lists ll WHERE ll.lead_id = l.id AND ll.list_name IN (${filters.lists.map(() => '?').join(',')}))`);
45
+ params.push(...filters.lists);
46
+ }
47
+ const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
48
+ const leads = this.db.prepare(`SELECT id, email FROM leads l ${where}`).all(...params);
49
+ const insertStmt = this.db.prepare('INSERT INTO email_queue (id, campaign_id, lead_id, to_email) VALUES (?, ?, ?, ?)');
50
+ const tx = this.db.transaction(() => {
51
+ for (const lead of leads) {
52
+ insertStmt.run(crypto.randomUUID(), campaignId, lead.id, lead.email);
53
+ }
54
+ this.db.prepare("UPDATE email_campaigns SET status = 'queued', total_recipients = ? WHERE id = ?").run(leads.length, campaignId);
55
+ });
56
+ tx();
57
+ }
58
+ startCampaign(campaignId) {
59
+ const campaign = this.getCampaign(campaignId);
60
+ if (!campaign || campaign.status !== 'queued')
61
+ throw new Error('Campaign must be in queued status to start');
62
+ if (campaign.total_recipients === 0)
63
+ throw new Error('Campaign has no recipients');
64
+ this.db.prepare("UPDATE email_campaigns SET status = 'sending', started_at = ? WHERE id = ?").run(new Date().toISOString(), campaignId);
65
+ }
66
+ pauseCampaign(campaignId) {
67
+ const campaign = this.getCampaign(campaignId);
68
+ if (!campaign || campaign.status !== 'sending')
69
+ throw new Error('Campaign must be sending to pause');
70
+ this.db.prepare("UPDATE email_campaigns SET status = 'paused' WHERE id = ?").run(campaignId);
71
+ }
72
+ resumeCampaign(campaignId) {
73
+ const campaign = this.getCampaign(campaignId);
74
+ if (!campaign || campaign.status !== 'paused')
75
+ throw new Error('Campaign must be paused to resume');
76
+ this.db.prepare("UPDATE email_campaigns SET status = 'sending' WHERE id = ?").run(campaignId);
77
+ }
78
+ cancelCampaign(campaignId) {
79
+ const campaign = this.getCampaign(campaignId);
80
+ if (!campaign || !['queued', 'sending', 'paused'].includes(campaign.status))
81
+ throw new Error('Cannot cancel campaign in current status');
82
+ this.db.prepare("UPDATE email_campaigns SET status = 'cancelled' WHERE id = ?").run(campaignId);
83
+ this.db.prepare("DELETE FROM email_queue WHERE campaign_id = ? AND status = 'pending'").run(campaignId);
84
+ }
85
+ getCampaignStats(campaignId) {
86
+ return this.getCampaign(campaignId);
87
+ }
88
+ }
89
+ //# sourceMappingURL=campaign-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"campaign-service.js","sourceRoot":"","sources":["../../src/email/campaign-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { SesClient } from './ses-client.js';
3
+ export interface EmailQueueWorkerConfig {
4
+ db: Database.Database;
5
+ ses: SesClient;
6
+ rateLimit: number;
7
+ jwtSecret: string;
8
+ serverUrl: string;
9
+ broadcast?: (message: Record<string, unknown>) => void;
10
+ }
11
+ export declare class EmailQueueWorker {
12
+ private db;
13
+ private ses;
14
+ private rateLimit;
15
+ private jwtSecret;
16
+ private serverUrl;
17
+ private broadcast?;
18
+ private interval;
19
+ private running;
20
+ private tickCount;
21
+ constructor(config: EmailQueueWorkerConfig);
22
+ start(): void;
23
+ stop(): void;
24
+ recover(): void;
25
+ tick(): Promise<void>;
26
+ }
27
+ //# sourceMappingURL=email-queue-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-queue-worker.d.ts","sourceRoot":"","sources":["../../src/email/email-queue-worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGjD,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAC;IACtB,GAAG,EAAE,SAAS,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACxD;AAqBD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,GAAG,CAAY;IACvB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAAC,CAA6C;IAC/D,OAAO,CAAC,QAAQ,CAA+C;IAC/D,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAK;gBAEV,MAAM,EAAE,sBAAsB;IAS1C,KAAK,IAAI,IAAI;IAMb,IAAI,IAAI,IAAI;IAIZ,OAAO,IAAI,IAAI;IAKT,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAmF5B"}
@@ -0,0 +1,119 @@
1
+ import { injectUnsubscribeFooter, generateUnsubscribeUrl, getUnsubscribeHeaders } from './email-utils.js';
2
+ export class EmailQueueWorker {
3
+ db;
4
+ ses;
5
+ rateLimit;
6
+ jwtSecret;
7
+ serverUrl;
8
+ broadcast;
9
+ interval = null;
10
+ running = false;
11
+ tickCount = 0;
12
+ constructor(config) {
13
+ this.db = config.db;
14
+ this.ses = config.ses;
15
+ this.rateLimit = config.rateLimit;
16
+ this.jwtSecret = config.jwtSecret;
17
+ this.serverUrl = config.serverUrl;
18
+ this.broadcast = config.broadcast;
19
+ }
20
+ start() {
21
+ if (this.interval)
22
+ return;
23
+ this.recover();
24
+ this.interval = setInterval(() => { this.tick().catch(() => { }); }, 1000);
25
+ }
26
+ stop() {
27
+ if (this.interval) {
28
+ clearInterval(this.interval);
29
+ this.interval = null;
30
+ }
31
+ }
32
+ recover() {
33
+ this.db.prepare("UPDATE email_queue SET status = 'pending' WHERE status = 'sending' AND attempts < 3").run();
34
+ this.db.prepare("UPDATE email_queue SET status = 'failed' WHERE status = 'sending' AND attempts >= 3").run();
35
+ }
36
+ async tick() {
37
+ if (this.running)
38
+ return;
39
+ this.running = true;
40
+ try {
41
+ const items = this.db.prepare(`
42
+ SELECT eq.* FROM email_queue eq
43
+ JOIN email_campaigns ec ON eq.campaign_id = ec.id
44
+ WHERE eq.status = 'pending' AND ec.status = 'sending'
45
+ ORDER BY eq.scheduled_at ASC, eq.created_at ASC
46
+ LIMIT ?
47
+ `).all(this.rateLimit);
48
+ if (items.length === 0) {
49
+ this.running = false;
50
+ return;
51
+ }
52
+ const campaignCache = new Map();
53
+ const getCampaign = (id) => {
54
+ if (!campaignCache.has(id)) {
55
+ campaignCache.set(id, this.db.prepare('SELECT id, subject, body_html, body_text, from_email, from_name, status FROM email_campaigns WHERE id = ?').get(id));
56
+ }
57
+ return campaignCache.get(id);
58
+ };
59
+ for (const item of items) {
60
+ const campaign = getCampaign(item.campaign_id);
61
+ if (campaign.status !== 'sending')
62
+ continue;
63
+ this.db.prepare("UPDATE email_queue SET status = 'sending' WHERE id = ?").run(item.id);
64
+ try {
65
+ const unsubUrl = generateUnsubscribeUrl(item.to_email, this.jwtSecret, this.serverUrl);
66
+ const html = injectUnsubscribeFooter(campaign.body_html, unsubUrl);
67
+ const headers = getUnsubscribeHeaders(unsubUrl);
68
+ const result = await this.ses.sendEmail({
69
+ to: item.to_email,
70
+ from: campaign.from_email,
71
+ fromName: campaign.from_name ?? '',
72
+ subject: campaign.subject,
73
+ htmlBody: html,
74
+ textBody: campaign.body_text ?? '',
75
+ headers,
76
+ });
77
+ this.db.transaction(() => {
78
+ this.db.prepare("UPDATE email_queue SET status = 'sent', ses_message_id = ?, sent_at = ?, attempts = attempts + 1 WHERE id = ?")
79
+ .run(result.messageId, new Date().toISOString(), item.id);
80
+ this.db.prepare('UPDATE email_campaigns SET sent_count = sent_count + 1 WHERE id = ?').run(item.campaign_id);
81
+ })();
82
+ }
83
+ catch (err) {
84
+ const newAttempts = item.attempts + 1;
85
+ const newStatus = newAttempts >= 3 ? 'failed' : 'pending';
86
+ this.db.transaction(() => {
87
+ this.db.prepare('UPDATE email_queue SET status = ?, attempts = ?, error = ? WHERE id = ?')
88
+ .run(newStatus, newAttempts, err.message, item.id);
89
+ if (newStatus === 'failed') {
90
+ this.db.prepare('UPDATE email_campaigns SET failed_count = failed_count + 1 WHERE id = ?').run(item.campaign_id);
91
+ }
92
+ })();
93
+ }
94
+ }
95
+ // Check campaign completion
96
+ const campaignIds = [...new Set(items.map(i => i.campaign_id))];
97
+ for (const cid of campaignIds) {
98
+ const pending = this.db.prepare("SELECT COUNT(*) as count FROM email_queue WHERE campaign_id = ? AND status IN ('pending', 'sending')").get(cid);
99
+ if (pending.count === 0) {
100
+ this.db.prepare("UPDATE email_campaigns SET status = 'sent', completed_at = ? WHERE id = ? AND status = 'sending'")
101
+ .run(new Date().toISOString(), cid);
102
+ }
103
+ }
104
+ // Broadcast progress every 5 ticks
105
+ this.tickCount++;
106
+ if (this.broadcast && this.tickCount % 5 === 0) {
107
+ for (const cid of campaignIds) {
108
+ const stats = this.db.prepare('SELECT sent_count, failed_count, total_recipients, status FROM email_campaigns WHERE id = ?').get(cid);
109
+ if (stats)
110
+ this.broadcast({ type: 'email:progress', campaignId: cid, sent: stats.sent_count, failed: stats.failed_count, total: stats.total_recipients });
111
+ }
112
+ }
113
+ }
114
+ finally {
115
+ this.running = false;
116
+ }
117
+ }
118
+ }
119
+ //# sourceMappingURL=email-queue-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-queue-worker.js","sourceRoot":"","sources":["../../src/email/email-queue-worker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export declare function generateUnsubscribeToken(email: string, jwtSecret: string): string;
2
+ export declare function generateUnsubscribeUrl(email: string, jwtSecret: string, serverUrl: string): string;
3
+ export declare function injectUnsubscribeFooter(htmlBody: string, unsubscribeUrl: string): string;
4
+ export declare function getUnsubscribeHeaders(unsubscribeUrl: string): Record<string, string>;
5
+ //# sourceMappingURL=email-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-utils.d.ts","sourceRoot":"","sources":["../../src/email/email-utils.ts"],"names":[],"mappings":"AAEA,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEjF;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAGlG;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CASxF;AAED,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAKpF"}
@@ -0,0 +1,24 @@
1
+ import jwt from 'jsonwebtoken';
2
+ export function generateUnsubscribeToken(email, jwtSecret) {
3
+ return jwt.sign({ email, action: 'unsubscribe' }, jwtSecret, { expiresIn: '90d' });
4
+ }
5
+ export function generateUnsubscribeUrl(email, jwtSecret, serverUrl) {
6
+ const token = generateUnsubscribeToken(email, jwtSecret);
7
+ return `${serverUrl}/api/email/unsubscribe/${token}`;
8
+ }
9
+ export function injectUnsubscribeFooter(htmlBody, unsubscribeUrl) {
10
+ const footer = `<div style="margin-top:32px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:12px;color:#888;">
11
+ <a href="${unsubscribeUrl}" style="color:#888;text-decoration:underline;">unsubscribe</a>
12
+ </div>`;
13
+ if (htmlBody.includes('</body>')) {
14
+ return htmlBody.replace('</body>', `${footer}</body>`);
15
+ }
16
+ return htmlBody + footer;
17
+ }
18
+ export function getUnsubscribeHeaders(unsubscribeUrl) {
19
+ return {
20
+ 'List-Unsubscribe': `<${unsubscribeUrl}>`,
21
+ 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
22
+ };
23
+ }
24
+ //# sourceMappingURL=email-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-utils.js","sourceRoot":"","sources":["../../src/email/email-utils.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,cAAc,CAAC;AAE/B,MAAM,UAAU,wBAAwB,CAAC,KAAa,EAAE,SAAiB;IACvE,OAAO,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,EAAE,SAAS,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,KAAa,EAAE,SAAiB,EAAE,SAAiB;IACxF,MAAM,KAAK,GAAG,wBAAwB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IACzD,OAAO,GAAG,SAAS,0BAA0B,KAAK,EAAE,CAAC;AACvD,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,QAAgB,EAAE,cAAsB;IAC9E,MAAM,MAAM,GAAG;aACJ,cAAc;OACpB,CAAC;IAEN,IAAI,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QACjC,OAAO,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,MAAM,SAAS,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,QAAQ,GAAG,MAAM,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,cAAsB;IAC1D,OAAO;QACL,kBAAkB,EAAE,IAAI,cAAc,GAAG;QACzC,uBAAuB,EAAE,4BAA4B;KACtD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
2
+ import type Database from 'better-sqlite3';
3
+ interface LeadRoutesOptions extends FastifyPluginOptions {
4
+ db: Database.Database;
5
+ }
6
+ export declare function leadRoutes(app: FastifyInstance, opts: LeadRoutesOptions): Promise<void>;
7
+ export {};
8
+ //# sourceMappingURL=lead-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lead-routes.d.ts","sourceRoot":"","sources":["../../src/email/lead-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AACrE,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAG3C,UAAU,iBAAkB,SAAQ,oBAAoB;IACtD,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAC;CACvB;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwF7F"}