@bernierllc/email-webhook-events 1.0.0 → 1.2.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/CHANGELOG.md +50 -0
- package/README.md +42 -0
- package/__tests__/fixtures/sendgrid-webhooks/.gitkeep +0 -0
- package/__tests__/fixtures/sendgrid-webhooks/README.md +63 -0
- package/__tests__/fixtures/sendgrid-webhooks/deferred.json +31 -0
- package/__tests__/fixtures/sendgrid-webhooks/delivered.json +83 -0
- package/__tests__/fixtures/sendgrid-webhooks/open.json +83 -0
- package/__tests__/integration/ngrok-tunnel-manager.ts +88 -0
- package/__tests__/integration/sendgrid-webhook-integration.test.ts +272 -0
- package/__tests__/integration/webhook-server.ts +188 -0
- package/__tests__/mocks/README.md +172 -0
- package/__tests__/mocks/index.ts +94 -0
- package/__tests__/mocks/sendgrid-api-responses.ts +391 -0
- package/__tests__/mocks/sendgrid-webhook-payloads.ts +463 -0
- package/jest.config.cjs +16 -7
- package/package.json +23 -15
- package/coverage/clover.xml +0 -328
- package/coverage/coverage-final.json +0 -10
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -161
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov-report/src/EmailWebhookEventsService.ts.html +0 -826
- package/coverage/lcov-report/src/analytics/AnalyticsEngine.ts.html +0 -775
- package/coverage/lcov-report/src/analytics/index.html +0 -116
- package/coverage/lcov-report/src/index.html +0 -131
- package/coverage/lcov-report/src/normalizers/MailgunNormalizer.ts.html +0 -301
- package/coverage/lcov-report/src/normalizers/PostmarkNormalizer.ts.html +0 -301
- package/coverage/lcov-report/src/normalizers/SESNormalizer.ts.html +0 -436
- package/coverage/lcov-report/src/normalizers/SMTP2GONormalizer.ts.html +0 -247
- package/coverage/lcov-report/src/normalizers/SendGridNormalizer.ts.html +0 -274
- package/coverage/lcov-report/src/normalizers/index.html +0 -176
- package/coverage/lcov-report/src/persistence/InMemoryPersistenceAdapter.ts.html +0 -289
- package/coverage/lcov-report/src/persistence/index.html +0 -116
- package/coverage/lcov-report/src/types.ts.html +0 -823
- package/coverage/lcov.info +0 -710
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
# 1.2.0 (2025-12-25)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **ci:** add lerna version step before publish ([3d20300](https://github.com/bernierllc/tools/commit/3d203002143bf353fffafe4f8a78a99009567347))
|
|
12
|
+
* update neverhub-adapter deps to workspace:* and publish 0.1.2 ([f0e3d04](https://github.com/bernierllc/tools/commit/f0e3d04d8d4f094e3bb899ddf81e93243d16e2c2))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **email-webhook-events:** migrate to Vitest and add integration testing ([c45c75c](https://github.com/bernierllc/tools/commit/c45c75ce2db9cc9f45fe1be6c73e8018a427727c))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# 1.1.0 (2025-12-25)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* update neverhub-adapter deps to workspace:* and publish 0.1.2 ([f0e3d04](https://github.com/bernierllc/tools/commit/f0e3d04d8d4f094e3bb899ddf81e93243d16e2c2))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Features
|
|
32
|
+
|
|
33
|
+
* **email-webhook-events:** migrate to Vitest and add integration testing ([c45c75c](https://github.com/bernierllc/tools/commit/c45c75ce2db9cc9f45fe1be6c73e8018a427727c))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# @bernierllc/email-webhook-events
|
|
40
|
+
|
|
41
|
+
## 1.0.1
|
|
42
|
+
|
|
43
|
+
### Patch Changes
|
|
44
|
+
|
|
45
|
+
- [`c45c75c`](https://github.com/bernierllc/tools/commit/c45c75ce2db9cc9f45fe1be6c73e8018a427727c) - Migrate test infrastructure from Jest to Vitest and add integration testing support
|
|
46
|
+
- Switch from Jest to Vitest for improved ESM compatibility
|
|
47
|
+
- Add integration test infrastructure using ngrok for real SendGrid webhook testing
|
|
48
|
+
- Add @bernierllc/email-manager as dependency for integration tests
|
|
49
|
+
- Capture real webhook payloads for accurate test fixtures
|
|
50
|
+
- Update README with integration testing documentation
|
package/README.md
CHANGED
|
@@ -321,6 +321,48 @@ EMAIL_WEBHOOK_RATE_LIMIT_ENABLED=false
|
|
|
321
321
|
EMAIL_WEBHOOK_RATE_LIMIT_MAX_PER_SECOND=100
|
|
322
322
|
```
|
|
323
323
|
|
|
324
|
+
## Integration Testing
|
|
325
|
+
|
|
326
|
+
This package includes integration tests that use real SendGrid webhooks.
|
|
327
|
+
|
|
328
|
+
### Prerequisites
|
|
329
|
+
|
|
330
|
+
1. SendGrid account with API key
|
|
331
|
+
2. Paid ngrok account with auth token
|
|
332
|
+
3. Test email address
|
|
333
|
+
4. `.env.test` file configured with:
|
|
334
|
+
- `SENDGRID_API_KEY`
|
|
335
|
+
- `SENDGRID_WEBHOOK_SECRET` (optional)
|
|
336
|
+
- `TEST_EMAIL`
|
|
337
|
+
- `NGROK_AUTH_TOKEN`
|
|
338
|
+
- `NGROK_RESERVED_DOMAIN` (optional, for consistent URLs)
|
|
339
|
+
- `WEBHOOK_SERVER_PORT` (optional, defaults to 3000)
|
|
340
|
+
|
|
341
|
+
### Running Integration Tests
|
|
342
|
+
|
|
343
|
+
1. Ensure `.env.test` file is configured with all required variables
|
|
344
|
+
2. Set `RUN_INTEGRATION_TESTS=true` environment variable
|
|
345
|
+
3. Configure SendGrid webhook URL (first time only, or use reserved domain):
|
|
346
|
+
- Run test once to get webhook URL from output, or
|
|
347
|
+
- Use reserved domain: `https://your-reserved-domain.ngrok.io/webhooks/sendgrid`
|
|
348
|
+
- Go to SendGrid dashboard → Settings → Mail Settings → Event Webhook
|
|
349
|
+
- Set webhook URL and enable all event types
|
|
350
|
+
4. Run tests: `npm test -- sendgrid-webhook-integration`
|
|
351
|
+
|
|
352
|
+
Tests automatically:
|
|
353
|
+
- Start webhook server
|
|
354
|
+
- Create ngrok tunnel (programmatically)
|
|
355
|
+
- Send test emails via @bernierllc/email-manager
|
|
356
|
+
- Capture and process webhooks
|
|
357
|
+
- Save payloads to `__tests__/fixtures/sendgrid-webhooks/`
|
|
358
|
+
|
|
359
|
+
### Captured Payloads
|
|
360
|
+
|
|
361
|
+
Real webhook payloads are captured in `__tests__/fixtures/sendgrid-webhooks/`
|
|
362
|
+
and used to generate accurate mocks for unit tests.
|
|
363
|
+
|
|
364
|
+
See `__tests__/fixtures/sendgrid-webhooks/README.md` for more details.
|
|
365
|
+
|
|
324
366
|
## Integration Status
|
|
325
367
|
|
|
326
368
|
- **Logger**: integrated - Uses MockLogger (pending @bernierllc/logger publication)
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# SendGrid Webhook Fixtures
|
|
2
|
+
|
|
3
|
+
This directory contains webhook payloads based on REAL SendGrid webhook structures captured during integration testing.
|
|
4
|
+
|
|
5
|
+
> ⚠️ **SANITIZED DATA**: All email addresses, IPs, message IDs, signatures, and other identifiable data have been replaced with fake test values. The **structure** matches real SendGrid payloads but all values are safe for version control.
|
|
6
|
+
|
|
7
|
+
## Structure
|
|
8
|
+
|
|
9
|
+
Each event type has its own JSON file containing an array of captured webhook payloads:
|
|
10
|
+
|
|
11
|
+
- `delivered.json` - Email delivered events
|
|
12
|
+
- `open.json` - Email opened events
|
|
13
|
+
- `click.json` - Link clicked events
|
|
14
|
+
- `bounce.json` - Email bounced events
|
|
15
|
+
- `spamreport.json` - Spam complaint events
|
|
16
|
+
- `unsubscribe.json` - Unsubscribe events
|
|
17
|
+
- `deferred.json` - Email delivery deferred events
|
|
18
|
+
- `dropped.json` - Email dropped events
|
|
19
|
+
|
|
20
|
+
## File Format
|
|
21
|
+
|
|
22
|
+
Each file contains an array of captured webhook objects:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
[
|
|
26
|
+
{
|
|
27
|
+
"headers": {
|
|
28
|
+
"x-twilio-email-event-webhook-signature": "...",
|
|
29
|
+
"content-type": "application/json",
|
|
30
|
+
...
|
|
31
|
+
},
|
|
32
|
+
"body": {
|
|
33
|
+
"event": "delivered",
|
|
34
|
+
"email": "user@example.com",
|
|
35
|
+
"timestamp": 1234567890,
|
|
36
|
+
"sg_message_id": "...",
|
|
37
|
+
...
|
|
38
|
+
},
|
|
39
|
+
"timestamp": "2025-01-27T12:00:00.000Z",
|
|
40
|
+
"eventType": "delivered"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
These fixtures are automatically generated during integration tests and can be used to:
|
|
48
|
+
|
|
49
|
+
1. Generate accurate mocks for unit tests
|
|
50
|
+
2. Validate webhook payload structure
|
|
51
|
+
3. Test normalizer implementations with real data
|
|
52
|
+
4. Debug webhook processing issues
|
|
53
|
+
|
|
54
|
+
## Generation
|
|
55
|
+
|
|
56
|
+
Fixtures are automatically captured when running integration tests:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm test -- sendgrid-webhook-integration
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The webhook server automatically saves all received webhooks to the appropriate fixture files.
|
|
63
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"headers": {
|
|
4
|
+
"host": "example-webhook.ngrok.app",
|
|
5
|
+
"user-agent": "SendGrid Event API",
|
|
6
|
+
"content-length": "498",
|
|
7
|
+
"accept-encoding": "gzip",
|
|
8
|
+
"content-type": "application/json;charset=utf-8",
|
|
9
|
+
"x-forwarded-for": "192.0.2.30",
|
|
10
|
+
"x-forwarded-host": "example-webhook.ngrok.app",
|
|
11
|
+
"x-forwarded-proto": "https",
|
|
12
|
+
"x-twilio-email-event-webhook-signature": "FAKE_SIGNATURE_MEQCIG5UlgMJ_DO_NOT_USE_IN_PRODUCTION=",
|
|
13
|
+
"x-twilio-email-event-webhook-timestamp": "1700000300"
|
|
14
|
+
},
|
|
15
|
+
"body": {
|
|
16
|
+
"attempt": "2",
|
|
17
|
+
"domain": "example.com",
|
|
18
|
+
"email": "deferred-recipient@example.com",
|
|
19
|
+
"event": "deferred",
|
|
20
|
+
"from": "Test Sender <sender@example.com>",
|
|
21
|
+
"response": "temporarily unable to deliver - server busy, please retry later",
|
|
22
|
+
"sg_event_id": "FAKE_DEFERRED_EVENT_001",
|
|
23
|
+
"sg_message_id": "FAKE_MSG_DEFERRED_001.recvd-1234567890-pqrst-1-1234567B-14.0",
|
|
24
|
+
"smtp-id": "<FAKE_MSG_DEFERRED_001@geopod-ismtpd-test>",
|
|
25
|
+
"timestamp": 1700000300,
|
|
26
|
+
"tls": 0
|
|
27
|
+
},
|
|
28
|
+
"timestamp": "2025-01-01T12:05:00.000Z",
|
|
29
|
+
"eventType": "deferred"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"headers": {
|
|
4
|
+
"host": "example-webhook.ngrok.app",
|
|
5
|
+
"user-agent": "SendGrid Event API",
|
|
6
|
+
"content-length": "405",
|
|
7
|
+
"accept-encoding": "gzip",
|
|
8
|
+
"content-type": "application/json;charset=utf-8",
|
|
9
|
+
"x-forwarded-for": "192.0.2.1",
|
|
10
|
+
"x-forwarded-host": "example-webhook.ngrok.app",
|
|
11
|
+
"x-forwarded-proto": "https",
|
|
12
|
+
"x-twilio-email-event-webhook-signature": "FAKE_SIGNATURE_MEUCIB5bSBgVWU_DO_NOT_USE_IN_PRODUCTION=",
|
|
13
|
+
"x-twilio-email-event-webhook-timestamp": "1700000000"
|
|
14
|
+
},
|
|
15
|
+
"body": {
|
|
16
|
+
"email": "test-recipient@example.com",
|
|
17
|
+
"event": "delivered",
|
|
18
|
+
"ip": "192.0.2.10",
|
|
19
|
+
"response": "250 2.0.0 OK 1700000000 example-mail-server.example.com - gsmtp",
|
|
20
|
+
"sg_event_id": "FAKE_DELIVERED_EVENT_001",
|
|
21
|
+
"sg_message_id": "FAKE_MSG_001.recvd-1234567890-abcde-1-12345678-37.0",
|
|
22
|
+
"smtp-id": "<FAKE_MSG_001@geopod-ismtpd-test>",
|
|
23
|
+
"timestamp": 1700000000,
|
|
24
|
+
"tls": 1
|
|
25
|
+
},
|
|
26
|
+
"timestamp": "2025-01-01T12:00:00.000Z",
|
|
27
|
+
"eventType": "delivered"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"headers": {
|
|
31
|
+
"host": "example-webhook.ngrok.app",
|
|
32
|
+
"user-agent": "SendGrid Event API",
|
|
33
|
+
"content-length": "405",
|
|
34
|
+
"accept-encoding": "gzip",
|
|
35
|
+
"content-type": "application/json;charset=utf-8",
|
|
36
|
+
"x-forwarded-for": "192.0.2.2",
|
|
37
|
+
"x-forwarded-host": "example-webhook.ngrok.app",
|
|
38
|
+
"x-forwarded-proto": "https",
|
|
39
|
+
"x-twilio-email-event-webhook-signature": "FAKE_SIGNATURE_MEUCICpeBdSY_DO_NOT_USE_IN_PRODUCTION=",
|
|
40
|
+
"x-twilio-email-event-webhook-timestamp": "1700000100"
|
|
41
|
+
},
|
|
42
|
+
"body": {
|
|
43
|
+
"email": "test-recipient@example.com",
|
|
44
|
+
"event": "delivered",
|
|
45
|
+
"ip": "192.0.2.11",
|
|
46
|
+
"response": "250 2.0.0 OK 1700000100 example-mail-server.example.com - gsmtp",
|
|
47
|
+
"sg_event_id": "FAKE_DELIVERED_EVENT_002",
|
|
48
|
+
"sg_message_id": "FAKE_MSG_002.recvd-1234567890-fghij-1-12345679-10.0",
|
|
49
|
+
"smtp-id": "<FAKE_MSG_002@geopod-ismtpd-test>",
|
|
50
|
+
"timestamp": 1700000100,
|
|
51
|
+
"tls": 1
|
|
52
|
+
},
|
|
53
|
+
"timestamp": "2025-01-01T12:01:40.000Z",
|
|
54
|
+
"eventType": "delivered"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"headers": {
|
|
58
|
+
"host": "example-webhook.ngrok.app",
|
|
59
|
+
"user-agent": "SendGrid Event API",
|
|
60
|
+
"content-length": "405",
|
|
61
|
+
"accept-encoding": "gzip",
|
|
62
|
+
"content-type": "application/json;charset=utf-8",
|
|
63
|
+
"x-forwarded-for": "192.0.2.3",
|
|
64
|
+
"x-forwarded-host": "example-webhook.ngrok.app",
|
|
65
|
+
"x-forwarded-proto": "https",
|
|
66
|
+
"x-twilio-email-event-webhook-signature": "FAKE_SIGNATURE_MEQCIAektsAA_DO_NOT_USE_IN_PRODUCTION=",
|
|
67
|
+
"x-twilio-email-event-webhook-timestamp": "1700000200"
|
|
68
|
+
},
|
|
69
|
+
"body": {
|
|
70
|
+
"email": "another-recipient@example.com",
|
|
71
|
+
"event": "delivered",
|
|
72
|
+
"ip": "192.0.2.12",
|
|
73
|
+
"response": "250 2.0.0 OK 1700000200 example-mail-server.example.com - gsmtp",
|
|
74
|
+
"sg_event_id": "FAKE_DELIVERED_EVENT_003",
|
|
75
|
+
"sg_message_id": "FAKE_MSG_003.recvd-1234567890-klmno-1-1234567A-10.0",
|
|
76
|
+
"smtp-id": "<FAKE_MSG_003@geopod-ismtpd-test>",
|
|
77
|
+
"timestamp": 1700000200,
|
|
78
|
+
"tls": 1
|
|
79
|
+
},
|
|
80
|
+
"timestamp": "2025-01-01T12:03:20.000Z",
|
|
81
|
+
"eventType": "delivered"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"headers": {
|
|
4
|
+
"host": "example-webhook.ngrok.app",
|
|
5
|
+
"user-agent": "SendGrid Event API",
|
|
6
|
+
"content-length": "416",
|
|
7
|
+
"accept-encoding": "gzip",
|
|
8
|
+
"content-type": "application/json;charset=utf-8",
|
|
9
|
+
"x-forwarded-for": "192.0.2.20",
|
|
10
|
+
"x-forwarded-host": "example-webhook.ngrok.app",
|
|
11
|
+
"x-forwarded-proto": "https",
|
|
12
|
+
"x-twilio-email-event-webhook-signature": "FAKE_SIGNATURE_MEQCIHN5S5nZ_DO_NOT_USE_IN_PRODUCTION=",
|
|
13
|
+
"x-twilio-email-event-webhook-timestamp": "1700000050"
|
|
14
|
+
},
|
|
15
|
+
"body": {
|
|
16
|
+
"email": "test-recipient@example.com",
|
|
17
|
+
"event": "open",
|
|
18
|
+
"ip": "192.0.2.100",
|
|
19
|
+
"sg_content_type": "html",
|
|
20
|
+
"sg_event_id": "FAKE_OPEN_EVENT_001",
|
|
21
|
+
"sg_machine_open": false,
|
|
22
|
+
"sg_message_id": "FAKE_MSG_001.recvd-1234567890-abcde-1-12345678-1B.0",
|
|
23
|
+
"timestamp": 1700000050,
|
|
24
|
+
"useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
25
|
+
},
|
|
26
|
+
"timestamp": "2025-01-01T12:00:50.000Z",
|
|
27
|
+
"eventType": "open"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"headers": {
|
|
31
|
+
"host": "example-webhook.ngrok.app",
|
|
32
|
+
"user-agent": "SendGrid Event API",
|
|
33
|
+
"content-length": "416",
|
|
34
|
+
"accept-encoding": "gzip",
|
|
35
|
+
"content-type": "application/json;charset=utf-8",
|
|
36
|
+
"x-forwarded-for": "192.0.2.21",
|
|
37
|
+
"x-forwarded-host": "example-webhook.ngrok.app",
|
|
38
|
+
"x-forwarded-proto": "https",
|
|
39
|
+
"x-twilio-email-event-webhook-signature": "FAKE_SIGNATURE_MEQCIFFHHjr5_DO_NOT_USE_IN_PRODUCTION=",
|
|
40
|
+
"x-twilio-email-event-webhook-timestamp": "1700000150"
|
|
41
|
+
},
|
|
42
|
+
"body": {
|
|
43
|
+
"email": "test-recipient@example.com",
|
|
44
|
+
"event": "open",
|
|
45
|
+
"ip": "192.0.2.101",
|
|
46
|
+
"sg_content_type": "html",
|
|
47
|
+
"sg_event_id": "FAKE_OPEN_EVENT_002",
|
|
48
|
+
"sg_machine_open": false,
|
|
49
|
+
"sg_message_id": "FAKE_MSG_002.recvd-1234567890-fghij-1-12345679-10.0",
|
|
50
|
+
"timestamp": 1700000150,
|
|
51
|
+
"useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
52
|
+
},
|
|
53
|
+
"timestamp": "2025-01-01T12:02:30.000Z",
|
|
54
|
+
"eventType": "open"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"headers": {
|
|
58
|
+
"host": "example-webhook.ngrok.app",
|
|
59
|
+
"user-agent": "SendGrid Event API",
|
|
60
|
+
"content-length": "366",
|
|
61
|
+
"accept-encoding": "gzip",
|
|
62
|
+
"content-type": "application/json;charset=utf-8",
|
|
63
|
+
"x-forwarded-for": "192.0.2.22",
|
|
64
|
+
"x-forwarded-host": "example-webhook.ngrok.app",
|
|
65
|
+
"x-forwarded-proto": "https",
|
|
66
|
+
"x-twilio-email-event-webhook-signature": "FAKE_SIGNATURE_MEUCIQC7bY6P_DO_NOT_USE_IN_PRODUCTION=",
|
|
67
|
+
"x-twilio-email-event-webhook-timestamp": "1700000250"
|
|
68
|
+
},
|
|
69
|
+
"body": {
|
|
70
|
+
"email": "another-recipient@example.com",
|
|
71
|
+
"event": "open",
|
|
72
|
+
"ip": "192.0.2.102",
|
|
73
|
+
"sg_content_type": "html",
|
|
74
|
+
"sg_event_id": "FAKE_OPEN_EVENT_003",
|
|
75
|
+
"sg_machine_open": true,
|
|
76
|
+
"sg_message_id": "FAKE_MSG_003.recvd-1234567890-klmno-1-1234567A-10.0",
|
|
77
|
+
"timestamp": 1700000250,
|
|
78
|
+
"useragent": "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0 (via ggpht.com GoogleImageProxy)"
|
|
79
|
+
},
|
|
80
|
+
"timestamp": "2025-01-01T12:04:10.000Z",
|
|
81
|
+
"eventType": "open"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
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 { connect, disconnect } from '@ngrok/ngrok';
|
|
10
|
+
|
|
11
|
+
export interface NgrokTunnelConfig {
|
|
12
|
+
port: number;
|
|
13
|
+
domain?: string; // Reserved domain (paid account feature)
|
|
14
|
+
authtoken?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class NgrokTunnelManager {
|
|
18
|
+
private listener: any;
|
|
19
|
+
private publicUrl: string | null = null;
|
|
20
|
+
|
|
21
|
+
async start(config: NgrokTunnelConfig): Promise<string> {
|
|
22
|
+
const connectOptions: any = {
|
|
23
|
+
addr: config.port,
|
|
24
|
+
authtoken: config.authtoken || process.env.NGROK_AUTH_TOKEN
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Use reserved domain if available (paid account feature)
|
|
28
|
+
if (config.domain) {
|
|
29
|
+
connectOptions.domain = config.domain;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Disconnect any existing tunnels first (in case of previous failed runs)
|
|
34
|
+
try {
|
|
35
|
+
await disconnect();
|
|
36
|
+
console.log('Disconnected existing ngrok tunnels');
|
|
37
|
+
} catch {
|
|
38
|
+
// Ignore errors if no tunnels exist
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.listener = await connect(connectOptions);
|
|
42
|
+
this.publicUrl = this.listener.url();
|
|
43
|
+
|
|
44
|
+
if (!this.publicUrl) {
|
|
45
|
+
throw new Error('Failed to get ngrok tunnel URL');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`ngrok tunnel established: ${this.publicUrl}`);
|
|
49
|
+
return this.publicUrl;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
52
|
+
throw new Error(`Failed to start ngrok tunnel: ${errorMessage}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async stop(): Promise<void> {
|
|
57
|
+
if (this.listener) {
|
|
58
|
+
try {
|
|
59
|
+
// Use disconnect with the URL, or close the listener directly
|
|
60
|
+
const url = this.listener.url();
|
|
61
|
+
if (url) {
|
|
62
|
+
const { disconnect } = await import('@ngrok/ngrok');
|
|
63
|
+
await disconnect(url);
|
|
64
|
+
}
|
|
65
|
+
this.listener = null;
|
|
66
|
+
this.publicUrl = null;
|
|
67
|
+
console.log('ngrok tunnel closed');
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
70
|
+
console.warn(`Error closing ngrok tunnel: ${errorMessage}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getUrl(): string | null {
|
|
76
|
+
return this.publicUrl;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Get tunnel statistics (paid account feature)
|
|
80
|
+
async getStats(): Promise<any> {
|
|
81
|
+
if (!this.listener) {
|
|
82
|
+
throw new Error('Tunnel not started');
|
|
83
|
+
}
|
|
84
|
+
// Access tunnel metrics via ngrok API
|
|
85
|
+
return this.listener;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|