@elizaos/plugin-ngrok 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +325 -0
  2. package/dist/__tests__/NgrokTestSuite.d.ts +6 -0
  3. package/dist/__tests__/NgrokTestSuite.d.ts.map +1 -0
  4. package/dist/__tests__/NgrokTestSuite.js +92 -0
  5. package/dist/__tests__/NgrokTestSuite.js.map +1 -0
  6. package/dist/actions/get-tunnel-status.d.ts +4 -0
  7. package/dist/actions/get-tunnel-status.d.ts.map +1 -0
  8. package/dist/actions/get-tunnel-status.js +186 -0
  9. package/dist/actions/get-tunnel-status.js.map +1 -0
  10. package/dist/actions/start-tunnel.d.ts +4 -0
  11. package/dist/actions/start-tunnel.d.ts.map +1 -0
  12. package/dist/actions/start-tunnel.js +221 -0
  13. package/dist/actions/start-tunnel.js.map +1 -0
  14. package/dist/actions/stop-tunnel.d.ts +4 -0
  15. package/dist/actions/stop-tunnel.d.ts.map +1 -0
  16. package/dist/actions/stop-tunnel.js +174 -0
  17. package/dist/actions/stop-tunnel.js.map +1 -0
  18. package/dist/environment.d.ts +12 -0
  19. package/dist/environment.d.ts.map +1 -0
  20. package/dist/environment.js +68 -0
  21. package/dist/environment.js.map +1 -0
  22. package/dist/index.d.ts +13 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +29 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/services/NgrokService.d.ts +30 -0
  27. package/dist/services/NgrokService.d.ts.map +1 -0
  28. package/dist/services/NgrokService.js +333 -0
  29. package/dist/services/NgrokService.js.map +1 -0
  30. package/package.json +63 -0
  31. package/src/__tests__/NgrokTestSuite.ts +110 -0
  32. package/src/__tests__/debug-mock.test.ts +15 -0
  33. package/src/__tests__/e2e/real-ngrok.test.ts +543 -0
  34. package/src/__tests__/integration/webhook-scenarios.test.ts +463 -0
  35. package/src/__tests__/mocks/NgrokServiceMock.ts +76 -0
  36. package/src/__tests__/ngrok-integration.test.ts +521 -0
  37. package/src/__tests__/test-config.ts +83 -0
  38. package/src/__tests__/test-helpers.ts +43 -0
  39. package/src/__tests__/test-setup.ts +174 -0
  40. package/src/__tests__/test-utils.ts +155 -0
  41. package/src/__tests__/unit/actions.test.ts +402 -0
  42. package/src/__tests__/unit/environment.test.ts +352 -0
  43. package/src/actions/get-tunnel-status.ts +218 -0
  44. package/src/actions/start-tunnel.ts +255 -0
  45. package/src/actions/stop-tunnel.ts +203 -0
  46. package/src/environment.ts +75 -0
  47. package/src/index.ts +33 -0
  48. package/src/services/NgrokService.ts +401 -0
@@ -0,0 +1,521 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
2
+ import { spawn } from 'node:child_process';
3
+ import * as http from 'node:http';
4
+ import * as https from 'node:https';
5
+ import * as path from 'node:path';
6
+ import type { IAgentRuntime } from '@elizaos/core';
7
+ import * as dotenv from 'dotenv';
8
+ import { NgrokService } from '../services/NgrokService';
9
+ import { createMockRuntime } from './test-utils';
10
+
11
+ // Load environment variables - use correct path relative to package location
12
+ dotenv.config({ path: path.resolve(__dirname, '../../../../.env') });
13
+
14
+ // Helper to check if we have ngrok credentials
15
+ const hasNgrokCredentials = () => {
16
+ return !!process.env.NGROK_AUTH_TOKEN;
17
+ };
18
+
19
+ // Helper to check if ngrok is installed
20
+ const isNgrokInstalled = async (): Promise<boolean> => {
21
+ return new Promise((resolve) => {
22
+ const checkProcess = spawn('which', ['ngrok']);
23
+ checkProcess.on('exit', (code) => {
24
+ resolve(code === 0);
25
+ });
26
+ checkProcess.on('error', () => {
27
+ resolve(false);
28
+ });
29
+ });
30
+ };
31
+
32
+ describe('Ngrok Integration Tests', () => {
33
+ let service: NgrokService;
34
+ let runtime: IAgentRuntime;
35
+ let testServer: http.Server;
36
+ let testServerPort: number;
37
+ let skipTests = false;
38
+
39
+ beforeAll(async () => {
40
+ // Check if we should skip tests
41
+ const hasAuthToken = Boolean(process.env.NGROK_AUTH_TOKEN);
42
+ const skipEnvVar = process.env.SKIP_NGROK_TESTS === 'true';
43
+
44
+ console.log('Ngrok integration test environment check:');
45
+ console.log('- NGROK_AUTH_TOKEN:', hasAuthToken ? 'Set' : 'Not set');
46
+ console.log('- NGROK_DOMAIN:', process.env.NGROK_DOMAIN || 'Not set');
47
+ console.log('- SKIP_NGROK_TESTS:', skipEnvVar);
48
+
49
+ if (!hasAuthToken || skipEnvVar) {
50
+ skipTests = true;
51
+ console.log('⚠️ Skipping ngrok integration tests');
52
+ return;
53
+ }
54
+
55
+ // Check prerequisites
56
+ const ngrokInstalled = await isNgrokInstalled();
57
+ if (!ngrokInstalled) {
58
+ console.log('⚠️ Skipping integration tests - ngrok is not installed');
59
+ console.log(' Please install ngrok: brew install ngrok');
60
+ skipTests = true;
61
+ return;
62
+ }
63
+
64
+ if (!hasNgrokCredentials()) {
65
+ console.log('⚠️ Running integration tests without auth token');
66
+ console.log(' Some features may be limited. Set NGROK_AUTH_TOKEN for full functionality');
67
+ }
68
+
69
+ // Create mock runtime using core test-utils
70
+ runtime = createMockRuntime();
71
+
72
+ // Create a test HTTP server
73
+ testServer = http.createServer((req, res) => {
74
+ const body: Buffer[] = [];
75
+ req.on('data', (chunk) => body.push(chunk));
76
+ req.on('end', () => {
77
+ const data = Buffer.concat(body).toString();
78
+
79
+ // Echo back request details
80
+ res.writeHead(200, { 'Content-Type': 'application/json' });
81
+ res.end(
82
+ JSON.stringify({
83
+ method: req.method,
84
+ url: req.url,
85
+ headers: req.headers,
86
+ body: data,
87
+ timestamp: new Date().toISOString(),
88
+ })
89
+ );
90
+ });
91
+ });
92
+
93
+ // Start test server on random port
94
+ await new Promise<void>((resolve) => {
95
+ testServer.listen(0, () => {
96
+ const address = testServer.address();
97
+ if (address && typeof address === 'object') {
98
+ testServerPort = address.port;
99
+ }
100
+ console.log(`✅ Test server started on port ${testServerPort}`);
101
+ resolve();
102
+ });
103
+ });
104
+ });
105
+
106
+ afterAll(async () => {
107
+ // Clean up test server
108
+ if (testServer) {
109
+ await new Promise<void>((resolve) => {
110
+ testServer.close(() => {
111
+ console.log('✅ Test server stopped');
112
+ resolve();
113
+ });
114
+ });
115
+ }
116
+ });
117
+
118
+ beforeEach(() => {
119
+ // Create fresh service instance for each test
120
+ if (skipTests) {
121
+ return;
122
+ }
123
+ console.log('Runtime in beforeEach:', !!runtime);
124
+ console.log('Runtime getSetting type:', typeof runtime?.getSetting);
125
+ service = new NgrokService(runtime);
126
+ });
127
+
128
+ afterEach(async () => {
129
+ if (service?.isActive()) {
130
+ await service.stopTunnel();
131
+ }
132
+ });
133
+
134
+ async function shouldSkipNgrokTest(): Promise<boolean> {
135
+ if (skipTests) {
136
+ console.log('Test skipped - ngrok credentials unavailable or tests disabled');
137
+ return true;
138
+ }
139
+ if (!(await isNgrokInstalled())) {
140
+ console.log('Skipping test: ngrok not installed');
141
+ return true;
142
+ }
143
+ return false;
144
+ }
145
+
146
+ describe('Basic Tunnel Operations', () => {
147
+ it('should start and stop a tunnel successfully', async () => {
148
+ if (await shouldSkipNgrokTest()) {
149
+ return;
150
+ }
151
+
152
+ // Start tunnel
153
+ const url = await service.startTunnel(testServerPort);
154
+
155
+ expect(url).toBeDefined();
156
+ expect(url).toMatch(/^https:\/\/.*\.ngrok.*\.(io|app)$/);
157
+ console.log(`✅ Tunnel started: ${url}`);
158
+
159
+ // Verify tunnel is active
160
+ expect(service.isActive()).toBe(true);
161
+ expect(service.getUrl()).toBe(url as string | null);
162
+
163
+ const status = service.getStatus();
164
+ expect(status.active).toBe(true);
165
+ expect(status.port).toBe(testServerPort);
166
+ expect(status.url).toBe(url as string | null);
167
+ expect(status.startedAt).toBeInstanceOf(Date);
168
+
169
+ // Stop tunnel
170
+ await service.stopTunnel();
171
+
172
+ expect(service.isActive()).toBe(false);
173
+ expect(service.getUrl()).toBe(null);
174
+ console.log('✅ Tunnel stopped');
175
+ }, 30000); // 30 second timeout for tunnel operations
176
+
177
+ it('should handle multiple start/stop cycles', async () => {
178
+ if (await shouldSkipNgrokTest()) {
179
+ return;
180
+ }
181
+
182
+ for (let i = 0; i < 3; i++) {
183
+ console.log(`\n🔄 Cycle ${i + 1}/3`);
184
+
185
+ const url = await service.startTunnel(testServerPort);
186
+ expect(url).toBeDefined();
187
+ expect(service.isActive()).toBe(true);
188
+
189
+ await service.stopTunnel();
190
+ expect(service.isActive()).toBe(false);
191
+ }
192
+ }, 60000); // 60 second timeout
193
+ });
194
+
195
+ describe('Webhook Testing', () => {
196
+ it('should receive webhook calls through ngrok tunnel', async () => {
197
+ if (await shouldSkipNgrokTest()) {
198
+ return;
199
+ }
200
+
201
+ // Start tunnel
202
+ const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
203
+ console.log(`\n🌐 Testing webhook at: ${tunnelUrl}`);
204
+
205
+ // Test webhook with different HTTP methods
206
+ const testCases = [
207
+ { method: 'GET', path: '/webhook/test' },
208
+ { method: 'POST', path: '/webhook/event', body: { event: 'test', data: { foo: 'bar' } } },
209
+ { method: 'PUT', path: '/webhook/update', body: { id: 123, status: 'active' } },
210
+ { method: 'DELETE', path: '/webhook/delete/123' },
211
+ ];
212
+
213
+ for (const testCase of testCases) {
214
+ console.log(`\n📤 Testing ${testCase.method} ${testCase.path}`);
215
+
216
+ const response = await makeHttpRequest(
217
+ tunnelUrl + testCase.path,
218
+ testCase.method,
219
+ testCase.body
220
+ );
221
+
222
+ expect(response.method).toBe(testCase.method);
223
+ expect(response.url).toBe(testCase.path);
224
+
225
+ if (testCase.body) {
226
+ expect(JSON.parse(response.body)).toEqual(testCase.body);
227
+ }
228
+
229
+ console.log(`✅ ${testCase.method} request successful`);
230
+ }
231
+
232
+ // Clean up
233
+ await service.stopTunnel();
234
+ }, 45000);
235
+
236
+ it('should handle concurrent webhook requests', async () => {
237
+ if (await shouldSkipNgrokTest()) {
238
+ return;
239
+ }
240
+
241
+ const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
242
+ console.log(`\n🌐 Testing concurrent webhooks at: ${tunnelUrl}`);
243
+
244
+ // Send multiple concurrent requests
245
+ const requests = Array.from({ length: 10 }, (_, i) =>
246
+ makeHttpRequest(`${tunnelUrl}/concurrent/${i}`, 'POST', {
247
+ requestId: i,
248
+ timestamp: Date.now(),
249
+ })
250
+ );
251
+
252
+ const responses = await Promise.all(requests);
253
+
254
+ // Verify all requests were received
255
+ expect(responses).toHaveLength(10);
256
+ responses.forEach((response, i) => {
257
+ expect(response.method).toBe('POST');
258
+ expect(response.url).toBe(`/concurrent/${i}`);
259
+ const body = JSON.parse(response.body);
260
+ expect(body.requestId).toBe(i);
261
+ });
262
+
263
+ console.log('✅ All concurrent requests processed successfully');
264
+
265
+ await service.stopTunnel();
266
+ }, 30000);
267
+ });
268
+
269
+ describe('Error Handling', () => {
270
+ it('should handle tunnel start failure gracefully', async () => {
271
+ if (await shouldSkipNgrokTest()) {
272
+ return;
273
+ }
274
+
275
+ // Test with port number out of valid range (> 65535)
276
+ // Note: NgrokService doesn't validate port numbers, it lets ngrok handle it
277
+ const result = await service.startTunnel(70000);
278
+ // The service will attempt to start with the invalid port
279
+ // but ngrok itself may handle it differently
280
+ expect(result).toBeDefined(); // Service returns a URL even if port is invalid
281
+
282
+ await service.stopTunnel();
283
+ });
284
+
285
+ it('should handle network interruptions', async () => {
286
+ if (await shouldSkipNgrokTest()) {
287
+ return;
288
+ }
289
+
290
+ const tunnelUrl = await service.startTunnel(testServerPort);
291
+
292
+ // Simulate network request with timeout
293
+ try {
294
+ await makeHttpRequest(`${tunnelUrl}/test`, 'GET', null, 5000);
295
+ } catch (_error) {
296
+ // Network errors are expected in some cases
297
+ console.log('⚠️ Network request failed (expected in some test scenarios)');
298
+ }
299
+
300
+ // Tunnel should still be active
301
+ expect(service.isActive()).toBe(true);
302
+
303
+ await service.stopTunnel();
304
+ }, 30000);
305
+ });
306
+
307
+ describe('Real-world Scenarios', () => {
308
+ it('should handle a simulated GitHub webhook', async () => {
309
+ if (await shouldSkipNgrokTest()) {
310
+ return;
311
+ }
312
+
313
+ const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
314
+ console.log(`\n🐙 Simulating GitHub webhook at: ${tunnelUrl}/github/webhook`);
315
+
316
+ // Simulate GitHub push event
317
+ const githubPayload = {
318
+ ref: 'refs/heads/main',
319
+ repository: {
320
+ name: 'test-repo',
321
+ full_name: 'user/test-repo',
322
+ },
323
+ pusher: {
324
+ name: 'test-user',
325
+ email: 'test@example.com',
326
+ },
327
+ commits: [
328
+ {
329
+ id: 'abc123',
330
+ message: 'Test commit',
331
+ author: {
332
+ name: 'Test User',
333
+ email: 'test@example.com',
334
+ },
335
+ },
336
+ ],
337
+ };
338
+
339
+ const response = await makeHttpRequest(
340
+ `${tunnelUrl}/github/webhook`,
341
+ 'POST',
342
+ githubPayload,
343
+ 10000,
344
+ {
345
+ 'X-GitHub-Event': 'push',
346
+ 'X-GitHub-Delivery': 'test-delivery-id',
347
+ 'Content-Type': 'application/json',
348
+ }
349
+ );
350
+
351
+ expect(response.method).toBe('POST');
352
+ expect(response.headers['x-github-event']).toBe('push');
353
+
354
+ const receivedPayload = JSON.parse(response.body);
355
+ expect(receivedPayload.repository.name).toBe('test-repo');
356
+ expect(receivedPayload.commits).toHaveLength(1);
357
+
358
+ console.log('✅ GitHub webhook simulation successful');
359
+
360
+ await service.stopTunnel();
361
+ }, 30000);
362
+
363
+ it('should handle a simulated Stripe webhook', async () => {
364
+ if (await shouldSkipNgrokTest()) {
365
+ return;
366
+ }
367
+
368
+ const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
369
+ console.log(`\n💳 Simulating Stripe webhook at: ${tunnelUrl}/stripe/webhook`);
370
+
371
+ // Simulate Stripe payment event
372
+ const stripePayload = {
373
+ id: 'evt_test_123',
374
+ object: 'event',
375
+ type: 'payment_intent.succeeded',
376
+ data: {
377
+ object: {
378
+ id: 'pi_test_123',
379
+ amount: 2000,
380
+ currency: 'usd',
381
+ status: 'succeeded',
382
+ customer: 'cus_test_123',
383
+ },
384
+ },
385
+ };
386
+
387
+ const response = await makeHttpRequest(
388
+ `${tunnelUrl}/stripe/webhook`,
389
+ 'POST',
390
+ stripePayload,
391
+ 10000,
392
+ {
393
+ 'Stripe-Signature': 'test-signature',
394
+ 'Content-Type': 'application/json',
395
+ }
396
+ );
397
+
398
+ expect(response.method).toBe('POST');
399
+ expect(response.headers['stripe-signature']).toBe('test-signature');
400
+
401
+ const receivedPayload = JSON.parse(response.body);
402
+ expect(receivedPayload.type).toBe('payment_intent.succeeded');
403
+ expect(receivedPayload.data.object.amount).toBe(2000);
404
+
405
+ console.log('✅ Stripe webhook simulation successful');
406
+
407
+ await service.stopTunnel();
408
+ }, 30000);
409
+ });
410
+
411
+ describe('Performance Testing', () => {
412
+ it('should handle sustained traffic', async () => {
413
+ if (await shouldSkipNgrokTest()) {
414
+ return;
415
+ }
416
+
417
+ const tunnelUrl = (await service.startTunnel(testServerPort)) as string;
418
+ console.log(`\n📊 Performance testing at: ${tunnelUrl}`);
419
+
420
+ const startTime = Date.now();
421
+ const duration = 10000; // 10 seconds
422
+ let requestCount = 0;
423
+ let errorCount = 0;
424
+
425
+ // Send requests continuously for duration
426
+ while (Date.now() - startTime < duration) {
427
+ try {
428
+ await makeHttpRequest(
429
+ `${tunnelUrl}/perf/${requestCount}`,
430
+ 'GET',
431
+ null,
432
+ 1000 // 1 second timeout per request
433
+ );
434
+ requestCount++;
435
+ } catch (_error) {
436
+ errorCount++;
437
+ }
438
+
439
+ // Small delay between requests
440
+ await new Promise((resolve) => setTimeout(resolve, 100));
441
+ }
442
+
443
+ const elapsed = (Date.now() - startTime) / 1000;
444
+ const rps = requestCount / elapsed;
445
+
446
+ console.log('\n📈 Performance Results:');
447
+ console.log(` - Duration: ${elapsed.toFixed(1)}s`);
448
+ console.log(` - Requests: ${requestCount}`);
449
+ console.log(` - Errors: ${errorCount}`);
450
+ console.log(` - RPS: ${rps.toFixed(1)}`);
451
+
452
+ expect(requestCount).toBeGreaterThan(0);
453
+ expect(errorCount / requestCount).toBeLessThan(0.1); // Less than 10% error rate
454
+
455
+ await service.stopTunnel();
456
+ }, 30000);
457
+ });
458
+ });
459
+
460
+ // Helper function to make HTTP requests
461
+ async function makeHttpRequest(
462
+ url: string,
463
+ method: string,
464
+ body: unknown,
465
+ timeout: number = 10000,
466
+ headers: Record<string, string> = {}
467
+ ): Promise<unknown> {
468
+ return new Promise((resolve, reject) => {
469
+ const urlObj = new URL(url);
470
+ const requestHeaders: Record<string, string> = {
471
+ ...headers,
472
+ 'User-Agent': 'ElizaOS-Ngrok-Test',
473
+ 'ngrok-skip-browser-warning': 'true',
474
+ };
475
+ const options = {
476
+ hostname: urlObj.hostname,
477
+ port: urlObj.port || 443,
478
+ path: urlObj.pathname + urlObj.search,
479
+ method,
480
+ headers: requestHeaders,
481
+ timeout,
482
+ };
483
+
484
+ if (body && method !== 'GET') {
485
+ const bodyStr = JSON.stringify(body);
486
+ requestHeaders['Content-Type'] = options.headers['Content-Type'] || 'application/json';
487
+ requestHeaders['Content-Length'] = Buffer.byteLength(bodyStr).toString();
488
+ }
489
+
490
+ const req = https.request(options, (res) => {
491
+ const chunks: Buffer[] = [];
492
+
493
+ res.on('data', (chunk) => chunks.push(chunk));
494
+ res.on('end', () => {
495
+ try {
496
+ const responseBody = Buffer.concat(chunks).toString();
497
+ const response = JSON.parse(responseBody);
498
+ resolve(response);
499
+ } catch (error) {
500
+ reject(
501
+ new Error(
502
+ `Failed to parse response: ${error instanceof Error ? error.message : String(error)}`
503
+ )
504
+ );
505
+ }
506
+ });
507
+ });
508
+
509
+ req.on('error', reject);
510
+ req.on('timeout', () => {
511
+ req.destroy();
512
+ reject(new Error('Request timeout'));
513
+ });
514
+
515
+ if (body && method !== 'GET') {
516
+ req.write(JSON.stringify(body));
517
+ }
518
+
519
+ req.end();
520
+ });
521
+ }
@@ -0,0 +1,83 @@
1
+ export const testConfig = {
2
+ // Test execution configuration
3
+ execution: {
4
+ // Add delays between test suites to avoid rate limiting
5
+ suitesDelay: 3000, // 3 seconds between test suites
6
+ testsDelay: 1000, // 1 second between individual tests
7
+
8
+ // Maximum retries for flaky tests
9
+ maxRetries: 2,
10
+
11
+ // Timeout configurations
12
+ defaultTimeout: 30000,
13
+ integrationTimeout: 60000,
14
+ e2eTimeout: 90000,
15
+ },
16
+
17
+ // Ngrok specific configuration for tests
18
+ ngrok: {
19
+ // Don't use subdomains in tests (requires paid account)
20
+ useRandomSubdomains: false,
21
+
22
+ // Wait time after stopping tunnel before starting a new one
23
+ stopWaitTime: 2000,
24
+
25
+ // Rate limiting configuration
26
+ minIntervalBetweenStarts: 3000,
27
+
28
+ // Domain conflict retry configuration
29
+ domainConflictRetries: 3,
30
+ domainConflictBackoff: 2000,
31
+ },
32
+
33
+ // Test categorization for prioritized execution
34
+ testPriority: {
35
+ // Run these test suites first (critical path)
36
+ high: ['unit/types.test.ts', 'unit/environment.test.ts', 'unit/plugin.test.ts'],
37
+
38
+ // Run these after high priority tests
39
+ medium: ['unit/actions.test.ts', 'mocks/NgrokServiceMock.ts'],
40
+
41
+ // Run these last (resource intensive)
42
+ low: [
43
+ 'integration/webhook-scenarios.test.ts',
44
+ 'e2e/real-ngrok.test.ts',
45
+ 'ngrok-integration.test.ts',
46
+ ],
47
+ },
48
+ };
49
+
50
+ // Helper to add delays in tests
51
+ export async function testDelay(ms: number = testConfig.execution.testsDelay): Promise<void> {
52
+ return new Promise((resolve) => setTimeout(resolve, ms));
53
+ }
54
+
55
+ // Helper to check if we should skip ngrok tests
56
+ export function shouldSkipNgrokTests(): boolean {
57
+ const hasAuthToken = Boolean(process.env.NGROK_AUTH_TOKEN);
58
+ const skipTests = process.env.SKIP_NGROK_TESTS === 'true';
59
+
60
+ if (!hasAuthToken) {
61
+ console.log('⚠️ Skipping ngrok tests - NGROK_AUTH_TOKEN not found');
62
+ }
63
+
64
+ if (skipTests) {
65
+ console.log('⚠️ Skipping ngrok tests - SKIP_NGROK_TESTS is set');
66
+ }
67
+
68
+ return !hasAuthToken || skipTests;
69
+ }
70
+
71
+ // Helper to get test-specific ngrok configuration
72
+ export function getTestNgrokConfig() {
73
+ return {
74
+ // Don't use subdomains in tests (free account limitation)
75
+ NGROK_USE_RANDOM_SUBDOMAIN: 'false',
76
+
77
+ // Use the auth token from environment
78
+ NGROK_AUTH_TOKEN: process.env.NGROK_AUTH_TOKEN,
79
+
80
+ // Don't use fixed domain in tests - let ngrok generate random URLs
81
+ NGROK_DOMAIN: undefined,
82
+ };
83
+ }
@@ -0,0 +1,43 @@
1
+ import * as http from 'node:http';
2
+
3
+ // Helper to wait for ngrok API to be ready
4
+ export async function waitForNgrokAPI(maxAttempts = 10, delayMs = 500): Promise<boolean> {
5
+ for (let i = 0; i < maxAttempts; i++) {
6
+ try {
7
+ const isReady = await checkNgrokAPIReady();
8
+ if (isReady) {
9
+ return true;
10
+ }
11
+ } catch {
12
+ // API not ready yet
13
+ }
14
+
15
+ if (i < maxAttempts - 1) {
16
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
17
+ }
18
+ }
19
+ return false;
20
+ }
21
+
22
+ // Check if ngrok API is responding
23
+ function checkNgrokAPIReady(): Promise<boolean> {
24
+ return new Promise((resolve) => {
25
+ const req = http.get('http://localhost:4040/api/tunnels', (res) => {
26
+ res.on('data', () => {}); // Consume data
27
+ res.on('end', () => resolve(res.statusCode === 200));
28
+ });
29
+
30
+ req.on('error', () => resolve(false));
31
+ req.setTimeout(1000, () => {
32
+ req.destroy();
33
+ resolve(false);
34
+ });
35
+ });
36
+ }
37
+
38
+ // Enhanced delay with ngrok API check
39
+ export async function ngrokSafeDelay(ms: number): Promise<void> {
40
+ await new Promise((resolve) => setTimeout(resolve, ms));
41
+ // Also wait for ngrok API to be ready
42
+ await waitForNgrokAPI();
43
+ }