@elizaos/plugin-ngrok 2.0.0-beta.1 → 2.0.11-beta.7

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 (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -294
  3. package/package.json +24 -7
  4. package/src/__tests__/test-utils.ts +2 -2
  5. package/src/__tests__/unit/environment.test.ts +1 -1
  6. package/dist/__tests__/NgrokTestSuite.d.ts +0 -6
  7. package/dist/__tests__/NgrokTestSuite.d.ts.map +0 -1
  8. package/dist/__tests__/NgrokTestSuite.js +0 -92
  9. package/dist/__tests__/NgrokTestSuite.js.map +0 -1
  10. package/dist/actions/get-tunnel-status.d.ts +0 -4
  11. package/dist/actions/get-tunnel-status.d.ts.map +0 -1
  12. package/dist/actions/get-tunnel-status.js +0 -186
  13. package/dist/actions/get-tunnel-status.js.map +0 -1
  14. package/dist/actions/start-tunnel.d.ts +0 -4
  15. package/dist/actions/start-tunnel.d.ts.map +0 -1
  16. package/dist/actions/start-tunnel.js +0 -221
  17. package/dist/actions/start-tunnel.js.map +0 -1
  18. package/dist/actions/stop-tunnel.d.ts +0 -4
  19. package/dist/actions/stop-tunnel.d.ts.map +0 -1
  20. package/dist/actions/stop-tunnel.js +0 -174
  21. package/dist/actions/stop-tunnel.js.map +0 -1
  22. package/dist/environment.d.ts +0 -12
  23. package/dist/environment.d.ts.map +0 -1
  24. package/dist/environment.js +0 -68
  25. package/dist/environment.js.map +0 -1
  26. package/dist/index.d.ts +0 -13
  27. package/dist/index.d.ts.map +0 -1
  28. package/dist/index.js +0 -29
  29. package/dist/index.js.map +0 -1
  30. package/dist/services/NgrokService.d.ts +0 -30
  31. package/dist/services/NgrokService.d.ts.map +0 -1
  32. package/dist/services/NgrokService.js +0 -333
  33. package/dist/services/NgrokService.js.map +0 -1
  34. package/src/__tests__/e2e/real-ngrok.test.ts +0 -543
  35. package/src/__tests__/unit/actions.test.ts +0 -402
  36. package/src/actions/get-tunnel-status.ts +0 -218
  37. package/src/actions/start-tunnel.ts +0 -255
  38. package/src/actions/stop-tunnel.ts +0 -203
@@ -1,543 +0,0 @@
1
- import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, jest } from 'bun:test';
2
- import * as http from 'node:http';
3
- import * as path from 'node:path';
4
- import type { IAgentRuntime, Memory, State } from '@elizaos/core';
5
- import { config } from 'dotenv';
6
- import { getTunnelStatusAction } from '../../actions/get-tunnel-status';
7
- import { startTunnelAction } from '../../actions/start-tunnel';
8
- import { stopTunnelAction } from '../../actions/stop-tunnel';
9
- import { NgrokService } from '../../services/NgrokService';
10
- import { testConfig, testDelay } from '../test-config';
11
- import { ngrokSafeDelay } from '../test-helpers';
12
-
13
- // Load environment variables
14
- config({ path: path.resolve(process.cwd(), '.env') });
15
-
16
- type PortExtractionParams = {
17
- context?: {
18
- userMessage?: string;
19
- };
20
- };
21
-
22
- describe('Real ngrok API E2E Tests', () => {
23
- let runtime: IAgentRuntime;
24
- let service: NgrokService;
25
- let testServer: http.Server;
26
- let testServerPort: number;
27
- let skipTests = false;
28
-
29
- beforeAll(async () => {
30
- // Check if we should skip tests
31
- const hasAuthToken = Boolean(process.env.NGROK_AUTH_TOKEN);
32
- const skipEnvVar = process.env.SKIP_NGROK_TESTS === 'true';
33
-
34
- console.log('E2E test environment check:');
35
- console.log('- NGROK_AUTH_TOKEN:', hasAuthToken ? 'Set' : 'Not set');
36
- console.log('- NGROK_DOMAIN:', process.env.NGROK_DOMAIN || 'Not set (will use random URL)');
37
- console.log('- SKIP_NGROK_TESTS:', skipEnvVar);
38
-
39
- if (!hasAuthToken || skipEnvVar) {
40
- skipTests = true;
41
- console.log('⚠️ Skipping E2E tests');
42
- return;
43
- }
44
-
45
- // Add delay before starting the test suite
46
- await testDelay(testConfig.execution.suitesDelay);
47
-
48
- // Create a test HTTP server
49
- testServer = http.createServer((req, res) => {
50
- const chunks: Buffer[] = [];
51
-
52
- req.on('data', (chunk) => chunks.push(chunk));
53
- req.on('end', () => {
54
- const body = Buffer.concat(chunks).toString();
55
-
56
- // Log the request for debugging
57
- console.log(`Received ${req.method} request to ${req.url}`);
58
-
59
- // Handle different endpoints
60
- if (req.url === '/health') {
61
- res.writeHead(200, { 'Content-Type': 'application/json' });
62
- res.end(JSON.stringify({ status: 'healthy', timestamp: new Date().toISOString() }));
63
- } else if (req.url === '/webhook') {
64
- res.writeHead(200, { 'Content-Type': 'application/json' });
65
- res.end(
66
- JSON.stringify({
67
- received: true,
68
- method: req.method,
69
- headers: req.headers,
70
- body: body ? JSON.parse(body) : null,
71
- })
72
- );
73
- } else if (req.url === '/echo') {
74
- res.writeHead(200, { 'Content-Type': 'application/json' });
75
- res.end(body || JSON.stringify({ echo: 'empty' }));
76
- } else {
77
- res.writeHead(404, { 'Content-Type': 'application/json' });
78
- res.end(JSON.stringify({ error: 'Not found' }));
79
- }
80
- });
81
- });
82
-
83
- // Start server on random port
84
- await new Promise<void>((resolve) => {
85
- testServer.listen(0, () => {
86
- const address = testServer.address();
87
- if (address && typeof address === 'object') {
88
- testServerPort = address.port;
89
- }
90
- console.log(`✅ Test server started on port ${testServerPort}`);
91
- resolve();
92
- });
93
- });
94
-
95
- // Create runtime with real environment
96
- runtime = {
97
- agentId: 'test-agent',
98
- getSetting: (key: string) => process.env[key],
99
- getService: (name: string) => (name === 'tunnel' ? service : undefined),
100
- registerService: (_service: unknown) => {},
101
- useModel: async (_model: string, params: PortExtractionParams) => {
102
- // Return the port from the message content
103
- const portMatch = params.context?.userMessage?.match(/port (\d+)/);
104
- const port = portMatch ? parseInt(portMatch[1], 10) : 3000;
105
- return JSON.stringify({ port });
106
- },
107
- } as IAgentRuntime;
108
-
109
- // Initialize service
110
- service = new NgrokService(runtime);
111
- });
112
-
113
- afterAll(async () => {
114
- if (skipTests) {
115
- return;
116
- }
117
-
118
- // Clean up any active tunnels
119
- if (service?.isActive()) {
120
- await service.stopTunnel();
121
- await testDelay(testConfig.ngrok.stopWaitTime);
122
- }
123
-
124
- // Stop test server
125
- if (testServer) {
126
- await new Promise<void>((resolve) => {
127
- testServer.close(() => resolve());
128
- });
129
- }
130
- });
131
-
132
- beforeEach(async () => {
133
- if (skipTests) {
134
- return;
135
- }
136
-
137
- // Add delay before each test
138
- await testDelay();
139
-
140
- // Ensure clean state before each test
141
- if (service?.isActive()) {
142
- await service.stopTunnel();
143
- await testDelay(testConfig.ngrok.stopWaitTime);
144
- }
145
- });
146
-
147
- afterEach(async () => {
148
- if (skipTests) {
149
- return;
150
- }
151
-
152
- // Ensure tunnel is stopped after each test
153
- if (service?.isActive()) {
154
- await service.stopTunnel();
155
- }
156
-
157
- // Add delay after each test
158
- await testDelay();
159
- });
160
-
161
- // Helper function for making requests through ngrok
162
- async function fetchWithNgrokHeaders(url: string, options?: RequestInit): Promise<Response> {
163
- return fetch(url, {
164
- ...options,
165
- headers: {
166
- ...options?.headers,
167
- 'ngrok-skip-browser-warning': 'true',
168
- },
169
- });
170
- }
171
-
172
- describe('Basic Tunnel Operations', () => {
173
- it(
174
- 'should start a tunnel with real ngrok',
175
- async () => {
176
- if (skipTests) {
177
- console.log('Test skipped - no auth token');
178
- return;
179
- }
180
-
181
- const url = await service.startTunnel(testServerPort);
182
-
183
- expect(url).toBeTruthy();
184
- expect(url).toMatch(/^https:\/\/[a-zA-Z0-9-]+\.ngrok(-free)?\.app$/);
185
- expect(service.isActive()).toBe(true);
186
-
187
- const status = service.getStatus();
188
- expect(status.active).toBe(true);
189
- expect(status.url).toBe(url as string | null);
190
- expect(status.port).toBe(testServerPort);
191
- expect(status.provider).toBe('ngrok');
192
-
193
- console.log(`✅ Tunnel created: ${url}`);
194
- },
195
- testConfig.execution.e2eTimeout
196
- );
197
-
198
- it(
199
- 'should stop a tunnel',
200
- async () => {
201
- if (skipTests) {
202
- return;
203
- }
204
-
205
- // Start tunnel first
206
- const _url = await service.startTunnel(testServerPort);
207
- expect(service.isActive()).toBe(true);
208
-
209
- // Stop tunnel
210
- await service.stopTunnel();
211
- expect(service.isActive()).toBe(false);
212
- expect(service.getUrl()).toBeNull();
213
-
214
- const status = service.getStatus();
215
- expect(status.active).toBe(false);
216
- expect(status.url).toBeNull();
217
- expect(status.port).toBeNull();
218
- },
219
- testConfig.execution.e2eTimeout
220
- );
221
-
222
- it(
223
- 'should handle multiple start/stop cycles',
224
- async () => {
225
- if (skipTests) {
226
- return;
227
- }
228
-
229
- for (let i = 0; i < 3; i++) {
230
- console.log(`\n🔄 Cycle ${i + 1}/3`);
231
-
232
- try {
233
- const url = await service.startTunnel(testServerPort);
234
- expect(url).toBeTruthy();
235
- expect(service.isActive()).toBe(true);
236
-
237
- await service.stopTunnel();
238
- expect(service.isActive()).toBe(false);
239
-
240
- // Use enhanced delay between cycles to ensure ngrok API is ready
241
- if (i < 2) {
242
- console.log('⏳ Waiting for ngrok to fully clean up...');
243
- await ngrokSafeDelay(testConfig.ngrok.minIntervalBetweenStarts + 2000);
244
- }
245
- } catch (error) {
246
- console.error(
247
- `❌ Cycle ${i + 1} failed:`,
248
- error instanceof Error ? error.message : String(error)
249
- );
250
- // If we get an error, wait extra time before continuing
251
- if (i < 2) {
252
- console.log('⏳ Extra wait after error...');
253
- await ngrokSafeDelay(5000);
254
- }
255
- throw error;
256
- }
257
- }
258
- },
259
- testConfig.execution.e2eTimeout * 2
260
- ); // Double timeout for this test
261
- });
262
-
263
- describe('Action Integration Tests', () => {
264
- it('should start tunnel via action', async () => {
265
- if (skipTests) {
266
- return;
267
- }
268
-
269
- const message: Memory = {
270
- id: '00000000-0000-0000-0000-000000000001' as `${string}-${string}-${string}-${string}-${string}`,
271
- agentId: runtime.agentId,
272
- roomId:
273
- '00000000-0000-0000-0000-000000000003' as `${string}-${string}-${string}-${string}-${string}`,
274
- content: { text: `start tunnel on port ${testServerPort}` },
275
- createdAt: Date.now(),
276
- } as Memory;
277
-
278
- const callback = jest.fn().mockResolvedValue([]);
279
- const result = await startTunnelAction.handler(runtime, message, {} as State, {}, callback);
280
-
281
- expect(result).toBeDefined();
282
- expect(typeof result).toBe('object');
283
- expect(callback).toHaveBeenCalledWith({
284
- text: expect.stringContaining('started successfully'),
285
- metadata: {
286
- action: 'tunnel_started',
287
- tunnelUrl: expect.stringMatching(/^https:\/\/[a-zA-Z0-9-]+\.ngrok(-free)?\.app$/),
288
- port: testServerPort,
289
- },
290
- });
291
-
292
- expect(service.isActive()).toBe(true);
293
- }, 30000);
294
-
295
- it('should get tunnel status via action', async () => {
296
- if (skipTests) {
297
- return;
298
- }
299
-
300
- // Start tunnel first
301
- await service.startTunnel(testServerPort);
302
- const tunnelUrl = service.getUrl();
303
-
304
- const message: Memory = {
305
- id: '00000000-0000-0000-0000-000000000001' as `${string}-${string}-${string}-${string}-${string}`,
306
- agentId: runtime.agentId,
307
- roomId:
308
- '00000000-0000-0000-0000-000000000003' as `${string}-${string}-${string}-${string}-${string}`,
309
- content: { text: 'tunnel status' },
310
- createdAt: Date.now(),
311
- } as Memory;
312
-
313
- const callback = jest.fn().mockResolvedValue([]);
314
- const result = await getTunnelStatusAction.handler(
315
- runtime,
316
- message,
317
- {} as State,
318
- {},
319
- callback
320
- );
321
-
322
- expect(result).toBeDefined();
323
- expect(typeof result).toBe('object');
324
- expect(callback).toHaveBeenCalledWith({
325
- text: expect.stringContaining('Ngrok tunnel is active'),
326
- metadata: expect.objectContaining({
327
- action: 'tunnel_status',
328
- active: true,
329
- url: tunnelUrl,
330
- port: testServerPort,
331
- uptime: expect.any(String),
332
- provider: 'ngrok',
333
- }),
334
- });
335
- }, 30000);
336
-
337
- it('should stop tunnel via action', async () => {
338
- if (skipTests) {
339
- return;
340
- }
341
-
342
- // Start tunnel first
343
- const url = await service.startTunnel(testServerPort);
344
- expect(service.isActive()).toBe(true);
345
-
346
- const message: Memory = {
347
- id: '00000000-0000-0000-0000-000000000001' as `${string}-${string}-${string}-${string}-${string}`,
348
- agentId: runtime.agentId,
349
- roomId:
350
- '00000000-0000-0000-0000-000000000003' as `${string}-${string}-${string}-${string}-${string}`,
351
- content: { text: 'stop tunnel' },
352
- createdAt: Date.now(),
353
- } as Memory;
354
-
355
- const callback = jest.fn().mockResolvedValue([]);
356
- const result = await stopTunnelAction.handler(runtime, message, {} as State, {}, callback);
357
-
358
- expect(result).toBeDefined();
359
- expect(typeof result).toBe('object');
360
- expect(callback).toHaveBeenCalledWith(
361
- expect.objectContaining({
362
- text: expect.stringContaining('stopped successfully'),
363
- metadata: expect.objectContaining({
364
- previousUrl: url,
365
- previousPort: testServerPort,
366
- }),
367
- })
368
- );
369
-
370
- expect(service.isActive()).toBe(false);
371
- }, 30000);
372
- });
373
-
374
- describe('Real HTTP Traffic Tests', () => {
375
- it('should handle real HTTP requests through tunnel', async () => {
376
- if (skipTests) {
377
- return;
378
- }
379
-
380
- const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
381
-
382
- // Test health endpoint
383
- const healthResponse = await fetchWithNgrokHeaders(`${tunnelUrl}/health`);
384
- const healthData = await healthResponse.json();
385
-
386
- expect(healthResponse.status).toBe(200);
387
- expect(healthData.status).toBe('healthy');
388
- expect(healthData.timestamp).toBeTruthy();
389
- }, 30000);
390
-
391
- it('should handle webhook requests through tunnel', async () => {
392
- if (skipTests) {
393
- return;
394
- }
395
-
396
- const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
397
-
398
- // Send webhook request
399
- const webhookPayload = {
400
- event: 'test.webhook',
401
- timestamp: new Date().toISOString(),
402
- data: {
403
- message: 'Hello from ngrok tunnel',
404
- testId: Math.random().toString(36),
405
- },
406
- };
407
-
408
- const response = await fetchWithNgrokHeaders(`${tunnelUrl}/webhook`, {
409
- method: 'POST',
410
- headers: {
411
- 'Content-Type': 'application/json',
412
- 'X-Webhook-Signature': 'test-signature',
413
- },
414
- body: JSON.stringify(webhookPayload),
415
- });
416
-
417
- const responseData = await response.json();
418
-
419
- expect(response.status).toBe(200);
420
- expect(responseData.received).toBe(true);
421
- expect(responseData.method).toBe('POST');
422
- expect(responseData.headers['x-webhook-signature']).toBe('test-signature');
423
- expect(responseData.body).toEqual(webhookPayload);
424
- }, 30000);
425
-
426
- it('should handle multiple concurrent requests', async () => {
427
- if (skipTests) {
428
- return;
429
- }
430
-
431
- const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
432
-
433
- // Send 10 concurrent requests
434
- const requests = Array.from({ length: 10 }, async (_, i) => {
435
- const response = await fetchWithNgrokHeaders(`${tunnelUrl}/echo`, {
436
- method: 'POST',
437
- headers: { 'Content-Type': 'application/json' },
438
- body: JSON.stringify({ requestId: i, timestamp: Date.now() }),
439
- });
440
- return response.json();
441
- });
442
-
443
- const responses = await Promise.all(requests);
444
-
445
- expect(responses).toHaveLength(10);
446
- responses.forEach((response, i) => {
447
- expect(response.requestId).toBe(i);
448
- expect(response.timestamp).toBeTruthy();
449
- });
450
- }, 30000);
451
- });
452
-
453
- describe('Error Handling with Real API', () => {
454
- it('should handle port already in use', async () => {
455
- if (skipTests) {
456
- return;
457
- }
458
-
459
- // Start first tunnel
460
- const url1 = await service.startTunnel(testServerPort);
461
- expect(url1).toBeTruthy();
462
-
463
- // Try to start another tunnel on same port (should replace)
464
- const url2 = await service.startTunnel(testServerPort);
465
- expect(url2).toBeTruthy();
466
- expect(url2).toBe(url1); // Should be the same tunnel
467
- }, 30000);
468
-
469
- it('should handle invalid port gracefully', async () => {
470
- if (skipTests) {
471
- return;
472
- }
473
-
474
- try {
475
- // Use port 0 which is reserved and invalid for ngrok
476
- await service.startTunnel(0);
477
- throw new Error('Should have thrown an error');
478
- } catch (error) {
479
- expect(error).toBeTruthy();
480
- expect(service.isActive()).toBe(false);
481
- }
482
- }, 30000);
483
-
484
- it('should recover from network interruption', async () => {
485
- if (skipTests) {
486
- return;
487
- }
488
-
489
- // Start tunnel
490
- const url = await service.startTunnel(testServerPort);
491
- expect(url).toBeTruthy();
492
-
493
- // Stop and wait for cleanup
494
- await service.stopTunnel();
495
- console.log('⏳ Waiting for ngrok to fully stop...');
496
- await ngrokSafeDelay(3000); // Increased delay with API check
497
-
498
- // Restart tunnel
499
- console.log('🔄 Restarting tunnel...');
500
- const newUrl = await service.startTunnel(testServerPort);
501
- expect(newUrl).toBeTruthy();
502
- expect(service.isActive()).toBe(true);
503
- }, 30000);
504
- });
505
-
506
- describe('Slack Agent Use Cases', () => {
507
- it('should simulate Slack webhook events', async () => {
508
- if (skipTests) {
509
- return;
510
- }
511
-
512
- const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
513
-
514
- // Simulate Slack event
515
- const slackEvent = {
516
- token: 'test-token',
517
- team_id: 'T1234567890',
518
- event: {
519
- type: 'message',
520
- channel: 'C1234567890',
521
- user: 'U1234567890',
522
- text: 'Hello from Slack!',
523
- ts: '1234567890.123456',
524
- },
525
- };
526
-
527
- const response = await fetchWithNgrokHeaders(`${tunnelUrl}/webhook`, {
528
- method: 'POST',
529
- headers: {
530
- 'Content-Type': 'application/json',
531
- 'X-Slack-Request-Timestamp': Date.now().toString(),
532
- 'X-Slack-Signature': 'v0=test-signature',
533
- },
534
- body: JSON.stringify(slackEvent),
535
- });
536
-
537
- expect(response.status).toBe(200);
538
- const data = await response.json();
539
- expect(data.received).toBe(true);
540
- expect(data.body.event.text).toBe('Hello from Slack!');
541
- }, 30000);
542
- });
543
- });