@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,333 @@
1
+ import { elizaLogger, Service } from '@elizaos/core';
2
+ /** Coerce runtime.getSetting() (string | number | boolean | null) to string. */
3
+ function settingString(value) {
4
+ if (value === null || value === undefined)
5
+ return undefined;
6
+ return String(value);
7
+ }
8
+ function errorMessage(error) {
9
+ return error instanceof Error ? error.message : String(error);
10
+ }
11
+ import { spawn } from 'node:child_process';
12
+ import * as http from 'node:http';
13
+ import { validateNgrokConfig } from '../environment';
14
+ export class NgrokService extends Service {
15
+ static serviceType = 'tunnel';
16
+ capabilityDescription = 'Provides secure tunnel functionality using ngrok for exposing local services to the internet';
17
+ static MIN_TUNNEL_INTERVAL = 2000; // 2 seconds minimum between tunnel starts
18
+ ngrokProcess = null;
19
+ tunnelUrl = null;
20
+ tunnelPort = null;
21
+ startedAt = null;
22
+ lastStartTime = 0;
23
+ tunnelConfig;
24
+ constructor(runtime) {
25
+ super(runtime);
26
+ this.tunnelConfig = {
27
+ provider: 'ngrok',
28
+ authToken: settingString(runtime?.getSetting('NGROK_AUTH_TOKEN')) ?? process.env.NGROK_AUTH_TOKEN,
29
+ };
30
+ }
31
+ async initialize() {
32
+ elizaLogger.info('🚇 Initializing Ngrok tunnel service...');
33
+ const isInstalled = await this.checkNgrokInstalled();
34
+ if (!isInstalled) {
35
+ throw new Error('ngrok is not installed. Please install it from https://ngrok.com/download or run: brew install ngrok');
36
+ }
37
+ const authToken = this.tunnelConfig.authToken ??
38
+ settingString(this.runtime.getSetting('NGROK_AUTH_TOKEN')) ??
39
+ process.env.NGROK_AUTH_TOKEN;
40
+ if (authToken) {
41
+ await this.setAuthToken(authToken);
42
+ elizaLogger.info('Setting ngrok auth token');
43
+ }
44
+ else {
45
+ elizaLogger.warn('No ngrok auth token found - running in limited mode');
46
+ }
47
+ }
48
+ static async start(runtime) {
49
+ const service = new NgrokService(runtime);
50
+ await service.start();
51
+ return service;
52
+ }
53
+ // Base Service lifecycle methods
54
+ async start() {
55
+ elizaLogger.info('NgrokService started');
56
+ }
57
+ async stop() {
58
+ await this.stopTunnel();
59
+ }
60
+ // ITunnelService implementation
61
+ async startTunnel(port) {
62
+ if (this.isActive()) {
63
+ elizaLogger.warn('Ngrok tunnel is already running');
64
+ return this.tunnelUrl || undefined;
65
+ }
66
+ if (!port) {
67
+ elizaLogger.warn('NgrokService.start() called without a port. The service will be active but no tunnel will be started.');
68
+ return;
69
+ }
70
+ // Validate environment
71
+ try {
72
+ await validateNgrokConfig(this.runtime);
73
+ }
74
+ catch (error) {
75
+ throw new Error(`Ngrok environment validation failed: ${errorMessage(error)}`);
76
+ }
77
+ // Enforce rate limiting
78
+ const now = Date.now();
79
+ if (this.lastStartTime && now - this.lastStartTime < NgrokService.MIN_TUNNEL_INTERVAL) {
80
+ const waitTime = NgrokService.MIN_TUNNEL_INTERVAL - (now - this.lastStartTime);
81
+ elizaLogger.warn(`Rate limiting: waiting ${waitTime}ms before starting tunnel`);
82
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
83
+ }
84
+ this.lastStartTime = Date.now();
85
+ elizaLogger.info(`🚀 Starting ngrok tunnel on port ${port}...`);
86
+ try {
87
+ const tunnelUrl = await this.attemptStartTunnel(port);
88
+ this.tunnelUrl = tunnelUrl;
89
+ this.tunnelPort = port;
90
+ this.startedAt = new Date();
91
+ elizaLogger.success(`✅ Ngrok tunnel started: ${tunnelUrl}`);
92
+ return tunnelUrl;
93
+ }
94
+ catch (error) {
95
+ elizaLogger.error('Failed to start ngrok tunnel:', errorMessage(error));
96
+ throw error;
97
+ }
98
+ }
99
+ async attemptStartTunnel(port) {
100
+ let attempts = 0;
101
+ const maxAttempts = 3;
102
+ const baseDelay = 2000;
103
+ while (attempts < maxAttempts) {
104
+ try {
105
+ return await this.startTunnelInternal(port);
106
+ }
107
+ catch (error) {
108
+ attempts++;
109
+ if (errorMessage(error).includes('domain might already be in use')) {
110
+ if (attempts < maxAttempts) {
111
+ elizaLogger.warn(`Domain conflict detected, retrying in ${baseDelay * attempts}ms (attempt ${attempts}/${maxAttempts})`);
112
+ await new Promise((resolve) => setTimeout(resolve, baseDelay * attempts));
113
+ // Try to stop any existing process just in case
114
+ if (this.ngrokProcess) {
115
+ this.ngrokProcess.kill();
116
+ this.ngrokProcess = null;
117
+ await new Promise((resolve) => setTimeout(resolve, 1000));
118
+ }
119
+ continue;
120
+ }
121
+ }
122
+ throw error;
123
+ }
124
+ }
125
+ throw new Error(`Failed to start tunnel after ${maxAttempts} attempts`);
126
+ }
127
+ async startTunnelInternal(port) {
128
+ return new Promise((resolve, reject) => {
129
+ const args = ['http', port.toString()];
130
+ if (this.tunnelConfig.region) {
131
+ args.push('--region', this.tunnelConfig.region);
132
+ }
133
+ // Check for domain configuration
134
+ const domain = settingString(this.runtime.getSetting('NGROK_DOMAIN')) ?? process.env.NGROK_DOMAIN;
135
+ const useRandomSubdomain = settingString(this.runtime.getSetting('NGROK_USE_RANDOM_SUBDOMAIN')) === 'true';
136
+ if (domain && !useRandomSubdomain) {
137
+ args.push('--domain', domain);
138
+ elizaLogger.info(`Using ngrok domain: ${domain}`);
139
+ }
140
+ else if (this.tunnelConfig.subdomain && !useRandomSubdomain) {
141
+ // Only use subdomain if explicitly configured and not in test mode
142
+ // Note: Subdomains require a paid ngrok account
143
+ args.push('--subdomain', this.tunnelConfig.subdomain);
144
+ elizaLogger.info(`Using configured subdomain: ${this.tunnelConfig.subdomain}`);
145
+ }
146
+ // For free accounts or when random subdomain is requested,
147
+ // don't specify any domain/subdomain - let ngrok generate a random URL
148
+ this.ngrokProcess = spawn('ngrok', args, { stdio: ['ignore', 'pipe', 'pipe'] });
149
+ let errorOccurred = false;
150
+ this.ngrokProcess.on('error', (error) => {
151
+ errorOccurred = true;
152
+ elizaLogger.error(`Failed to start ngrok: ${error.message}`);
153
+ reject(new Error(`Failed to start ngrok: ${error.message}`));
154
+ });
155
+ this.ngrokProcess.stderr?.on('data', (data) => {
156
+ const message = data.toString();
157
+ elizaLogger.error('Ngrok error:', message);
158
+ if (!errorOccurred) {
159
+ errorOccurred = true;
160
+ // Kill the process to clean up
161
+ if (this.ngrokProcess && !this.ngrokProcess.killed) {
162
+ this.ngrokProcess.kill();
163
+ }
164
+ // Handle specific error cases
165
+ if (message.includes('invalid port')) {
166
+ reject(new Error('Invalid port specified'));
167
+ }
168
+ else if (message.includes('address already in use')) {
169
+ reject(new Error('Port is already in use'));
170
+ }
171
+ else if (message.includes('ERR_NGROK_15002') || message.includes('Pay-as-you-go')) {
172
+ // Pay-as-you-go account requires domain
173
+ if (!domain) {
174
+ reject(new Error('Pay-as-you-go ngrok account requires NGROK_DOMAIN to be set. Please set NGROK_DOMAIN=your-domain.ngrok-free.app in your .env file'));
175
+ }
176
+ else {
177
+ reject(new Error('Failed to start tunnel with pay-as-you-go account. Ensure your domain is registered at https://dashboard.ngrok.com/domains'));
178
+ }
179
+ }
180
+ else if (message.includes('failed to start tunnel') ||
181
+ message.includes('is already bound to another tunnel') ||
182
+ message.includes('tunnel session failed')) {
183
+ // This might happen if the domain is already in use
184
+ reject(new Error('Failed to start tunnel - domain might already be in use'));
185
+ }
186
+ else {
187
+ reject(new Error(`Ngrok error: ${message}`));
188
+ }
189
+ }
190
+ });
191
+ // Give ngrok more time to start and handle multiple retry attempts
192
+ let retryCount = 0;
193
+ const maxRetries = 3;
194
+ const retryDelay = 2000;
195
+ const tryFetchUrl = async () => {
196
+ if (errorOccurred) {
197
+ return; // Don't try to fetch URL if we already have an error
198
+ }
199
+ try {
200
+ const url = await this.fetchTunnelUrl();
201
+ if (url) {
202
+ this.tunnelUrl = url;
203
+ this.tunnelPort = port;
204
+ this.startedAt = new Date();
205
+ elizaLogger.success(`✅ Ngrok tunnel started: ${url}`);
206
+ resolve(url);
207
+ }
208
+ else if (retryCount < maxRetries) {
209
+ retryCount++;
210
+ elizaLogger.warn(`Retrying to fetch tunnel URL (attempt ${retryCount}/${maxRetries})...`);
211
+ setTimeout(tryFetchUrl, retryDelay);
212
+ }
213
+ else {
214
+ reject(new Error('Failed to get tunnel URL from ngrok after multiple attempts'));
215
+ }
216
+ }
217
+ catch (error) {
218
+ if (retryCount < maxRetries && !errorOccurred) {
219
+ retryCount++;
220
+ elizaLogger.warn(`Retrying to fetch tunnel URL (attempt ${retryCount}/${maxRetries})...`);
221
+ setTimeout(tryFetchUrl, retryDelay);
222
+ }
223
+ else {
224
+ reject(error);
225
+ }
226
+ }
227
+ };
228
+ setTimeout(tryFetchUrl, 2000);
229
+ });
230
+ }
231
+ async stopTunnel() {
232
+ if (!this.ngrokProcess) {
233
+ elizaLogger.warn('Ngrok tunnel is not running');
234
+ return;
235
+ }
236
+ elizaLogger.info('🛑 Stopping ngrok tunnel...');
237
+ return new Promise((resolve) => {
238
+ if (this.ngrokProcess) {
239
+ this.ngrokProcess.on('exit', () => {
240
+ this.cleanup();
241
+ elizaLogger.info('✅ Ngrok tunnel stopped');
242
+ resolve();
243
+ });
244
+ this.ngrokProcess.kill();
245
+ setTimeout(() => {
246
+ if (this.ngrokProcess && !this.ngrokProcess.killed) {
247
+ this.ngrokProcess.kill('SIGKILL');
248
+ }
249
+ this.cleanup();
250
+ resolve();
251
+ }, 5000);
252
+ }
253
+ else {
254
+ resolve();
255
+ }
256
+ });
257
+ }
258
+ getUrl() {
259
+ return this.tunnelUrl;
260
+ }
261
+ isActive() {
262
+ return this.ngrokProcess !== null && !this.ngrokProcess.killed && this.tunnelUrl !== null;
263
+ }
264
+ getStatus() {
265
+ return {
266
+ active: this.isActive(),
267
+ url: this.tunnelUrl,
268
+ port: this.tunnelPort,
269
+ startedAt: this.startedAt,
270
+ provider: 'ngrok',
271
+ };
272
+ }
273
+ cleanup() {
274
+ this.ngrokProcess = null;
275
+ this.tunnelUrl = null;
276
+ this.tunnelPort = null;
277
+ this.startedAt = null;
278
+ }
279
+ async checkNgrokInstalled() {
280
+ return new Promise((resolve) => {
281
+ const proc = spawn('which', ['ngrok']);
282
+ proc.on('exit', (code) => resolve(code === 0));
283
+ proc.on('error', () => resolve(false));
284
+ });
285
+ }
286
+ async setAuthToken(token) {
287
+ return new Promise((resolve, reject) => {
288
+ const proc = spawn('ngrok', ['config', 'add-authtoken', token]);
289
+ proc.on('exit', (code) => {
290
+ if (code === 0) {
291
+ elizaLogger.info('✅ Ngrok auth token configured');
292
+ resolve();
293
+ }
294
+ else {
295
+ reject(new Error('Failed to set ngrok auth token'));
296
+ }
297
+ });
298
+ proc.on('error', reject);
299
+ });
300
+ }
301
+ async fetchTunnelUrl() {
302
+ return new Promise((resolve) => {
303
+ http
304
+ .get('http://localhost:4040/api/tunnels', (res) => {
305
+ let data = '';
306
+ res.on('data', (chunk) => (data += chunk));
307
+ res.on('end', () => {
308
+ try {
309
+ const tunnels = JSON.parse(data);
310
+ const httpsTunnel = tunnels.tunnels?.find((t) => t.proto === 'https');
311
+ if (httpsTunnel?.public_url) {
312
+ resolve(httpsTunnel.public_url);
313
+ }
314
+ else {
315
+ elizaLogger.warn('No HTTPS tunnel found in ngrok response');
316
+ resolve(null);
317
+ }
318
+ }
319
+ catch (error) {
320
+ const msg = error instanceof Error ? error.message : String(error);
321
+ elizaLogger.error(`Failed to parse ngrok API response: ${msg}`);
322
+ resolve(null);
323
+ }
324
+ });
325
+ })
326
+ .on('error', (error) => {
327
+ elizaLogger.error(`Failed to connect to ngrok API: ${error.message}`);
328
+ resolve(null);
329
+ });
330
+ });
331
+ }
332
+ }
333
+ //# sourceMappingURL=NgrokService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NgrokService.js","sourceRoot":"","sources":["../../src/services/NgrokService.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@elizaos/plugin-ngrok",
3
+ "version": "2.0.0-beta.1",
4
+ "description": "Ngrok tunnel plugin for elizaOS — third-party backend implementing the @elizaos/plugin-tunnel ITunnelService contract.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ "./package.json": "./package.json",
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc --noCheck -p tsconfig.json",
23
+ "dev": "tsc --watch",
24
+ "typecheck": "tsc --noEmit",
25
+ "test": "bun run --cwd ../plugin-tunnel build && bun test",
26
+ "test:unit": "bun test src/__tests__/unit/",
27
+ "lint": "bunx @biomejs/biome check --write --unsafe .",
28
+ "lint:check": "bunx @biomejs/biome check .",
29
+ "format": "bunx @biomejs/biome format --write .",
30
+ "format:check": "bunx @biomejs/biome format ."
31
+ },
32
+ "keywords": [
33
+ "elizaos",
34
+ "plugin",
35
+ "tunnel",
36
+ "ngrok",
37
+ "networking"
38
+ ],
39
+ "peerDependencies": {
40
+ "@elizaos/core": "2.0.0-beta.1",
41
+ "@elizaos/plugin-tunnel": "2.0.0-beta.1"
42
+ },
43
+ "dependencies": {
44
+ "@elizaos/core": "2.0.0-beta.1",
45
+ "@elizaos/plugin-tunnel": "2.0.0-beta.1",
46
+ "@ngrok/ngrok": "^1.5.1",
47
+ "zod": "^4.4.2"
48
+ },
49
+ "devDependencies": {
50
+ "@biomejs/biome": "^2.4.14",
51
+ "@types/bun": "^1.3.8",
52
+ "@types/node": "^25.1.0",
53
+ "dotenv": "^17.2.3",
54
+ "typescript": "^6.0.3"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "agentConfig": {
60
+ "pluginType": "elizaos:plugin:1.0.0",
61
+ "pluginParameters": {}
62
+ }
63
+ }
@@ -0,0 +1,110 @@
1
+ import type { IAgentRuntime, TestCase } from '@elizaos/core';
2
+ import { NgrokService } from '../services/NgrokService';
3
+
4
+ export class NgrokTestSuite {
5
+ name = 'ngrok';
6
+ tests: TestCase[] = [
7
+ {
8
+ name: 'Initialize Ngrok Service',
9
+ fn: async (runtime: IAgentRuntime) => {
10
+ // Test service initialization
11
+ const service = new NgrokService(runtime);
12
+
13
+ // Verify service is initialized
14
+ if (!service) {
15
+ throw new Error('Failed to initialize Ngrok service');
16
+ }
17
+
18
+ if (!(service instanceof NgrokService)) {
19
+ throw new Error('Service is not instance of NgrokService');
20
+ }
21
+ },
22
+ },
23
+ {
24
+ name: 'Start and Stop Tunnel',
25
+ fn: async (runtime: IAgentRuntime) => {
26
+ const service = new NgrokService(runtime);
27
+
28
+ // Test starting tunnel
29
+ const url = await service.startTunnel(3000);
30
+
31
+ if (!url?.startsWith('https://')) {
32
+ throw new Error('Failed to start tunnel or invalid URL returned');
33
+ }
34
+
35
+ // Verify tunnel is active
36
+ if (!service.isActive()) {
37
+ throw new Error('Tunnel should be active after starting');
38
+ }
39
+
40
+ // Test getting status
41
+ const status = service.getStatus();
42
+ if (!status.active || status.port !== 3000) {
43
+ throw new Error('Invalid tunnel status');
44
+ }
45
+
46
+ // Test stopping tunnel
47
+ await service.stopTunnel();
48
+
49
+ // Verify tunnel is stopped
50
+ if (service.isActive()) {
51
+ throw new Error('Tunnel should not be active after stopping');
52
+ }
53
+ },
54
+ },
55
+ {
56
+ name: 'Service Registration',
57
+ fn: async (runtime: IAgentRuntime) => {
58
+ // Test that service can be registered and retrieved
59
+ const _service = new NgrokService(runtime);
60
+
61
+ // In a real test, we would register the service with runtime
62
+ // and then retrieve it to verify registration
63
+ const serviceType = NgrokService.serviceType;
64
+ if (serviceType !== 'tunnel') {
65
+ throw new Error('Service type should be "tunnel"');
66
+ }
67
+ },
68
+ },
69
+ {
70
+ name: 'Tunnel Status When Inactive',
71
+ fn: async (runtime: IAgentRuntime) => {
72
+ const service = new NgrokService(runtime);
73
+
74
+ // Get status when tunnel is not active
75
+ const status = service.getStatus();
76
+
77
+ if (status.active) {
78
+ throw new Error('Tunnel should not be active initially');
79
+ }
80
+
81
+ if (status.url !== null || status.port !== null) {
82
+ throw new Error('URL and port should be null when inactive');
83
+ }
84
+
85
+ if (status.provider !== 'ngrok') {
86
+ throw new Error('Provider should always be "ngrok"');
87
+ }
88
+ },
89
+ },
90
+ {
91
+ name: 'Multiple Start Attempts',
92
+ fn: async (runtime: IAgentRuntime) => {
93
+ const service = new NgrokService(runtime);
94
+
95
+ // Start tunnel
96
+ const url1 = await service.startTunnel(3000);
97
+
98
+ // Try to start again (should return same URL)
99
+ const url2 = await service.startTunnel(3000);
100
+
101
+ if (url1 !== url2) {
102
+ throw new Error('Starting tunnel twice should return the same URL');
103
+ }
104
+
105
+ // Clean up
106
+ await service.stopTunnel();
107
+ },
108
+ },
109
+ ];
110
+ }
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { createMockRuntime } from './test-utils';
3
+
4
+ describe('Debug Mock Runtime', () => {
5
+ it('should have getSetting method', () => {
6
+ const runtime = createMockRuntime();
7
+
8
+ console.log('Runtime object keys:', Object.keys(runtime));
9
+ console.log('getSetting method exists:', typeof runtime.getSetting);
10
+ console.log('getSetting result for NGROK_AUTH_TOKEN:', runtime.getSetting('NGROK_AUTH_TOKEN'));
11
+
12
+ expect(typeof runtime.getSetting).toBe('function');
13
+ expect(runtime.getSetting('NGROK_AUTH_TOKEN')).toBeDefined();
14
+ });
15
+ });