@kadi.build/tunnel-services 1.0.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/README.md ADDED
@@ -0,0 +1,343 @@
1
+ # @kadi.build/tunnel-services
2
+
3
+ > Unified tunnel service manager for exposing local ports to the internet. Supports 6 tunnel providers with automatic failover, KĀDI as the default primary service.
4
+
5
+ ## Features
6
+
7
+ - 🚇 **6 Tunnel Providers** — KĀDI, Serveo, ngrok, LocalTunnel, Pinggy, localhost.run
8
+ - 🔄 **Automatic Fallback** — Seamlessly switches to the next provider on failure
9
+ - 🎯 **KĀDI-First** — KĀDI is the default primary tunnel service
10
+ - 📡 **Event-Driven** — Rich event system for monitoring tunnel lifecycle
11
+ - 🔧 **Diagnostic Tools** — Built-in diagnostics for troubleshooting
12
+ - 🧩 **Pluggable** — Register custom tunnel services easily
13
+ - 📦 **Zero Config** — Works out of the box with sensible defaults
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @kadi.build/tunnel-services
19
+ ```
20
+
21
+ ### Optional Dependencies
22
+
23
+ Install based on which providers you want to use:
24
+
25
+ ```bash
26
+ # For ngrok support
27
+ npm install @ngrok/ngrok
28
+
29
+ # For localtunnel support
30
+ npm install localtunnel
31
+
32
+ # SSH-based providers (serveo, pinggy, localhost.run, kadi)
33
+ # require `ssh` on your PATH — no extra packages needed
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ### One-Liner
39
+
40
+ ```js
41
+ import { expose } from '@kadi.build/tunnel-services';
42
+
43
+ // Expose port 3000 to the internet
44
+ const url = await expose(3000);
45
+ console.log(`Public URL: ${url}`);
46
+ ```
47
+
48
+ ### With Tunnel Management
49
+
50
+ ```js
51
+ import { createTunnel } from '@kadi.build/tunnel-services';
52
+
53
+ const tunnel = await createTunnel(3000, {
54
+ service: 'kadi', // or 'serveo', 'ngrok', 'localtunnel', 'pinggy', 'localhost.run'
55
+ subdomain: 'my-app', // optional
56
+ autoFallback: true // try next provider on failure
57
+ });
58
+
59
+ console.log(`Public URL: ${tunnel.publicUrl}`);
60
+ console.log(`Local Port: ${tunnel.localPort}`);
61
+ console.log(`Service: ${tunnel.service}`);
62
+
63
+ // When done
64
+ await tunnel.close();
65
+ ```
66
+
67
+ ### Full Control with TunnelManager
68
+
69
+ ```js
70
+ import { TunnelManager } from '@kadi.build/tunnel-services';
71
+
72
+ const manager = new TunnelManager({
73
+ primaryService: 'kadi',
74
+ fallbackServices: ['serveo', 'ngrok', 'localtunnel', 'pinggy'],
75
+ autoFallback: true,
76
+ maxConcurrentTunnels: 10,
77
+ connectionTimeout: 30000
78
+ });
79
+
80
+ await manager.initialize();
81
+
82
+ // Listen for events
83
+ manager.on('tunnelCreated', ({ id, publicUrl, service }) => {
84
+ console.log(`Tunnel ${id} created on ${service}: ${publicUrl}`);
85
+ });
86
+
87
+ manager.on('tunnelDestroyed', ({ id }) => {
88
+ console.log(`Tunnel ${id} closed`);
89
+ });
90
+
91
+ manager.on('serviceFailed', ({ service, error }) => {
92
+ console.warn(`${service} failed: ${error.message}, trying next...`);
93
+ });
94
+
95
+ // Create tunnels
96
+ const tunnel1 = await manager.createTunnel(3000);
97
+ const tunnel2 = await manager.createTunnel(8080, { service: 'ngrok' });
98
+
99
+ // Check status
100
+ console.log(manager.getStatus());
101
+
102
+ // Close specific tunnel
103
+ await manager.closeTunnel(tunnel1.id);
104
+
105
+ // Cleanup everything
106
+ await manager.cleanup();
107
+ ```
108
+
109
+ ## API Reference
110
+
111
+ ### Factory Functions
112
+
113
+ #### `expose(port, service?)` → `Promise<string>`
114
+
115
+ Returns a public URL string. Simplest way to expose a port.
116
+
117
+ | Parameter | Type | Default | Description |
118
+ |-----------|------|---------|-------------|
119
+ | `port` | `number` | — | Local port to expose |
120
+ | `service` | `string` | `'kadi'` | Tunnel service to use |
121
+
122
+ #### `createTunnel(port, options?)` → `Promise<TunnelInfo>`
123
+
124
+ Creates a tunnel and returns a `TunnelInfo` object with a `close()` method.
125
+
126
+ | Parameter | Type | Default | Description |
127
+ |-----------|------|---------|-------------|
128
+ | `port` | `number` | — | Local port to expose |
129
+ | `options.service` | `string` | `'kadi'` | Preferred service |
130
+ | `options.subdomain` | `string` | — | Requested subdomain |
131
+ | `options.autoFallback` | `boolean` | `true` | Enable fallback |
132
+
133
+ ### TunnelManager
134
+
135
+ The main orchestrator class. Extends `EventEmitter`.
136
+
137
+ #### Constructor Options
138
+
139
+ ```js
140
+ new TunnelManager({
141
+ primaryService: 'kadi', // Default primary service
142
+ fallbackServices: ['serveo', 'ngrok', 'localtunnel', 'pinggy'],
143
+ autoFallback: true, // Auto-fallback on failure
144
+ maxConcurrentTunnels: 10, // Max simultaneous tunnels
145
+ connectionTimeout: 30000, // Connection timeout (ms)
146
+ ngrokAuthToken: '', // Ngrok auth token
147
+ kadiServer: '', // KĀDI tunnel server
148
+ kadiMode: 'ssh' // 'ssh' or 'frpc'
149
+ })
150
+ ```
151
+
152
+ #### Methods
153
+
154
+ | Method | Returns | Description |
155
+ |--------|---------|-------------|
156
+ | `initialize()` | `Promise<void>` | Discover and load services |
157
+ | `createTunnel(port, options?)` | `Promise<TunnelInfo>` | Create a new tunnel |
158
+ | `closeTunnel(id)` | `Promise<void>` | Close specific tunnel |
159
+ | `closeAllTunnels()` | `Promise<void>` | Close all active tunnels |
160
+ | `getStatus()` | `object` | Manager status + active tunnels |
161
+ | `getAvailableServices()` | `string[]` | List discovered services |
162
+ | `testService(name)` | `Promise<object>` | Test if a service is available |
163
+ | `runDiagnostics()` | `Promise<object>` | Run diagnostics on all services |
164
+ | `cleanup()` | `Promise<void>` | Full shutdown |
165
+
166
+ #### Events
167
+
168
+ | Event | Payload | Description |
169
+ |-------|---------|-------------|
170
+ | `tunnelCreated` | `TunnelInfo` | Tunnel successfully created |
171
+ | `tunnelDestroyed` | `{ id }` | Tunnel closed |
172
+ | `serviceFailed` | `{ service, error }` | Service failed, falling back |
173
+ | `error` | `Error` | Unrecoverable error |
174
+
175
+ ### TunnelInfo Object
176
+
177
+ ```js
178
+ {
179
+ id: 'tunnel-abc123', // Unique tunnel ID
180
+ publicUrl: 'https://...', // Public URL
181
+ localPort: 3000, // Local port being exposed
182
+ service: 'kadi', // Service that created it
183
+ subdomain: 'my-app', // Subdomain (if requested)
184
+ createdAt: Date, // Creation timestamp
185
+ metadata: {} // Service-specific metadata
186
+ }
187
+ ```
188
+
189
+ ### BaseTunnelService
190
+
191
+ Abstract base class for all tunnel services. Extend this to create custom providers.
192
+
193
+ ```js
194
+ import { BaseTunnelService } from '@kadi.build/tunnel-services';
195
+
196
+ class MyTunnelService extends BaseTunnelService {
197
+ get name() { return 'my-tunnel'; }
198
+
199
+ async connect(port, options = {}) {
200
+ // Connect and return { url, port, ... }
201
+ }
202
+
203
+ async disconnect() {
204
+ // Clean up
205
+ }
206
+
207
+ getStatus() {
208
+ return { connected: this._connected, service: this.name };
209
+ }
210
+ }
211
+ ```
212
+
213
+ ### Error Classes
214
+
215
+ | Error | Fallback? | Description |
216
+ |-------|-----------|-------------|
217
+ | `TunnelError` | — | Base tunnel error |
218
+ | `TransientTunnelError` | ✅ Yes | Temporary failure, try next service |
219
+ | `PermanentTunnelError` | ❌ No | Permanent failure, do not fallback |
220
+ | `CriticalTunnelError` | ❌ No | Critical system-level failure |
221
+ | `ConfigurationError` | ❌ No | Invalid configuration |
222
+ | `ServiceUnavailableError` | ✅ Yes | Service not available/installed |
223
+ | `ConnectionTimeoutError` | ✅ Yes | Connection timed out |
224
+ | `SSHUnavailableError` | ✅ Yes | SSH binary not found |
225
+ | `AuthenticationFailedError` | ❌ No | Authentication failed |
226
+
227
+ ## Tunnel Providers
228
+
229
+ ### KĀDI (default)
230
+
231
+ SSH or frpc-based tunnel to KĀDI infrastructure.
232
+
233
+ ```js
234
+ const manager = new TunnelManager({
235
+ primaryService: 'kadi',
236
+ kadiServer: 'tunnel.kadi.build',
237
+ kadiMode: 'ssh' // or 'frpc'
238
+ });
239
+ ```
240
+
241
+ **Environment Variables:** `KADI_TUNNEL_SERVER`, `KADI_TUNNEL_PORT`
242
+
243
+ ### Serveo
244
+
245
+ Free SSH-based tunneling via serveo.net. No account required.
246
+
247
+ ```js
248
+ await manager.createTunnel(3000, { service: 'serveo' });
249
+ ```
250
+
251
+ **Requires:** `ssh` on PATH
252
+
253
+ ### ngrok
254
+
255
+ Industry-standard tunnel service. Requires auth token for extended use.
256
+
257
+ ```js
258
+ const manager = new TunnelManager({
259
+ primaryService: 'ngrok',
260
+ ngrokAuthToken: process.env.NGROK_AUTH_TOKEN
261
+ });
262
+ ```
263
+
264
+ **Requires:** `@ngrok/ngrok` or legacy `ngrok` package
265
+ **Environment Variables:** `NGROK_AUTH_TOKEN`
266
+
267
+ ### LocalTunnel
268
+
269
+ Open-source tunneling via localtunnel.me.
270
+
271
+ ```js
272
+ await manager.createTunnel(3000, { service: 'localtunnel' });
273
+ ```
274
+
275
+ **Requires:** `localtunnel` npm package
276
+
277
+ ### Pinggy
278
+
279
+ SSH-based tunneling via pinggy.io.
280
+
281
+ ```js
282
+ await manager.createTunnel(3000, { service: 'pinggy' });
283
+ ```
284
+
285
+ **Requires:** `ssh` on PATH
286
+
287
+ ### localhost.run
288
+
289
+ SSH-based tunneling via localhost.run. No account required.
290
+
291
+ ```js
292
+ await manager.createTunnel(3000, { service: 'localhost.run' });
293
+ ```
294
+
295
+ **Requires:** `ssh` on PATH
296
+
297
+ ## Fallback Order
298
+
299
+ When `autoFallback: true` (default), the manager tries services in this order:
300
+
301
+ 1. **KĀDI** (primary)
302
+ 2. **Serveo**
303
+ 3. **ngrok**
304
+ 4. **LocalTunnel**
305
+ 5. **Pinggy**
306
+
307
+ If a service throws a `TransientTunnelError` or `ServiceUnavailableError`, the next service in the chain is attempted. `PermanentTunnelError` stops the fallback chain.
308
+
309
+ ## Testing
310
+
311
+ ```bash
312
+ # Run all tests
313
+ npm test
314
+
315
+ # Run specific test suite
316
+ npm run test:manager
317
+ npm run test:kadi
318
+ npm run test:ngrok
319
+ npm run test:serveo
320
+
321
+ # Integration tests (require credentials/tools)
322
+ NGROK_AUTH_TOKEN=xxx npm run test:ngrok
323
+ KADI_TUNNEL_SERVER=tunnel.kadi.build npm run test:kadi
324
+ ```
325
+
326
+ ## Environment Variables
327
+
328
+ | Variable | Used By | Description |
329
+ |----------|---------|-------------|
330
+ | `NGROK_AUTH_TOKEN` | ngrok | Authentication token |
331
+ | `KADI_TUNNEL_SERVER` | KĀDI | Tunnel server hostname |
332
+ | `KADI_TUNNEL_PORT` | KĀDI | Tunnel server port |
333
+ | `DEBUG` | all | Debug logging (e.g., `kadi:tunnel:*`) |
334
+
335
+ ## Requirements
336
+
337
+ - Node.js >= 18.0.0
338
+ - SSH on PATH (for serveo, pinggy, localhost.run, kadi)
339
+ - Optional: `@ngrok/ngrok`, `localtunnel` npm packages
340
+
341
+ ## License
342
+
343
+ MIT © KĀDI
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@kadi.build/tunnel-services",
3
+ "version": "1.0.0",
4
+ "description": "Unified tunnel management for exposing local ports via ngrok, serveo, localtunnel, KĀDI, and more",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./src/index.js"
10
+ }
11
+ },
12
+ "files": [
13
+ "src/",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "test": "node tests/run-all-tests.js",
18
+ "test:integration": "node tests/test-kadi-integration.js",
19
+ "test:manager": "node tests/test-tunnel-manager.js",
20
+ "test:ngrok": "node tests/test-ngrok-tunnel.js",
21
+ "test:serveo": "node tests/test-serveo-tunnel.js",
22
+ "test:localtunnel": "node tests/test-localtunnel.js",
23
+ "test:pinggy": "node tests/test-pinggy-tunnel.js",
24
+ "test:kadi": "node tests/test-kadi-tunnel.js",
25
+ "test:all-services": "node tests/test-all-tunnels.js"
26
+ },
27
+ "keywords": [
28
+ "tunnel",
29
+ "ngrok",
30
+ "serveo",
31
+ "localtunnel",
32
+ "pinggy",
33
+ "kadi",
34
+ "localhost",
35
+ "expose",
36
+ "port-forwarding"
37
+ ],
38
+ "dependencies": {
39
+ "@kadi.build/tunnel-client": "^0.3.0",
40
+ "debug": "^4.3.4"
41
+ },
42
+ "optionalDependencies": {
43
+ "@ngrok/ngrok": "^1.4.1",
44
+ "localtunnel": "^2.0.2"
45
+ },
46
+ "peerDependencies": {
47
+ "ssh2": "^1.15.0"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "ssh2": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "engines": {
55
+ "node": ">=18.0.0"
56
+ },
57
+ "license": "MIT"
58
+ }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * @fileoverview Abstract base class for all tunnel services
3
+ * Based on TunnelManagerSpec.md Section 3.2 specification
4
+ */
5
+
6
+ import EventEmitter from 'events';
7
+ import {
8
+ TransientTunnelError,
9
+ PermanentTunnelError,
10
+ CriticalTunnelError,
11
+ ConfigurationError,
12
+ SSHUnavailableError,
13
+ ConnectionTimeoutError,
14
+ AuthenticationFailedError
15
+ } from './errors.js';
16
+
17
+ /**
18
+ * Abstract base class that defines the contract for all tunnel service implementations.
19
+ * This class MUST NOT be instantiated directly - it serves as an interface specification.
20
+ *
21
+ * All concrete tunnel services must extend this class and implement its abstract methods.
22
+ *
23
+ * @abstract
24
+ * @extends EventEmitter
25
+ */
26
+ export class BaseTunnelService extends EventEmitter {
27
+ /**
28
+ * Constructor for the base tunnel service
29
+ * @param {Object} config - Global tunnel configuration object
30
+ * @throws {Error} If instantiated directly (must be subclassed)
31
+ */
32
+ constructor(config) {
33
+ super();
34
+
35
+ // Prevent direct instantiation of abstract class
36
+ if (this.constructor === BaseTunnelService) {
37
+ throw new Error('BaseTunnelService is abstract and cannot be instantiated directly');
38
+ }
39
+
40
+ this.config = config || {};
41
+ this.activeTunnels = new Map();
42
+ this.isShuttingDown = false;
43
+
44
+ // Validate that subclass implements required abstract methods
45
+ this._validateImplementation();
46
+ }
47
+
48
+ /**
49
+ * Validates that the subclass properly implements all abstract methods
50
+ * @private
51
+ */
52
+ _validateImplementation() {
53
+ const requiredMethods = ['name', 'connect', 'disconnect', 'getStatus'];
54
+
55
+ for (const method of requiredMethods) {
56
+ if (method === 'name') {
57
+ // Check getter exists and returns string
58
+ const descriptor = Object.getOwnPropertyDescriptor(this.constructor.prototype, method);
59
+ if (!descriptor || !descriptor.get) {
60
+ throw new Error(`Subclass must implement getter '${method}'`);
61
+ }
62
+ } else {
63
+ // Check method exists and is not the abstract implementation
64
+ if (typeof this[method] !== 'function' || this[method] === BaseTunnelService.prototype[method]) {
65
+ throw new Error(`Subclass must implement method '${method}'`);
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Abstract getter that returns the unique service name identifier
73
+ * @abstract
74
+ * @returns {string} The service name (e.g., 'serveo', 'pinggy', 'localtunnel')
75
+ * @throws {Error} Must be implemented by subclass
76
+ */
77
+ get name() {
78
+ throw new Error('Abstract getter "name" must be implemented by subclass');
79
+ }
80
+
81
+ /**
82
+ * Abstract method to establish a tunnel connection
83
+ * @abstract
84
+ * @param {Object} options - Connection options specific to the service
85
+ * @param {number} options.port - Local port to tunnel
86
+ * @param {string} [options.subdomain] - Requested subdomain (if supported)
87
+ * @param {string} [options.region] - Preferred region (if supported)
88
+ * @param {number} [options.timeout=30000] - Connection timeout in milliseconds
89
+ * @returns {Promise<Object>} Promise that resolves with tunnel information
90
+ * @throws {TransientTunnelError} For temporary failures that should trigger fallback
91
+ * @throws {PermanentTunnelError} For permanent failures that should not trigger fallback
92
+ * @throws {CriticalTunnelError} For critical failures that should stop all operations
93
+ */
94
+ async connect(options) {
95
+ throw new Error('Abstract method "connect" must be implemented by subclass');
96
+ }
97
+
98
+ /**
99
+ * Abstract method to disconnect and destroy a tunnel
100
+ * @abstract
101
+ * @param {string} tunnelId - Unique identifier of the tunnel to destroy
102
+ * @returns {Promise<void>} Promise that resolves when tunnel is destroyed
103
+ * @throws {Error} If tunnel ID is not found or destruction fails
104
+ */
105
+ async disconnect(tunnelId) {
106
+ throw new Error('Abstract method "disconnect" must be implemented by subclass');
107
+ }
108
+
109
+ /**
110
+ * Abstract method to get current service status
111
+ * @abstract
112
+ * @returns {Object} Current status of the service
113
+ */
114
+ getStatus() {
115
+ throw new Error('Abstract method "getStatus" must be implemented by subclass');
116
+ }
117
+
118
+ /**
119
+ * Shutdown method for cleanup when service is no longer needed
120
+ * Can be overridden by subclasses for specific cleanup logic
121
+ * @returns {Promise<void>} Promise that resolves when shutdown is complete
122
+ */
123
+ async shutdown() {
124
+ this.isShuttingDown = true;
125
+
126
+ // Disconnect all active tunnels
127
+ const disconnectPromises = Array.from(this.activeTunnels.keys()).map(tunnelId =>
128
+ this.disconnect(tunnelId).catch(error => {
129
+ // Silently handle shutdown errors
130
+ })
131
+ );
132
+
133
+ await Promise.allSettled(disconnectPromises);
134
+ this.activeTunnels.clear();
135
+ this.removeAllListeners();
136
+ }
137
+
138
+ /**
139
+ * Validates configuration object for the service
140
+ * Can be overridden by subclasses for service-specific validation
141
+ * @param {Object} config - Configuration to validate
142
+ * @throws {ConfigurationError} If configuration is invalid
143
+ */
144
+ validateConfig(config) {
145
+ if (!config || typeof config !== 'object') {
146
+ throw new ConfigurationError('Configuration must be an object');
147
+ }
148
+
149
+ // Base validation - subclasses can override for specific requirements
150
+ if (config.timeout !== undefined && (typeof config.timeout !== 'number' || config.timeout <= 0)) {
151
+ throw new ConfigurationError('Timeout must be a positive number', 'timeout', config.timeout);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Determines if an error is transient (should trigger fallback)
157
+ * @param {Error} error - Error to categorize
158
+ * @returns {boolean} True if error is transient
159
+ */
160
+ isTransientError(error) {
161
+ if (error instanceof TransientTunnelError) return true;
162
+ if (error instanceof PermanentTunnelError || error instanceof CriticalTunnelError) return false;
163
+
164
+ const message = error.message ? error.message.toLowerCase() : '';
165
+ const transientPatterns = [
166
+ 'timeout',
167
+ 'connection refused',
168
+ 'network unreachable',
169
+ 'host unreachable',
170
+ 'temporarily unavailable',
171
+ 'service unavailable',
172
+ 'econnrefused',
173
+ 'enotfound',
174
+ 'etimedout'
175
+ ];
176
+
177
+ return transientPatterns.some(pattern => message.includes(pattern));
178
+ }
179
+
180
+ /**
181
+ * Determines if an error is permanent (should not trigger fallback)
182
+ * @param {Error} error - Error to categorize
183
+ * @returns {boolean} True if error is permanent
184
+ */
185
+ isPermanentError(error) {
186
+ if (error instanceof PermanentTunnelError) return true;
187
+ if (error instanceof TransientTunnelError) return false;
188
+
189
+ const message = error.message ? error.message.toLowerCase() : '';
190
+ const permanentPatterns = [
191
+ 'ssh: command not found',
192
+ 'permission denied',
193
+ 'authentication failed',
194
+ 'invalid configuration',
195
+ 'command not found',
196
+ 'access denied',
197
+ 'unauthorized',
198
+ 'forbidden',
199
+ 'invalid subdomain',
200
+ 'malformed',
201
+ 'eacces'
202
+ ];
203
+
204
+ return permanentPatterns.some(pattern => message.includes(pattern));
205
+ }
206
+
207
+ /**
208
+ * Determines if an error is critical (should stop all operations)
209
+ * @param {Error} error - Error to categorize
210
+ * @returns {boolean} True if error is critical
211
+ */
212
+ isCriticalError(error) {
213
+ if (error instanceof CriticalTunnelError) return true;
214
+
215
+ const message = error.message ? error.message.toLowerCase() : '';
216
+ const criticalPatterns = [
217
+ 'out of memory',
218
+ 'file descriptor',
219
+ 'resource exhaustion',
220
+ 'security violation',
221
+ 'corrupted',
222
+ 'system error',
223
+ 'emfile',
224
+ 'enomem'
225
+ ];
226
+
227
+ return criticalPatterns.some(pattern => message.includes(pattern));
228
+ }
229
+
230
+ /**
231
+ * Generates a unique tunnel ID
232
+ * @protected
233
+ * @returns {string} Unique tunnel identifier
234
+ */
235
+ _generateTunnelId() {
236
+ return `tunnel_${this.name}_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
237
+ }
238
+
239
+ /**
240
+ * Emits a standardized progress event
241
+ * @protected
242
+ * @param {string} status - Status identifier (e.g., 'connecting', 'connected', 'disconnecting')
243
+ * @param {string} message - Human-readable progress message
244
+ * @param {string} [tunnelId] - Associated tunnel ID if applicable
245
+ */
246
+ _emitProgress(status, message, tunnelId = null) {
247
+ this.emit('tunnelProgress', {
248
+ service: this.name,
249
+ status,
250
+ message,
251
+ tunnelId,
252
+ timestamp: new Date().toISOString()
253
+ });
254
+ }
255
+
256
+ /**
257
+ * Emits a standardized error event
258
+ * @protected
259
+ * @param {Error} error - Error that occurred
260
+ * @param {string} [tunnelId] - Associated tunnel ID if applicable
261
+ */
262
+ _emitError(error, tunnelId = null) {
263
+ this.emit('tunnelError', {
264
+ service: this.name,
265
+ error: error.message || String(error),
266
+ type: error.constructor ? error.constructor.name : 'Error',
267
+ tunnelId,
268
+ timestamp: new Date().toISOString(),
269
+ isTransient: this.isTransientError(error instanceof Error ? error : new Error(String(error))),
270
+ isPermanent: this.isPermanentError(error instanceof Error ? error : new Error(String(error))),
271
+ isCritical: this.isCriticalError(error instanceof Error ? error : new Error(String(error)))
272
+ });
273
+ }
274
+
275
+ /**
276
+ * Emits a tunnel created event
277
+ * @protected
278
+ * @param {Object} tunnelInfo - Information about the created tunnel
279
+ */
280
+ _emitTunnelCreated(tunnelInfo) {
281
+ this.emit('tunnelCreated', {
282
+ ...tunnelInfo,
283
+ service: this.name,
284
+ timestamp: new Date().toISOString()
285
+ });
286
+ }
287
+
288
+ /**
289
+ * Emits a tunnel destroyed event
290
+ * @protected
291
+ * @param {string} tunnelId - ID of the destroyed tunnel
292
+ */
293
+ _emitTunnelDestroyed(tunnelId) {
294
+ this.emit('tunnelDestroyed', {
295
+ service: this.name,
296
+ tunnelId,
297
+ timestamp: new Date().toISOString()
298
+ });
299
+ }
300
+ }