@frp-bridge/core 0.0.1 → 0.0.2

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/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- import { ClientConfig, ServerConfig, ProxyConfig } from '@frp-bridge/types';
1
+ import { NodeRegisterPayload, NodeHeartbeatPayload, NodeInfo as NodeInfo$1, NodeListQuery, NodeListResponse, NodeStatistics, TunnelSyncPayload, ProxyConfig, ClientConfig, ServerConfig, RpcRequest } from '@frp-bridge/types';
2
+ import { EventEmitter } from 'node:events';
2
3
 
3
4
  type Awaitable<T> = T | Promise<T>;
4
5
  type RuntimeMode = 'client' | 'server';
@@ -62,7 +63,7 @@ interface RuntimeError {
62
63
  message: string;
63
64
  details?: Record<string, unknown>;
64
65
  }
65
- type RuntimeErrorCode = 'VALIDATION_ERROR' | 'RUNTIME_ERROR' | 'SYSTEM_ERROR';
66
+ type RuntimeErrorCode = 'VALIDATION_ERROR' | 'RUNTIME_ERROR' | 'SYSTEM_ERROR' | 'PORT_CONFLICT' | 'RPC_NOT_AVAILABLE' | 'RPC_ERROR';
66
67
  interface ConfigSnapshot {
67
68
  version: number;
68
69
  checksum: string;
@@ -113,6 +114,153 @@ declare class FrpRuntime {
113
114
  private now;
114
115
  }
115
116
 
117
+ /**
118
+ * Client-side node information collector
119
+ * Gathers system information and reports to server via heartbeat
120
+ */
121
+
122
+ interface ClientCollectorOptions {
123
+ /** Node ID (set by server after registration) */
124
+ nodeId?: string;
125
+ /** Heartbeat interval in milliseconds (default: 30000) */
126
+ heartbeatInterval?: number;
127
+ /** Logger instance */
128
+ logger?: {
129
+ debug?: (msg: string, data?: unknown) => void;
130
+ info?: (msg: string, data?: unknown) => void;
131
+ error?: (msg: string, error?: unknown) => void;
132
+ };
133
+ }
134
+ /**
135
+ * Collects node information on client side
136
+ * Used in client mode to send system info and heartbeat to server
137
+ */
138
+ declare class ClientNodeCollector {
139
+ private nodeId?;
140
+ private heartbeatInterval;
141
+ private logger;
142
+ private heartbeatTimer?;
143
+ constructor(options?: ClientCollectorOptions);
144
+ /** Set node ID after server registration */
145
+ setNodeId(nodeId: string): void;
146
+ /** Collect current node information */
147
+ collectNodeInfo(): Partial<NodeRegisterPayload>;
148
+ /** Collect heartbeat payload */
149
+ collectHeartbeat(): Partial<NodeHeartbeatPayload>;
150
+ /**
151
+ * Start periodic heartbeat collection
152
+ * Callback will be called at each interval with heartbeat payload
153
+ */
154
+ startHeartbeat(callback: (payload: Partial<NodeHeartbeatPayload>) => void, interval?: number): void;
155
+ /** Stop periodic heartbeat collection */
156
+ stopHeartbeat(): void;
157
+ /** Check if heartbeat is running */
158
+ isHeartbeatRunning(): boolean;
159
+ }
160
+
161
+ /**
162
+ * Node Manager for server-side node management
163
+ * Handles node registration, heartbeat, tunnel registry, and queries
164
+ */
165
+
166
+ interface NodeManagerOptions {
167
+ heartbeatTimeout?: number;
168
+ logger?: any;
169
+ }
170
+ interface NodeStorage {
171
+ save: (node: NodeInfo$1) => Promise<void> | void;
172
+ delete: (id: string) => Promise<void> | void;
173
+ load: (id: string) => Promise<NodeInfo$1 | undefined> | NodeInfo$1 | undefined;
174
+ list: () => Promise<NodeInfo$1[]> | NodeInfo$1[];
175
+ }
176
+ type NodeEvent = 'node:registered' | 'node:heartbeat' | 'node:unregistered' | 'node:statusChanged' | 'tunnel:synced';
177
+ /**
178
+ * Manages nodes in server mode
179
+ * Stores node info, handles heartbeat, manages global tunnel registry, emits events
180
+ */
181
+ declare class NodeManager extends EventEmitter {
182
+ private context;
183
+ private nodes;
184
+ private heartbeatTimers;
185
+ private tunnelRegistry;
186
+ private storage?;
187
+ private heartbeatTimeout;
188
+ private logger?;
189
+ constructor(context: RuntimeContext, options?: NodeManagerOptions, storage?: NodeStorage);
190
+ initialize(): Promise<void>;
191
+ /** Register a new node (called when client connects) */
192
+ registerNode(payload: NodeRegisterPayload): Promise<NodeInfo$1>;
193
+ /** Update node heartbeat and status */
194
+ updateHeartbeat(payload: NodeHeartbeatPayload): Promise<void>;
195
+ /** Unregister a node (called when client disconnects) */
196
+ unregisterNode(nodeId: string): Promise<void>;
197
+ /** Get node by id */
198
+ getNode(id: string): Promise<NodeInfo$1 | undefined>;
199
+ /** List nodes with pagination and filtering */
200
+ listNodes(query?: NodeListQuery): Promise<NodeListResponse>;
201
+ /** Get node statistics */
202
+ getStatistics(): Promise<NodeStatistics>;
203
+ /** Check if node exists */
204
+ hasNode(id: string): boolean;
205
+ /** Get all online nodes */
206
+ getOnlineNodes(): NodeInfo$1[];
207
+ /** Get all offline nodes */
208
+ getOfflineNodes(): NodeInfo$1[];
209
+ /** Get nodes by status */
210
+ getNodesByStatus(status: NodeInfo$1['status']): NodeInfo$1[];
211
+ /** Setup heartbeat timer for a node */
212
+ private setupHeartbeatTimer;
213
+ /** Clear heartbeat timer for a node */
214
+ private clearHeartbeatTimer;
215
+ /** Handle heartbeat timeout */
216
+ private handleHeartbeatTimeout;
217
+ /** Sync tunnels for a node (called when node connects or updates tunnels) */
218
+ syncTunnels(payload: TunnelSyncPayload): Promise<void>;
219
+ /** Get tunnels for a specific node */
220
+ getNodeTunnels(nodeId: string): ProxyConfig[];
221
+ /** Get all tunnels across all nodes */
222
+ getAllTunnels(): Map<string, ProxyConfig[]>;
223
+ /** Check if a remotePort is in use across all nodes (for conflict detection) */
224
+ isRemotePortInUse(remotePort: number, excludeNodeId?: string): {
225
+ inUse: boolean;
226
+ nodeId?: string;
227
+ tunnelName?: string;
228
+ };
229
+ /** Clear tunnels for a node (called when node disconnects) */
230
+ private clearNodeTunnels;
231
+ /** Update dispose method to clear tunnels */
232
+ dispose(): Promise<void>;
233
+ }
234
+
235
+ /**
236
+ * File-based node storage implementation
237
+ * Persists node information to disk
238
+ */
239
+
240
+ /**
241
+ * Stores nodes in JSON files
242
+ * Directory structure:
243
+ * ~/.frp-bridge/runtime/nodes/
244
+ * ├── nodes.json (index of all nodes)
245
+ * └── node-{id}.json (individual node data)
246
+ */
247
+ declare class FileNodeStorage implements NodeStorage {
248
+ private storagePath;
249
+ private indexPath;
250
+ private nodeDir;
251
+ constructor(storagePath: string);
252
+ /** Save or update a node */
253
+ save(node: NodeInfo$1): Promise<void>;
254
+ /** Delete a node */
255
+ delete(id: string): Promise<void>;
256
+ /** Load a single node */
257
+ load(id: string): Promise<NodeInfo$1 | undefined>;
258
+ /** Load all nodes */
259
+ list(): Promise<NodeInfo$1[]>;
260
+ /** Update the index of node IDs */
261
+ private updateIndex;
262
+ }
263
+
116
264
  /**
117
265
  * FRP process management utilities
118
266
  */
@@ -120,6 +268,8 @@ declare class FrpRuntime {
120
268
  interface FrpProcessManagerOptions {
121
269
  /** Working directory for FRP files */
122
270
  workDir?: string;
271
+ /** Path to config file (overrides default) */
272
+ configPath?: string;
123
273
  /** FRP version (defaults to latest) */
124
274
  version?: string;
125
275
  /** Mode: client or server */
@@ -144,7 +294,7 @@ interface NodeInfo {
144
294
  /**
145
295
  * Manages FRP client/server lifecycle, config, and tunnels
146
296
  */
147
- declare class FrpProcessManager {
297
+ declare class FrpProcessManager extends EventEmitter {
148
298
  private readonly workDir;
149
299
  private version;
150
300
  private readonly mode;
@@ -153,6 +303,8 @@ declare class FrpProcessManager {
153
303
  private process;
154
304
  private configPath;
155
305
  private binaryPath;
306
+ private uptime;
307
+ private isManualStop;
156
308
  constructor(options: FrpProcessManagerOptions);
157
309
  /** Ensure version is fetched and binary path is set */
158
310
  private ensureVersion;
@@ -171,9 +323,9 @@ declare class FrpProcessManager {
171
323
  /** Return the absolute config file path */
172
324
  getConfigPath(): string;
173
325
  /** Read raw config file contents */
174
- readConfigFile(): string | null;
326
+ getConfigRaw(): string | null;
175
327
  /** Overwrite config file with provided content */
176
- writeConfigFile(content: string): void;
328
+ updateConfigRaw(content: string): void;
177
329
  /** Start FRP process */
178
330
  start(): Promise<void>;
179
331
  /** Stop FRP process */
@@ -190,6 +342,8 @@ declare class FrpProcessManager {
190
342
  removeNode(): void;
191
343
  /** Add tunnel (proxy) */
192
344
  addTunnel(proxy: ProxyConfig): void;
345
+ /** Check if proxy type uses remotePort */
346
+ private typeUsesRemotePort;
193
347
  /** Get tunnel by name */
194
348
  getTunnel(name: string): ProxyConfig | null;
195
349
  /** Update tunnel */
@@ -198,6 +352,70 @@ declare class FrpProcessManager {
198
352
  removeTunnel(name: string): void;
199
353
  /** List all tunnels */
200
354
  listTunnels(): ProxyConfig[];
355
+ /**
356
+ * Query current process status
357
+ */
358
+ queryProcess(): {
359
+ pid: number | undefined;
360
+ uptime: number;
361
+ };
362
+ private setupProcessListeners;
363
+ }
364
+
365
+ interface RpcClientOptions {
366
+ url: string;
367
+ nodeId: string;
368
+ getRegisterPayload: () => Promise<NodeInfo$1> | NodeInfo$1;
369
+ handleRequest: (req: RpcRequest) => Promise<unknown>;
370
+ reconnectInterval?: number;
371
+ logger?: {
372
+ info?: (msg: string, data?: unknown) => void;
373
+ warn?: (msg: string, data?: unknown) => void;
374
+ error?: (msg: string, data?: unknown) => void;
375
+ };
376
+ }
377
+ declare class RpcClient {
378
+ private readonly options;
379
+ private ws;
380
+ private reconnectTimer?;
381
+ private readonly reconnectInterval;
382
+ constructor(options: RpcClientOptions);
383
+ connect(): Promise<void>;
384
+ disconnect(): void;
385
+ private createConnection;
386
+ private handleMessage;
387
+ private handleRpcRequest;
388
+ private send;
389
+ private scheduleReconnect;
390
+ }
391
+
392
+ interface RpcServerOptions {
393
+ port: number;
394
+ heartbeatInterval?: number;
395
+ logger?: {
396
+ info?: (msg: string, data?: unknown) => void;
397
+ warn?: (msg: string, data?: unknown) => void;
398
+ error?: (msg: string, data?: unknown) => void;
399
+ };
400
+ validateToken?: (token: string | undefined, nodeId: string | undefined) => boolean | Promise<boolean>;
401
+ authorize?: (nodeId: string, method: string) => boolean | Promise<boolean>;
402
+ onRegister?: (nodeId: string, payload: NodeInfo$1) => void | Promise<void>;
403
+ }
404
+ declare class RpcServer {
405
+ private readonly options;
406
+ private readonly clients;
407
+ private readonly pendingRequests;
408
+ private readonly wsToNode;
409
+ private heartbeatTimer?;
410
+ private server?;
411
+ constructor(options: RpcServerOptions);
412
+ start(): void;
413
+ stop(): void;
414
+ rpcCall(nodeId: string, method: string, params: Record<string, unknown>, timeout?: number): Promise<unknown>;
415
+ private handleMessage;
416
+ private handleRpcResponse;
417
+ private handleClose;
418
+ private startHeartbeat;
201
419
  }
202
420
 
203
421
  interface FrpBridgeRuntimeOptions {
@@ -210,34 +428,90 @@ interface FrpBridgeRuntimeOptions {
210
428
  }
211
429
  interface FrpBridgeProcessOptions extends Partial<Omit<FrpProcessManagerOptions, 'mode'>> {
212
430
  mode?: 'client' | 'server';
213
- workDir?: string;
214
431
  }
432
+ interface FrpBridgeRpcOptions {
433
+ serverPort?: number;
434
+ serverHeartbeatInterval?: number;
435
+ serverValidateToken?: (token: string | undefined, nodeId: string | undefined) => boolean | Promise<boolean>;
436
+ serverAuthorize?: (nodeId: string, method: string) => boolean | Promise<boolean>;
437
+ clientUrl?: string;
438
+ clientNodeId?: string;
439
+ clientToken?: string;
440
+ clientReconnectInterval?: number;
441
+ getRegisterPayload?: () => Promise<NodeInfo$1> | NodeInfo$1;
442
+ handleRequest?: (req: RpcRequest) => Promise<unknown>;
443
+ }
444
+
215
445
  interface FrpBridgeOptions {
216
446
  mode: 'client' | 'server';
217
447
  workDir?: string;
448
+ configPath?: string;
218
449
  runtime?: FrpBridgeRuntimeOptions;
219
450
  process?: FrpBridgeProcessOptions;
451
+ rpc?: FrpBridgeRpcOptions;
220
452
  storage?: SnapshotStorage;
221
453
  commands?: Record<string, CommandHandler>;
222
454
  queries?: Record<string, QueryHandler>;
223
455
  eventSink?: (event: RuntimeEvent) => void;
224
456
  }
457
+ /**
458
+ * FrpBridge - Main facade class for managing FRP bridge operations.
459
+ *
460
+ * This class serves as a facade that coordinates multiple components:
461
+ * - Runtime management (command/query execution)
462
+ * - Process management (FRP process lifecycle)
463
+ * - Node management (server mode only)
464
+ * - RPC communication
465
+ *
466
+ * Design patterns used:
467
+ * - Facade Pattern: Simplifies interface to complex subsystems
468
+ * - Dependency Injection: All dependencies injected via constructor
469
+ * - Factory Pattern: Handlers created via factory functions
470
+ */
225
471
  declare class FrpBridge {
226
- private readonly options;
227
472
  private readonly runtime;
228
473
  private readonly process;
474
+ private readonly mode;
229
475
  private readonly eventSink?;
476
+ private readonly nodeManager?;
477
+ private readonly clientCollector?;
478
+ private readonly rpcServer?;
479
+ private readonly rpcClient?;
230
480
  constructor(options: FrpBridgeOptions);
481
+ /**
482
+ * Execute a command
483
+ */
231
484
  execute<TPayload, TResult = unknown>(command: RuntimeCommand<TPayload>): Promise<CommandResult<TResult>>;
485
+ /**
486
+ * Execute a query
487
+ */
232
488
  query<TPayload, TResult = unknown>(query: RuntimeQuery<TPayload>): Promise<QueryResult<TResult>>;
489
+ /**
490
+ * Get current runtime state snapshot
491
+ */
233
492
  snapshot(): RuntimeState;
493
+ /**
494
+ * Drain and return all pending events
495
+ */
234
496
  drainEvents(): RuntimeEvent[];
235
497
  getProcessManager(): FrpProcessManager;
236
498
  getRuntime(): FrpRuntime;
237
- private createDefaultCommands;
238
- private createDefaultQueries;
499
+ getNodeManager(): NodeManager | undefined;
500
+ getClientCollector(): ClientNodeCollector | undefined;
501
+ getRpcServer(): RpcServer | undefined;
502
+ getRpcClient(): RpcClient | undefined;
503
+ /**
504
+ * Initialize all async components
505
+ */
506
+ initialize(): Promise<void>;
507
+ /**
508
+ * Cleanup and dispose all resources
509
+ */
510
+ dispose(): Promise<void>;
511
+ /**
512
+ * Forward runtime events to external event sink
513
+ */
239
514
  private forwardEvents;
240
- private runConfigMutation;
241
515
  }
242
516
 
243
517
  /**
@@ -313,5 +587,5 @@ declare function parseToml(content: string): Record<string, any>;
313
587
  /** Convert JSON to TOML-like config */
314
588
  declare function toToml(obj: Record<string, any>): string;
315
589
 
316
- export { ARCH_MAP, BINARY_NAMES, ErrorCode, FileSnapshotStorage, FrpBridge, FrpBridgeError, FrpProcessManager, FrpRuntime, GITHUB_OWNER, GITHUB_REPO, OS_MAP, commandExists, downloadFile, ensureDir, executeCommand, getDownloadUrl, getLatestVersion, getPlatform, parseToml, toToml };
317
- export type { Awaitable, CommandHandler, CommandHandlerContext, CommandMetadata, CommandResult, CommandStatus, ConfigSnapshot, FrpBridgeOptions, FrpProcessManagerOptions, NodeInfo, QueryHandler, QueryResult, RuntimeAdapters, RuntimeCommand, RuntimeContext, RuntimeError, RuntimeErrorCode, RuntimeEvent, RuntimeLogger, RuntimeMode, RuntimeQuery, RuntimeState, RuntimeStatus, SnapshotStorage };
590
+ export { ARCH_MAP, BINARY_NAMES, ClientNodeCollector, ErrorCode, FileNodeStorage, FileSnapshotStorage, FrpBridge, FrpBridgeError, FrpProcessManager, FrpRuntime, GITHUB_OWNER, GITHUB_REPO, NodeManager, OS_MAP, RpcClient, RpcServer, commandExists, downloadFile, ensureDir, executeCommand, getDownloadUrl, getLatestVersion, getPlatform, parseToml, toToml };
591
+ export type { Awaitable, ClientCollectorOptions, CommandHandler, CommandHandlerContext, CommandMetadata, CommandResult, CommandStatus, ConfigSnapshot, FrpBridgeOptions, FrpProcessManagerOptions, NodeEvent, NodeInfo, NodeManagerOptions, NodeStorage, QueryHandler, QueryResult, RpcClientOptions, RpcServerOptions, RuntimeAdapters, RuntimeCommand, RuntimeContext, RuntimeError, RuntimeErrorCode, RuntimeEvent, RuntimeLogger, RuntimeMode, RuntimeQuery, RuntimeState, RuntimeStatus, SnapshotStorage };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { ClientConfig, ServerConfig, ProxyConfig } from '@frp-bridge/types';
1
+ import { NodeRegisterPayload, NodeHeartbeatPayload, NodeInfo as NodeInfo$1, NodeListQuery, NodeListResponse, NodeStatistics, TunnelSyncPayload, ProxyConfig, ClientConfig, ServerConfig, RpcRequest } from '@frp-bridge/types';
2
+ import { EventEmitter } from 'node:events';
2
3
 
3
4
  type Awaitable<T> = T | Promise<T>;
4
5
  type RuntimeMode = 'client' | 'server';
@@ -62,7 +63,7 @@ interface RuntimeError {
62
63
  message: string;
63
64
  details?: Record<string, unknown>;
64
65
  }
65
- type RuntimeErrorCode = 'VALIDATION_ERROR' | 'RUNTIME_ERROR' | 'SYSTEM_ERROR';
66
+ type RuntimeErrorCode = 'VALIDATION_ERROR' | 'RUNTIME_ERROR' | 'SYSTEM_ERROR' | 'PORT_CONFLICT' | 'RPC_NOT_AVAILABLE' | 'RPC_ERROR';
66
67
  interface ConfigSnapshot {
67
68
  version: number;
68
69
  checksum: string;
@@ -113,6 +114,153 @@ declare class FrpRuntime {
113
114
  private now;
114
115
  }
115
116
 
117
+ /**
118
+ * Client-side node information collector
119
+ * Gathers system information and reports to server via heartbeat
120
+ */
121
+
122
+ interface ClientCollectorOptions {
123
+ /** Node ID (set by server after registration) */
124
+ nodeId?: string;
125
+ /** Heartbeat interval in milliseconds (default: 30000) */
126
+ heartbeatInterval?: number;
127
+ /** Logger instance */
128
+ logger?: {
129
+ debug?: (msg: string, data?: unknown) => void;
130
+ info?: (msg: string, data?: unknown) => void;
131
+ error?: (msg: string, error?: unknown) => void;
132
+ };
133
+ }
134
+ /**
135
+ * Collects node information on client side
136
+ * Used in client mode to send system info and heartbeat to server
137
+ */
138
+ declare class ClientNodeCollector {
139
+ private nodeId?;
140
+ private heartbeatInterval;
141
+ private logger;
142
+ private heartbeatTimer?;
143
+ constructor(options?: ClientCollectorOptions);
144
+ /** Set node ID after server registration */
145
+ setNodeId(nodeId: string): void;
146
+ /** Collect current node information */
147
+ collectNodeInfo(): Partial<NodeRegisterPayload>;
148
+ /** Collect heartbeat payload */
149
+ collectHeartbeat(): Partial<NodeHeartbeatPayload>;
150
+ /**
151
+ * Start periodic heartbeat collection
152
+ * Callback will be called at each interval with heartbeat payload
153
+ */
154
+ startHeartbeat(callback: (payload: Partial<NodeHeartbeatPayload>) => void, interval?: number): void;
155
+ /** Stop periodic heartbeat collection */
156
+ stopHeartbeat(): void;
157
+ /** Check if heartbeat is running */
158
+ isHeartbeatRunning(): boolean;
159
+ }
160
+
161
+ /**
162
+ * Node Manager for server-side node management
163
+ * Handles node registration, heartbeat, tunnel registry, and queries
164
+ */
165
+
166
+ interface NodeManagerOptions {
167
+ heartbeatTimeout?: number;
168
+ logger?: any;
169
+ }
170
+ interface NodeStorage {
171
+ save: (node: NodeInfo$1) => Promise<void> | void;
172
+ delete: (id: string) => Promise<void> | void;
173
+ load: (id: string) => Promise<NodeInfo$1 | undefined> | NodeInfo$1 | undefined;
174
+ list: () => Promise<NodeInfo$1[]> | NodeInfo$1[];
175
+ }
176
+ type NodeEvent = 'node:registered' | 'node:heartbeat' | 'node:unregistered' | 'node:statusChanged' | 'tunnel:synced';
177
+ /**
178
+ * Manages nodes in server mode
179
+ * Stores node info, handles heartbeat, manages global tunnel registry, emits events
180
+ */
181
+ declare class NodeManager extends EventEmitter {
182
+ private context;
183
+ private nodes;
184
+ private heartbeatTimers;
185
+ private tunnelRegistry;
186
+ private storage?;
187
+ private heartbeatTimeout;
188
+ private logger?;
189
+ constructor(context: RuntimeContext, options?: NodeManagerOptions, storage?: NodeStorage);
190
+ initialize(): Promise<void>;
191
+ /** Register a new node (called when client connects) */
192
+ registerNode(payload: NodeRegisterPayload): Promise<NodeInfo$1>;
193
+ /** Update node heartbeat and status */
194
+ updateHeartbeat(payload: NodeHeartbeatPayload): Promise<void>;
195
+ /** Unregister a node (called when client disconnects) */
196
+ unregisterNode(nodeId: string): Promise<void>;
197
+ /** Get node by id */
198
+ getNode(id: string): Promise<NodeInfo$1 | undefined>;
199
+ /** List nodes with pagination and filtering */
200
+ listNodes(query?: NodeListQuery): Promise<NodeListResponse>;
201
+ /** Get node statistics */
202
+ getStatistics(): Promise<NodeStatistics>;
203
+ /** Check if node exists */
204
+ hasNode(id: string): boolean;
205
+ /** Get all online nodes */
206
+ getOnlineNodes(): NodeInfo$1[];
207
+ /** Get all offline nodes */
208
+ getOfflineNodes(): NodeInfo$1[];
209
+ /** Get nodes by status */
210
+ getNodesByStatus(status: NodeInfo$1['status']): NodeInfo$1[];
211
+ /** Setup heartbeat timer for a node */
212
+ private setupHeartbeatTimer;
213
+ /** Clear heartbeat timer for a node */
214
+ private clearHeartbeatTimer;
215
+ /** Handle heartbeat timeout */
216
+ private handleHeartbeatTimeout;
217
+ /** Sync tunnels for a node (called when node connects or updates tunnels) */
218
+ syncTunnels(payload: TunnelSyncPayload): Promise<void>;
219
+ /** Get tunnels for a specific node */
220
+ getNodeTunnels(nodeId: string): ProxyConfig[];
221
+ /** Get all tunnels across all nodes */
222
+ getAllTunnels(): Map<string, ProxyConfig[]>;
223
+ /** Check if a remotePort is in use across all nodes (for conflict detection) */
224
+ isRemotePortInUse(remotePort: number, excludeNodeId?: string): {
225
+ inUse: boolean;
226
+ nodeId?: string;
227
+ tunnelName?: string;
228
+ };
229
+ /** Clear tunnels for a node (called when node disconnects) */
230
+ private clearNodeTunnels;
231
+ /** Update dispose method to clear tunnels */
232
+ dispose(): Promise<void>;
233
+ }
234
+
235
+ /**
236
+ * File-based node storage implementation
237
+ * Persists node information to disk
238
+ */
239
+
240
+ /**
241
+ * Stores nodes in JSON files
242
+ * Directory structure:
243
+ * ~/.frp-bridge/runtime/nodes/
244
+ * ├── nodes.json (index of all nodes)
245
+ * └── node-{id}.json (individual node data)
246
+ */
247
+ declare class FileNodeStorage implements NodeStorage {
248
+ private storagePath;
249
+ private indexPath;
250
+ private nodeDir;
251
+ constructor(storagePath: string);
252
+ /** Save or update a node */
253
+ save(node: NodeInfo$1): Promise<void>;
254
+ /** Delete a node */
255
+ delete(id: string): Promise<void>;
256
+ /** Load a single node */
257
+ load(id: string): Promise<NodeInfo$1 | undefined>;
258
+ /** Load all nodes */
259
+ list(): Promise<NodeInfo$1[]>;
260
+ /** Update the index of node IDs */
261
+ private updateIndex;
262
+ }
263
+
116
264
  /**
117
265
  * FRP process management utilities
118
266
  */
@@ -120,6 +268,8 @@ declare class FrpRuntime {
120
268
  interface FrpProcessManagerOptions {
121
269
  /** Working directory for FRP files */
122
270
  workDir?: string;
271
+ /** Path to config file (overrides default) */
272
+ configPath?: string;
123
273
  /** FRP version (defaults to latest) */
124
274
  version?: string;
125
275
  /** Mode: client or server */
@@ -144,7 +294,7 @@ interface NodeInfo {
144
294
  /**
145
295
  * Manages FRP client/server lifecycle, config, and tunnels
146
296
  */
147
- declare class FrpProcessManager {
297
+ declare class FrpProcessManager extends EventEmitter {
148
298
  private readonly workDir;
149
299
  private version;
150
300
  private readonly mode;
@@ -153,6 +303,8 @@ declare class FrpProcessManager {
153
303
  private process;
154
304
  private configPath;
155
305
  private binaryPath;
306
+ private uptime;
307
+ private isManualStop;
156
308
  constructor(options: FrpProcessManagerOptions);
157
309
  /** Ensure version is fetched and binary path is set */
158
310
  private ensureVersion;
@@ -171,9 +323,9 @@ declare class FrpProcessManager {
171
323
  /** Return the absolute config file path */
172
324
  getConfigPath(): string;
173
325
  /** Read raw config file contents */
174
- readConfigFile(): string | null;
326
+ getConfigRaw(): string | null;
175
327
  /** Overwrite config file with provided content */
176
- writeConfigFile(content: string): void;
328
+ updateConfigRaw(content: string): void;
177
329
  /** Start FRP process */
178
330
  start(): Promise<void>;
179
331
  /** Stop FRP process */
@@ -190,6 +342,8 @@ declare class FrpProcessManager {
190
342
  removeNode(): void;
191
343
  /** Add tunnel (proxy) */
192
344
  addTunnel(proxy: ProxyConfig): void;
345
+ /** Check if proxy type uses remotePort */
346
+ private typeUsesRemotePort;
193
347
  /** Get tunnel by name */
194
348
  getTunnel(name: string): ProxyConfig | null;
195
349
  /** Update tunnel */
@@ -198,6 +352,70 @@ declare class FrpProcessManager {
198
352
  removeTunnel(name: string): void;
199
353
  /** List all tunnels */
200
354
  listTunnels(): ProxyConfig[];
355
+ /**
356
+ * Query current process status
357
+ */
358
+ queryProcess(): {
359
+ pid: number | undefined;
360
+ uptime: number;
361
+ };
362
+ private setupProcessListeners;
363
+ }
364
+
365
+ interface RpcClientOptions {
366
+ url: string;
367
+ nodeId: string;
368
+ getRegisterPayload: () => Promise<NodeInfo$1> | NodeInfo$1;
369
+ handleRequest: (req: RpcRequest) => Promise<unknown>;
370
+ reconnectInterval?: number;
371
+ logger?: {
372
+ info?: (msg: string, data?: unknown) => void;
373
+ warn?: (msg: string, data?: unknown) => void;
374
+ error?: (msg: string, data?: unknown) => void;
375
+ };
376
+ }
377
+ declare class RpcClient {
378
+ private readonly options;
379
+ private ws;
380
+ private reconnectTimer?;
381
+ private readonly reconnectInterval;
382
+ constructor(options: RpcClientOptions);
383
+ connect(): Promise<void>;
384
+ disconnect(): void;
385
+ private createConnection;
386
+ private handleMessage;
387
+ private handleRpcRequest;
388
+ private send;
389
+ private scheduleReconnect;
390
+ }
391
+
392
+ interface RpcServerOptions {
393
+ port: number;
394
+ heartbeatInterval?: number;
395
+ logger?: {
396
+ info?: (msg: string, data?: unknown) => void;
397
+ warn?: (msg: string, data?: unknown) => void;
398
+ error?: (msg: string, data?: unknown) => void;
399
+ };
400
+ validateToken?: (token: string | undefined, nodeId: string | undefined) => boolean | Promise<boolean>;
401
+ authorize?: (nodeId: string, method: string) => boolean | Promise<boolean>;
402
+ onRegister?: (nodeId: string, payload: NodeInfo$1) => void | Promise<void>;
403
+ }
404
+ declare class RpcServer {
405
+ private readonly options;
406
+ private readonly clients;
407
+ private readonly pendingRequests;
408
+ private readonly wsToNode;
409
+ private heartbeatTimer?;
410
+ private server?;
411
+ constructor(options: RpcServerOptions);
412
+ start(): void;
413
+ stop(): void;
414
+ rpcCall(nodeId: string, method: string, params: Record<string, unknown>, timeout?: number): Promise<unknown>;
415
+ private handleMessage;
416
+ private handleRpcResponse;
417
+ private handleClose;
418
+ private startHeartbeat;
201
419
  }
202
420
 
203
421
  interface FrpBridgeRuntimeOptions {
@@ -210,34 +428,90 @@ interface FrpBridgeRuntimeOptions {
210
428
  }
211
429
  interface FrpBridgeProcessOptions extends Partial<Omit<FrpProcessManagerOptions, 'mode'>> {
212
430
  mode?: 'client' | 'server';
213
- workDir?: string;
214
431
  }
432
+ interface FrpBridgeRpcOptions {
433
+ serverPort?: number;
434
+ serverHeartbeatInterval?: number;
435
+ serverValidateToken?: (token: string | undefined, nodeId: string | undefined) => boolean | Promise<boolean>;
436
+ serverAuthorize?: (nodeId: string, method: string) => boolean | Promise<boolean>;
437
+ clientUrl?: string;
438
+ clientNodeId?: string;
439
+ clientToken?: string;
440
+ clientReconnectInterval?: number;
441
+ getRegisterPayload?: () => Promise<NodeInfo$1> | NodeInfo$1;
442
+ handleRequest?: (req: RpcRequest) => Promise<unknown>;
443
+ }
444
+
215
445
  interface FrpBridgeOptions {
216
446
  mode: 'client' | 'server';
217
447
  workDir?: string;
448
+ configPath?: string;
218
449
  runtime?: FrpBridgeRuntimeOptions;
219
450
  process?: FrpBridgeProcessOptions;
451
+ rpc?: FrpBridgeRpcOptions;
220
452
  storage?: SnapshotStorage;
221
453
  commands?: Record<string, CommandHandler>;
222
454
  queries?: Record<string, QueryHandler>;
223
455
  eventSink?: (event: RuntimeEvent) => void;
224
456
  }
457
+ /**
458
+ * FrpBridge - Main facade class for managing FRP bridge operations.
459
+ *
460
+ * This class serves as a facade that coordinates multiple components:
461
+ * - Runtime management (command/query execution)
462
+ * - Process management (FRP process lifecycle)
463
+ * - Node management (server mode only)
464
+ * - RPC communication
465
+ *
466
+ * Design patterns used:
467
+ * - Facade Pattern: Simplifies interface to complex subsystems
468
+ * - Dependency Injection: All dependencies injected via constructor
469
+ * - Factory Pattern: Handlers created via factory functions
470
+ */
225
471
  declare class FrpBridge {
226
- private readonly options;
227
472
  private readonly runtime;
228
473
  private readonly process;
474
+ private readonly mode;
229
475
  private readonly eventSink?;
476
+ private readonly nodeManager?;
477
+ private readonly clientCollector?;
478
+ private readonly rpcServer?;
479
+ private readonly rpcClient?;
230
480
  constructor(options: FrpBridgeOptions);
481
+ /**
482
+ * Execute a command
483
+ */
231
484
  execute<TPayload, TResult = unknown>(command: RuntimeCommand<TPayload>): Promise<CommandResult<TResult>>;
485
+ /**
486
+ * Execute a query
487
+ */
232
488
  query<TPayload, TResult = unknown>(query: RuntimeQuery<TPayload>): Promise<QueryResult<TResult>>;
489
+ /**
490
+ * Get current runtime state snapshot
491
+ */
233
492
  snapshot(): RuntimeState;
493
+ /**
494
+ * Drain and return all pending events
495
+ */
234
496
  drainEvents(): RuntimeEvent[];
235
497
  getProcessManager(): FrpProcessManager;
236
498
  getRuntime(): FrpRuntime;
237
- private createDefaultCommands;
238
- private createDefaultQueries;
499
+ getNodeManager(): NodeManager | undefined;
500
+ getClientCollector(): ClientNodeCollector | undefined;
501
+ getRpcServer(): RpcServer | undefined;
502
+ getRpcClient(): RpcClient | undefined;
503
+ /**
504
+ * Initialize all async components
505
+ */
506
+ initialize(): Promise<void>;
507
+ /**
508
+ * Cleanup and dispose all resources
509
+ */
510
+ dispose(): Promise<void>;
511
+ /**
512
+ * Forward runtime events to external event sink
513
+ */
239
514
  private forwardEvents;
240
- private runConfigMutation;
241
515
  }
242
516
 
243
517
  /**
@@ -313,5 +587,5 @@ declare function parseToml(content: string): Record<string, any>;
313
587
  /** Convert JSON to TOML-like config */
314
588
  declare function toToml(obj: Record<string, any>): string;
315
589
 
316
- export { ARCH_MAP, BINARY_NAMES, ErrorCode, FileSnapshotStorage, FrpBridge, FrpBridgeError, FrpProcessManager, FrpRuntime, GITHUB_OWNER, GITHUB_REPO, OS_MAP, commandExists, downloadFile, ensureDir, executeCommand, getDownloadUrl, getLatestVersion, getPlatform, parseToml, toToml };
317
- export type { Awaitable, CommandHandler, CommandHandlerContext, CommandMetadata, CommandResult, CommandStatus, ConfigSnapshot, FrpBridgeOptions, FrpProcessManagerOptions, NodeInfo, QueryHandler, QueryResult, RuntimeAdapters, RuntimeCommand, RuntimeContext, RuntimeError, RuntimeErrorCode, RuntimeEvent, RuntimeLogger, RuntimeMode, RuntimeQuery, RuntimeState, RuntimeStatus, SnapshotStorage };
590
+ export { ARCH_MAP, BINARY_NAMES, ClientNodeCollector, ErrorCode, FileNodeStorage, FileSnapshotStorage, FrpBridge, FrpBridgeError, FrpProcessManager, FrpRuntime, GITHUB_OWNER, GITHUB_REPO, NodeManager, OS_MAP, RpcClient, RpcServer, commandExists, downloadFile, ensureDir, executeCommand, getDownloadUrl, getLatestVersion, getPlatform, parseToml, toToml };
591
+ export type { Awaitable, ClientCollectorOptions, CommandHandler, CommandHandlerContext, CommandMetadata, CommandResult, CommandStatus, ConfigSnapshot, FrpBridgeOptions, FrpProcessManagerOptions, NodeEvent, NodeInfo, NodeManagerOptions, NodeStorage, QueryHandler, QueryResult, RpcClientOptions, RpcServerOptions, RuntimeAdapters, RuntimeCommand, RuntimeContext, RuntimeError, RuntimeErrorCode, RuntimeEvent, RuntimeLogger, RuntimeMode, RuntimeQuery, RuntimeState, RuntimeStatus, SnapshotStorage };
package/dist/index.mjs CHANGED
@@ -1,5 +1,3 @@
1
- import{homedir as F}from"node:os";import p from"node:process";import{consola as _}from"consola";import{join as d}from"pathe";import{exec as W,spawn as G}from"node:child_process";import{createWriteStream as H,existsSync as f,mkdirSync as z,chmodSync as Y,readFileSync as m,writeFileSync as O,unlinkSync as Q}from"node:fs";import{get as J}from"node:http";import{get as C}from"node:https";import{promisify as X}from"node:util";import{writeFile as K,readFile as S,readdir as Z}from"node:fs/promises";const P="fatedier",D="frp",g={client:p.platform==="win32"?"frpc.exe":"frpc",server:p.platform==="win32"?"frps.exe":"frps"},I={x64:"amd64",arm64:"arm64",arm:"arm",ia32:"386"},$={win32:"windows",darwin:"darwin",linux:"linux",freebsd:"freebsd"};class u extends Error{constructor(t,e,s){super(t),this.code=e,this.details=s,this.name="FrpBridgeError"}}var c=(r=>(r.BINARY_NOT_FOUND="BINARY_NOT_FOUND",r.DOWNLOAD_FAILED="DOWNLOAD_FAILED",r.EXTRACTION_FAILED="EXTRACTION_FAILED",r.CONFIG_NOT_FOUND="CONFIG_NOT_FOUND",r.CONFIG_INVALID="CONFIG_INVALID",r.PROCESS_ALREADY_RUNNING="PROCESS_ALREADY_RUNNING",r.PROCESS_NOT_RUNNING="PROCESS_NOT_RUNNING",r.PROCESS_START_FAILED="PROCESS_START_FAILED",r.UNSUPPORTED_PLATFORM="UNSUPPORTED_PLATFORM",r.VERSION_FETCH_FAILED="VERSION_FETCH_FAILED",r.MODE_ERROR="MODE_ERROR",r.NOT_FOUND="NOT_FOUND",r))(c||{});const N=X(W);async function A(){const r=`https://api.github.com/repos/${P}/${D}/releases/latest`;return new Promise((t,e)=>{C(r,{headers:{"User-Agent":"frp-bridge"}},s=>{if(s.statusCode!==200){e(new Error(`Failed to fetch latest version: ${s.statusCode}`));return}let n="";s.on("data",i=>n+=i),s.on("end",()=>{try{const i=JSON.parse(n).tag_name?.replace(/^v/,"")||"0.65.0";t(i)}catch(i){e(i)}})}).on("error",e)})}function k(){const r=$[p.platform],t=I[p.arch];if(!r||!t)throw new Error(`Unsupported platform: ${p.platform}-${p.arch}`);return`${r}_${t}`}function U(r,t){const e=t.startsWith("windows_")?"zip":"tar.gz";return`https://github.com/${P}/${D}/releases/download/v${r}/frp_${r}_${t}.${e}`}async function T(r,t){return new Promise((e,s)=>{const n=H(t);(r.startsWith("https")?C:J)(r,i=>{if(i.statusCode===302||i.statusCode===301){const o=i.headers.location;if(o){n.close(),T(o,t).then(e).catch(s);return}}if(i.statusCode!==200){s(new Error(`Failed to download: ${i.statusCode}`));return}i.pipe(n),n.on("finish",()=>{n.close(),e()})}).on("error",i=>{n.close(),s(i)})})}async function b(r){return N(r)}async function R(r){try{return p.platform==="win32"?await N(`where ${r}`):await N(`which ${r}`),!0}catch{return!1}}function l(r){f(r)||z(r,{recursive:!0})}function w(r){const t=r.split(`
2
- `),e={};let s="";for(const n of t){const i=n.trim();if(!i||i.startsWith("#"))continue;if(i.startsWith("[")&&i.endsWith("]")){s=i.slice(1,-1),e[s]||(e[s]={});continue}const o=i.indexOf("=");if(o>0){const h=i.slice(0,o).trim();let a=i.slice(o+1).trim();(a.startsWith('"')&&a.endsWith('"')||a.startsWith("'")&&a.endsWith("'"))&&(a=a.slice(1,-1)),a==="true"?a=!0:a==="false"?a=!1:Number.isNaN(Number(a))||(a=Number(a)),s?e[s][h]=a:e[h]=a}}return e}function E(r){const t=[];for(const[e,s]of Object.entries(r))(typeof s!="object"||s===null||Array.isArray(s))&&t.push(L(e,s));for(const[e,s]of Object.entries(r))if(typeof s=="object"&&s!==null&&!Array.isArray(s)){t.push(""),t.push(`[${e}]`);for(const[n,i]of Object.entries(s))t.push(L(n,i))}return t.join(`
3
- `)}function L(r,t){return typeof t=="string"?`${r} = "${t}"`:typeof t=="boolean"||typeof t=="number"?`${r} = ${t}`:Array.isArray(t)?`${r} = [${t.map(e=>typeof e=="string"?`"${e}"`:e).join(", ")}]`:`${r} = "${String(t)}"`}class M{workDir;version=null;mode;specifiedVersion;logger;process=null;configPath;binaryPath;constructor(t){this.mode=t.mode,this.specifiedVersion=t.version,this.workDir=t.workDir||d(F(),".frp-bridge"),this.logger=t.logger??_.withTag("FrpProcessManager"),l(this.workDir),this.configPath=d(this.workDir,`frp${this.mode==="client"?"c":"s"}.toml`),this.binaryPath=""}async ensureVersion(){if(!this.version){this.version=this.specifiedVersion||await A();const t=this.mode==="client"?g.client:g.server;this.binaryPath=d(this.workDir,"bin",this.version,t)}}async downloadFrpBinary(){await this.ensureVersion();const t=k(),e=U(this.version,t),s=t.startsWith("windows_"),n=s?"zip":"tar.gz",i=d(this.workDir,`frp_${this.version}.${n}`),o=d(this.workDir,"bin",this.version);l(o),await T(e,i);const h=d(this.workDir,"temp");if(l(h),s){if(!await R("unzip"))throw new u("unzip is required for extraction on Windows",c.EXTRACTION_FAILED);await b(`unzip -o "${i}" -d "${h}"`)}else{const j=await R("gzip"),q=await R("tar");if(!j||!q)throw new u("gzip and tar are required for extraction",c.EXTRACTION_FAILED);await b(`tar -xzf "${i}" -C "${h}"`)}const a=d(h,`frp_${this.version}_${t}`),y=d(a,this.mode==="client"?g.client:g.server);if(!f(y))throw new u(`Binary not found: ${y}`,c.BINARY_NOT_FOUND);const v=await import("fs-extra");await v.copy(y,this.binaryPath),s||Y(this.binaryPath,493),await v.remove(i),await v.remove(h)}async updateFrpBinary(t){await this.ensureVersion();const e=t||await A();if(e===this.version)return;if(f(this.binaryPath)){const n=`${this.binaryPath}.bak`;await(await import("fs-extra")).copy(this.binaryPath,n)}this.version=e;const s=this.mode==="client"?g.client:g.server;this.binaryPath=d(this.workDir,"bin",this.version,s),await this.downloadFrpBinary()}hasBinary(){return f(this.binaryPath)}getConfig(){if(!f(this.configPath))return null;const t=m(this.configPath,"utf-8");return w(t)}updateConfig(t){const e={...this.getConfig()||{},...t},s=E(e);O(this.configPath,s,"utf-8")}async backupConfig(){if(!f(this.configPath))throw new u("Config file does not exist",c.CONFIG_NOT_FOUND);const t=Date.now(),e=`${this.configPath}.${t}.bak`;return await(await import("fs-extra")).copy(this.configPath,e),e}getConfigPath(){return this.configPath}readConfigFile(){return f(this.configPath)?m(this.configPath,"utf-8"):null}writeConfigFile(t){l(this.workDir),O(this.configPath,t,"utf-8")}async start(){if(await this.ensureVersion(),this.process)throw new u("Process already running",c.PROCESS_ALREADY_RUNNING);if(this.hasBinary()||await this.downloadFrpBinary(),!f(this.configPath))throw new u("Config file does not exist",c.CONFIG_NOT_FOUND);this.process=G(this.binaryPath,["-c",this.configPath],{stdio:"inherit"}),this.process.on("error",t=>{this.logger.error("FRP process error",{error:t}),this.process=null}),this.process.on("exit",t=>{t!==0&&this.logger.error("FRP process exited with non-zero code",{code:t}),this.process=null})}async stop(){if(this.process)return new Promise(t=>{this.process.on("exit",()=>{this.process=null,t()}),this.process.kill("SIGTERM"),setTimeout(()=>{this.process&&this.process.kill("SIGKILL")},5e3)})}isRunning(){return this.process!==null&&!this.process.killed}addNode(t){if(this.mode!=="client")throw new u("Nodes can only be added in client mode",c.MODE_ERROR);const e=this.getConfig()||{};e.serverAddr=t.serverAddr,e.serverPort=t.serverPort||7e3,t.token&&(e.auth={...e.auth,token:t.token}),t.config&&Object.assign(e,t.config),this.updateConfig(e)}getNode(){if(this.mode!=="client")throw new u("Nodes are only available in client mode",c.MODE_ERROR);const t=this.getConfig();return!t||!t.serverAddr?null:{id:"default",name:"default",serverAddr:t.serverAddr,serverPort:t.serverPort,token:t.auth?.token}}updateNode(t){if(this.mode!=="client")throw new u("Nodes can only be updated in client mode",c.MODE_ERROR);const e=this.getConfig()||{};t.serverAddr&&(e.serverAddr=t.serverAddr),t.serverPort&&(e.serverPort=t.serverPort),t.token&&(e.auth={...e.auth,token:t.token}),t.config&&Object.assign(e,t.config),this.updateConfig(e)}removeNode(){if(this.mode!=="client")throw new u("Nodes can only be removed in client mode",c.MODE_ERROR);f(this.configPath)&&Q(this.configPath)}addTunnel(t){if(this.mode!=="client")throw new Error("Tunnels can only be added in client mode");const e=f(this.configPath)?m(this.configPath,"utf-8"):"",s=E({[t.name]:t}),n=e?`${e}
4
-
5
- ${s}`:s;O(this.configPath,n,"utf-8")}getTunnel(t){if(this.mode!=="client")throw new Error("Tunnels are only available in client mode");if(!f(this.configPath))return null;const e=m(this.configPath,"utf-8");return w(e)[t]||null}updateTunnel(t,e){if(this.mode!=="client")throw new u("Tunnels can only be updated in client mode",c.MODE_ERROR);if(!f(this.configPath))throw new u("Config file does not exist",c.CONFIG_NOT_FOUND);const s=m(this.configPath,"utf-8"),n=w(s);if(!n[t])throw new u(`Tunnel ${t} not found`,c.NOT_FOUND);n[t]={...n[t],...e};const i=E(n);O(this.configPath,i,"utf-8")}removeTunnel(t){if(this.mode!=="client")throw new u("Tunnels can only be removed in client mode",c.MODE_ERROR);if(!f(this.configPath))return;const e=m(this.configPath,"utf-8"),s=w(e);delete s[t];const n=E(s);O(this.configPath,n,"utf-8")}listTunnels(){if(this.mode!=="client")throw new u("Tunnels are only available in client mode",c.MODE_ERROR);if(!f(this.configPath))return[];const t=m(this.configPath,"utf-8"),e=w(t),s=[];for(const[n,i]of Object.entries(e))typeof i=="object"&&i!==null&&"type"in i&&s.push(i);return s}}const tt="Unknown command",et="Unknown query";class x{constructor(t,e={}){this.context=t,this.storage=e.storage,Object.entries(e.commands??{}).forEach(([s,n])=>{this.commandHandlers.set(s,n)}),Object.entries(e.queries??{}).forEach(([s,n])=>{this.queryHandlers.set(s,n)})}storage;commandHandlers=new Map;queryHandlers=new Map;eventBuffer=[];commandQueue=Promise.resolve();state={status:"idle",version:0};registerCommand(t,e){this.commandHandlers.set(t,e)}registerQuery(t,e){this.queryHandlers.set(t,e)}execute(t){const e=this.commandQueue.then(()=>this.runCommand(t));return this.commandQueue=e.then(()=>{},()=>{}),e}async query(t){const e=this.queryHandlers.get(t.name);if(!e)throw this.buildError("VALIDATION_ERROR",`${et}: ${t.name}`);return e(t,this.context)}snapshot(){return{...this.state}}drainEvents(){const t=this.eventBuffer;return this.eventBuffer=[],t}async runCommand(t){const e=this.commandHandlers.get(t.name);if(!e)return{status:"failed",error:this.buildError("VALIDATION_ERROR",`${tt}: ${t.name}`)};const s={...this.state},n={bumped:!1},i={context:this.context,state:s,emit:o=>this.pushEvents(o),requestVersionBump:()=>this.bumpVersion(t.metadata?.author,n)};this.state.status="running";try{const o=await e(t,i);return o.events&&this.pushEvents(o.events),o.snapshot&&await this.persistSnapshot(o.snapshot,t.metadata?.author),o.error?(this.state.lastError=o.error,this.state.status="error"):o.status==="success"&&(this.state.lastError=void 0,this.state.status="running"),{...o,version:o.version??this.state.version}}catch(o){const h=this.normalizeError(o);return this.state.lastError=h,this.state.status="error",{status:"failed",error:h,version:this.state.version}}}pushEvents(t){const e=this.now();t.forEach(s=>{this.eventBuffer.push({...s,timestamp:s.timestamp??e,version:s.version??this.state.version})})}bumpVersion(t,e){return e.bumped?this.state.version:(e.bumped=!0,this.state.version+=1,this.state.lastAppliedAt=this.now(),t&&this.pushEvents([{type:"config:version-bumped",timestamp:this.now(),version:this.state.version,payload:{author:t}}]),this.state.version)}async persistSnapshot(t,e){this.storage&&t&&await this.storage.save({...t,version:t.version??this.state.version,appliedAt:t.appliedAt??this.now(),author:t.author??e})}normalizeError(t){return t&&typeof t=="object"&&"code"in t&&"message"in t?t:{code:"SYSTEM_ERROR",message:t instanceof Error?t.message:"Unknown error"}}buildError(t,e,s){return{code:t,message:e,details:s}}now(){return this.context.clock?this.context.clock():Date.now()}}function B(r){return!!r&&typeof r=="object"&&"version"in r}class V{constructor(t){this.directory=t,l(t)}async save(t){if(typeof t.version!="number")throw new TypeError("Snapshot version must be a number when using FileSnapshotStorage");l(this.directory);const e=JSON.stringify(t,null,2);await K(this.buildPath(t.version),e,"utf-8")}async load(t){const e=this.buildPath(t);if(!f(e))return;const s=await S(e,"utf-8"),n=JSON.parse(s);if(!B(n))throw new TypeError(`Invalid snapshot schema at version ${t}`);return n}async list(){l(this.directory);const t=await Z(this.directory),e=[];for(const s of t){if(!s.endsWith(".json"))continue;const n=await S(d(this.directory,s),"utf-8"),i=JSON.parse(n);B(i)&&e.push(i)}return e.sort((s,n)=>s.version-n.version)}buildPath(t){return d(this.directory,`${t}.json`)}}const st="config.apply",rt="config.applyRaw",nt="process.stop",it="process.status",ot="runtime.snapshot";class at{constructor(t){this.options=t;const e=t.workDir??d(F(),".frp-bridge"),s=t.runtime?.workDir??d(e,"runtime"),n=t.process?.workDir??d(e,"process");l(e),l(s),l(n);const i=t.runtime?.logger??_.withTag("FrpRuntime"),o=t.process?.logger??_.withTag("FrpProcessManager");this.process=new M({mode:t.process?.mode??t.mode,version:t.process?.version,workDir:n,logger:o});const h=t.storage??new V(d(s,"snapshots")),a={id:t.runtime?.id??"default",mode:t.runtime?.mode??t.mode,workDir:s,platform:t.runtime?.platform??p.platform,clock:t.runtime?.clock,logger:i},y={...this.createDefaultCommands(),...t.commands??{}},v={...this.createDefaultQueries(),...t.queries??{}};this.runtime=new x(a,{storage:h,commands:y,queries:v}),this.eventSink=t.eventSink}runtime;process;eventSink;execute(t){return this.runtime.execute(t).finally(()=>{this.forwardEvents()})}query(t){return this.runtime.query(t).finally(()=>{this.forwardEvents()})}snapshot(){return this.runtime.snapshot()}drainEvents(){return this.runtime.drainEvents()}getProcessManager(){return this.process}getRuntime(){return this.runtime}createDefaultCommands(){const t=async(n,i)=>n.payload?.config?this.runConfigMutation(async()=>{this.process.updateConfig(n.payload.config)},n.payload.restart,i):{status:"failed",error:{code:"VALIDATION_ERROR",message:"config.apply requires payload.config"}},e=async(n,i)=>{const o=n.payload?.content;if(!o?.trim())return{status:"failed",error:{code:"VALIDATION_ERROR",message:"config.applyRaw requires payload.content"}};try{w(o)}catch(h){return{status:"failed",error:{code:"VALIDATION_ERROR",message:"config.applyRaw received invalid TOML content",details:h instanceof Error?{message:h.message}:void 0}}}return this.runConfigMutation(async()=>{this.process.writeConfigFile(o)},n.payload?.restart,i)},s=async()=>this.process.isRunning()?(await this.process.stop(),{status:"success",events:[{type:"process:stopped",timestamp:Date.now()}]}):{status:"success"};return{[st]:t,[rt]:e,[nt]:s}}createDefaultQueries(){const t=async()=>{const s=this.runtime.snapshot();return{result:{running:this.process.isRunning(),config:this.process.getConfig()},version:s.version}},e=async()=>{const s=this.runtime.snapshot();return{result:s,version:s.version}};return{[it]:t,[ot]:e}}forwardEvents(){this.eventSink&&this.runtime.drainEvents().forEach(t=>this.eventSink?.(t))}async runConfigMutation(t,e,s){await t();const n=e??!0;let i;return n&&(this.process.isRunning()&&await this.process.stop(),await this.process.start(),i=[{type:"process:started",timestamp:Date.now()}]),s.requestVersionBump(),{status:"success",events:i}}}export{I as ARCH_MAP,g as BINARY_NAMES,c as ErrorCode,V as FileSnapshotStorage,at as FrpBridge,u as FrpBridgeError,M as FrpProcessManager,x as FrpRuntime,P as GITHUB_OWNER,D as GITHUB_REPO,$ as OS_MAP,R as commandExists,T as downloadFile,l as ensureDir,b as executeCommand,U as getDownloadUrl,A as getLatestVersion,k as getPlatform,w as parseToml,E as toToml};
1
+ import{join as l}from"pathe";import{createWriteStream as ie,existsSync as m,mkdirSync as U,chmodSync as ae,readFileSync as v,writeFileSync as N,unlinkSync as ce}from"node:fs";import{writeFile as S,readFile as C,readdir as de,unlink as le}from"node:fs/promises";import{exec as ue,spawn as he}from"node:child_process";import{get as pe}from"node:http";import{get as V}from"node:https";import y from"node:process";import{promisify as me}from"node:util";import{cpus as H,totalmem as q,release as fe,platform as ge,hostname as ye,homedir as B}from"node:os";import{consola as I}from"consola";import{randomUUID as j}from"node:crypto";import{EventEmitter as z}from"node:events";import{WebSocket as E,WebSocketServer as we}from"ws";import{Buffer as T}from"node:buffer";const ve="Unknown command",Re="Unknown query";class W{constructor(e,t={}){this.context=e,this.storage=t.storage,Object.entries(t.commands??{}).forEach(([r,n])=>{this.commandHandlers.set(r,n)}),Object.entries(t.queries??{}).forEach(([r,n])=>{this.queryHandlers.set(r,n)})}storage;commandHandlers=new Map;queryHandlers=new Map;eventBuffer=[];commandQueue=Promise.resolve();state={status:"idle",version:0};registerCommand(e,t){this.commandHandlers.set(e,t)}registerQuery(e,t){this.queryHandlers.set(e,t)}execute(e){const t=this.commandQueue.then(()=>this.runCommand(e));return this.commandQueue=t.then(()=>{},()=>{}),t}async query(e){const t=this.queryHandlers.get(e.name);if(!t)throw this.buildError("VALIDATION_ERROR",`${Re}: ${e.name}`);return t(e,this.context)}snapshot(){return{...this.state}}drainEvents(){const e=this.eventBuffer;return this.eventBuffer=[],e}async runCommand(e){const t=this.commandHandlers.get(e.name);if(!t)return{status:"failed",error:this.buildError("VALIDATION_ERROR",`${ve}: ${e.name}`)};const r={...this.state},n={bumped:!1},o={context:this.context,state:r,emit:i=>this.pushEvents(i),requestVersionBump:()=>this.bumpVersion(e.metadata?.author,n)};this.state.status="running";try{const i=await t(e,o);return i.events&&this.pushEvents(i.events),i.snapshot&&await this.persistSnapshot(i.snapshot,e.metadata?.author),i.error?(this.state.lastError=i.error,this.state.status="error"):i.status==="success"&&(this.state.lastError=void 0,this.state.status="running"),{...i,version:i.version??this.state.version}}catch(i){const c=this.normalizeError(i);return this.state.lastError=c,this.state.status="error",{status:"failed",error:c,version:this.state.version}}}pushEvents(e){const t=this.now();e.forEach(r=>{this.eventBuffer.push({...r,timestamp:r.timestamp??t,version:r.version??this.state.version})})}bumpVersion(e,t){return t.bumped?this.state.version:(t.bumped=!0,this.state.version+=1,this.state.lastAppliedAt=this.now(),e&&this.pushEvents([{type:"config:version-bumped",timestamp:this.now(),version:this.state.version,payload:{author:e}}]),this.state.version)}async persistSnapshot(e,t){this.storage&&e&&await this.storage.save({...e,version:e.version??this.state.version,appliedAt:e.appliedAt??this.now(),author:e.author??t})}normalizeError(e){return e&&typeof e=="object"&&"code"in e&&"message"in e?e:{code:"SYSTEM_ERROR",message:e instanceof Error?e.message:"Unknown error"}}buildError(e,t,r){return{code:e,message:t,details:r}}now(){return this.context.clock?this.context.clock():Date.now()}}const _="fatedier",F="frp",R={client:y.platform==="win32"?"frpc.exe":"frpc",server:y.platform==="win32"?"frps.exe":"frps"},G={x64:"amd64",arm64:"arm64",arm:"arm",ia32:"386"},J={win32:"windows",darwin:"darwin",linux:"linux",freebsd:"freebsd"},M=me(ue);async function A(){const s=`https://api.github.com/repos/${_}/${F}/releases/latest`;return new Promise((e,t)=>{V(s,{headers:{"User-Agent":"frp-bridge"}},r=>{if(r.statusCode!==200){t(new Error(`Failed to fetch latest version: ${r.statusCode}`));return}let n="";r.on("data",o=>n+=o),r.on("end",()=>{try{const o=JSON.parse(n).tag_name?.replace(/^v/,"")||"0.65.0";e(o)}catch(o){t(o)}})}).on("error",t)})}function L(){const s=J[y.platform],e=G[y.arch];if(!s||!e)throw new Error(`Unsupported platform: ${y.platform}-${y.arch}`);return`${s}_${e}`}function k(s,e){const t=e.startsWith("windows_")?"zip":"tar.gz";return`https://github.com/${_}/${F}/releases/download/v${s}/frp_${s}_${e}.${t}`}async function D(s,e){return new Promise((t,r)=>{const n=ie(e);(s.startsWith("https")?V:pe)(s,o=>{if(o.statusCode===302||o.statusCode===301){const i=o.headers.location;if(i){n.close(),D(i,e).then(t).catch(r);return}}if(o.statusCode!==200){r(new Error(`Failed to download: ${o.statusCode}`));return}o.pipe(n),n.on("finish",()=>{n.close(),t()})}).on("error",o=>{n.close(),r(o)})})}async function x(s){return M(s)}async function P(s){try{return y.platform==="win32"?await M(`where ${s}`):await M(`which ${s}`),!0}catch{return!1}}function f(s){m(s)||U(s,{recursive:!0})}function w(s){const e=s.split(`
2
+ `),t={};let r="",n=null;for(const o of e){const i=o.trim();if(!i||i.startsWith("#"))continue;if(i.startsWith("[[")&&i.endsWith("]]")){r=i.slice(2,-2).trim(),Array.isArray(t[r])||(t[r]=[]),n={},t[r].push(n);continue}if(i.startsWith("[")&&i.endsWith("]")){r=i.slice(1,-1).trim(),n=null,t[r]||(t[r]={});continue}const c=i.indexOf("=");if(c>0){const p=i.slice(0,c).trim();let a=i.slice(c+1).trim();(a.startsWith('"')&&a.endsWith('"')||a.startsWith("'")&&a.endsWith("'"))&&(a=a.slice(1,-1)),a==="true"?a=!0:a==="false"?a=!1:Number.isNaN(Number(a))||(a=Number(a)),r?n?n[p]=a:t[r][p]=a:t[p]=a}}return t}function O(s){const e=[];for(const[t,r]of Object.entries(s))(typeof r!="object"||r===null)&&e.push($(t,r));for(const[t,r]of Object.entries(s))if(Array.isArray(r)&&r.length>0&&typeof r[0]=="object"&&r[0]!==null){e.push("");for(const n of r){e.push(`[[${t}]]`);for(const[o,i]of Object.entries(n))e.push($(o,i));e.push("")}}for(const[t,r]of Object.entries(s))if(typeof r=="object"&&r!==null&&!Array.isArray(r)){e.push(""),e.push(`[${t}]`);for(const[n,o]of Object.entries(r))e.push($(n,o))}return e.join(`
3
+ `).trim()}function $(s,e){return typeof e=="string"?`${s} = "${e}"`:typeof e=="boolean"||typeof e=="number"?`${s} = ${e}`:Array.isArray(e)?`${s} = [${e.map(t=>typeof t=="string"?`"${t}"`:t).join(", ")}]`:`${s} = "${String(e)}"`}const Ie={__proto__:null,commandExists:P,downloadFile:D,ensureDir:f,executeCommand:x,getDownloadUrl:k,getLatestVersion:A,getPlatform:L,parseToml:w,toToml:O};function Q(s){return!!s&&typeof s=="object"&&"version"in s}class Y{constructor(e){this.directory=e,f(e)}async save(e){if(typeof e.version!="number")throw new TypeError("Snapshot version must be a number when using FileSnapshotStorage");f(this.directory);const t=JSON.stringify(e,null,2);await S(this.buildPath(e.version),t,"utf-8")}async load(e){const t=this.buildPath(e);if(!m(t))return;const r=await C(t,"utf-8"),n=JSON.parse(r);if(!Q(n))throw new TypeError(`Invalid snapshot schema at version ${e}`);return n}async list(){f(this.directory);const e=await de(this.directory),t=[];for(const r of e){if(!r.endsWith(".json"))continue;const n=await C(l(this.directory,r),"utf-8"),o=JSON.parse(n);Q(o)&&t.push(o)}return t.sort((r,n)=>r.version-n.version)}buildPath(e){return l(this.directory,`${e}.json`)}}function X(s){return["tcp","udp","stcp","xtcp","sudp","tcpmux"].includes(s)}async function K(s,e,t,r){await e();const n=t??!0;let o;return n&&(s.isRunning()&&await s.stop(),await s.start(),o=[{type:"process:started",timestamp:Date.now()}]),r.requestVersionBump(),{status:"success",events:o}}function Te(s){return async(e,t)=>e.payload?.config?K(s.process,async()=>{s.process.updateConfig(e.payload.config)},e.payload.restart,t):{status:"failed",error:{code:"VALIDATION_ERROR",message:"config.apply requires payload.config"}}}function Oe(s){return async(e,t)=>{const r=e.payload?.content;if(!r?.trim())return{status:"failed",error:{code:"VALIDATION_ERROR",message:"config.applyRaw requires payload.content"}};try{const{parseToml:n}=await Promise.resolve().then(function(){return Ie});n(r)}catch(n){return{status:"failed",error:{code:"VALIDATION_ERROR",message:"config.applyRaw received invalid TOML content",details:n instanceof Error?{message:n.message}:void 0}}}return K(s.process,async()=>{s.process.updateConfigRaw(r)},e.payload?.restart,t)}}function be(s){return async()=>s.process.isRunning()?(await s.process.stop(),{status:"success",events:[{type:"process:stopped",timestamp:Date.now()}]}):{status:"success"}}function Ne(s){return async e=>{if(!s.nodeManager)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"node.register is only available in server mode"}};const t=e.payload;if(!t||!t.hostname||!t.serverAddr||!t.serverPort)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"node.register requires hostname, serverAddr, and serverPort"}};try{return{status:"success",result:await s.nodeManager.registerNode(t)}}catch(r){return{status:"failed",error:{code:"VALIDATION_ERROR",message:r instanceof Error?r.message:"Failed to register node"}}}}}function Ce(s){return async e=>{if(!s.nodeManager)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"node.heartbeat is only available in server mode"}};const t=e.payload;if(!t||!t.nodeId)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"node.heartbeat requires nodeId"}};try{return await s.nodeManager.updateHeartbeat(t),{status:"success"}}catch(r){return{status:"failed",error:{code:"VALIDATION_ERROR",message:r instanceof Error?r.message:"Failed to process heartbeat"}}}}}function Pe(s){return async e=>{if(!s.nodeManager)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"node.unregister is only available in server mode"}};const t=e.payload?.nodeId;if(!t)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"node.unregister requires nodeId"}};try{return s.nodeManager.unregisterNode(t),{status:"success"}}catch(r){return{status:"failed",error:{code:"VALIDATION_ERROR",message:r instanceof Error?r.message:"Failed to unregister node"}}}}}function Ee(s){return async e=>{const t=e.payload;if(!t||!t.proxy)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"proxy.add requires payload.proxy"}};if(s.mode==="server"){if(!t.nodeId)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"proxy.add requires payload.nodeId in server mode"}};const r=t.proxy.remotePort;if(r&&X(t.proxy.type)){const n=s.nodeManager?.isRemotePortInUse(r,t.nodeId);if(n?.inUse)return{status:"failed",error:{code:"PORT_CONFLICT",message:`Remote port ${r} is already in use by tunnel "${n.tunnelName}" on node ${n.nodeId}`}}}if(!s.rpcServer)return{status:"failed",error:{code:"RPC_NOT_AVAILABLE",message:"RPC server not available"}};try{return{status:"success",result:await s.rpcServer.rpcCall(t.nodeId,"proxy.add",{proxy:t.proxy})}}catch(n){return{status:"failed",error:{code:"RPC_ERROR",message:n instanceof Error?n.message:"Failed to add tunnel on node"}}}}try{return s.process.addTunnel(t.proxy),{status:"success",result:t.proxy}}catch(r){return{status:"failed",error:{code:"RUNTIME_ERROR",message:r instanceof Error?r.message:"Failed to add tunnel"}}}}}function Ae(s){return async e=>{const t=e.payload;if(!t||!t.name)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"proxy.update requires payload.name"}};if(s.mode==="server"){if(!t.nodeId)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"proxy.update requires payload.nodeId in server mode"}};const r=t.proxy?.remotePort;if(r&&X(t.proxy?.type)){const n=s.nodeManager?.isRemotePortInUse(r,t.nodeId);if(n?.inUse)return{status:"failed",error:{code:"PORT_CONFLICT",message:`Remote port ${r} is already in use by tunnel "${n.tunnelName}" on node ${n.nodeId}`}}}if(!s.rpcServer)return{status:"failed",error:{code:"RPC_NOT_AVAILABLE",message:"RPC server not available"}};try{return{status:"success",result:await s.rpcServer.rpcCall(t.nodeId,"proxy.update",{name:t.name,proxy:t.proxy})}}catch(n){return{status:"failed",error:{code:"RPC_ERROR",message:n instanceof Error?n.message:"Failed to update tunnel on node"}}}}try{return s.process.updateTunnel(t.name,t.proxy),{status:"success",result:{name:t.name,...t.proxy}}}catch(r){return{status:"failed",error:{code:"RUNTIME_ERROR",message:r instanceof Error?r.message:"Failed to update tunnel"}}}}}function De(s){return async e=>{const t=e.payload;if(!t||!t.name)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"proxy.remove requires payload.name"}};if(s.mode==="server"){if(!t.nodeId)return{status:"failed",error:{code:"VALIDATION_ERROR",message:"proxy.remove requires payload.nodeId in server mode"}};if(!s.rpcServer)return{status:"failed",error:{code:"RPC_NOT_AVAILABLE",message:"RPC server not available"}};try{return{status:"success",result:await s.rpcServer.rpcCall(t.nodeId,"proxy.remove",{name:t.name})}}catch(r){return{status:"failed",error:{code:"RPC_ERROR",message:r instanceof Error?r.message:"Failed to remove tunnel on node"}}}}try{return s.process.removeTunnel(t.name),{status:"success",result:{name:t.name}}}catch(r){return{status:"failed",error:{code:"RUNTIME_ERROR",message:r instanceof Error?r.message:"Failed to remove tunnel"}}}}}function xe(s){return{"config.apply":Te(s),"config.applyRaw":Oe(s),"process.stop":be(s),"node.register":Ne(s),"node.heartbeat":Ce(s),"node.unregister":Pe(s),"proxy.add":Ee(s),"proxy.update":Ae(s),"proxy.remove":De(s)}}function Se(s){return async()=>{const e=s.runtime.snapshot();return{result:{running:s.process.isRunning(),config:s.process.getConfig()},version:e.version}}}function _e(s){return async()=>{const e=s.runtime.snapshot();return{result:e,version:e.version}}}function Fe(s){return async()=>{if(!s.nodeManager)return{result:{items:[],total:0,page:1,pageSize:100,hasMore:!1},version:s.runtime.snapshot().version};const e={page:1,pageSize:100};return{result:s.nodeManager.listNodes(e),version:s.runtime.snapshot().version}}}function Me(s){return async e=>{if(!s.nodeManager)return{result:null,version:s.runtime.snapshot().version};const t=e.payload?.nodeId;return t?{result:s.nodeManager.getNode(t)??null,version:s.runtime.snapshot().version}:{result:null,version:s.runtime.snapshot().version}}}function Le(s){return async()=>s.nodeManager?{result:s.nodeManager.getStatistics(),version:s.runtime.snapshot().version}:{result:{total:0,online:0,offline:0,connecting:0,error:0},version:s.runtime.snapshot().version}}function ke(s){return async()=>{if(s.mode!=="client")return{result:[],version:s.runtime.snapshot().version};try{return{result:s.process.listTunnels(),version:s.runtime.snapshot().version}}catch{return{result:[],version:s.runtime.snapshot().version}}}}function $e(s){return async e=>{if(s.mode!=="client")return{result:null,version:s.runtime.snapshot().version};const t=e.payload?.name;if(!t)return{result:null,version:s.runtime.snapshot().version};try{return{result:s.process.getTunnel(t)??null,version:s.runtime.snapshot().version}}catch{return{result:null,version:s.runtime.snapshot().version}}}}function Ue(s){return{"process.status":Se(s),"runtime.snapshot":_e(s),"node.list":Fe(s),"node.get":Me(s),"node.statistics":Le(s),"proxy.list":ke(s),"proxy.get":$e(s)}}class Z{nodeId;heartbeatInterval;logger;heartbeatTimer;constructor(e={}){this.nodeId=e.nodeId,this.heartbeatInterval=e.heartbeatInterval??3e4,this.logger=e.logger}setNodeId(e){this.nodeId=e}collectNodeInfo(){const e=H(),t=q();return{hostname:ye(),osType:ge(),osRelease:fe(),cpuCores:e.length,memTotal:t,protocol:"tcp",serverAddr:"",serverPort:0}}collectHeartbeat(){if(!this.nodeId)throw new Error("Node ID not set. Call setNodeId() first or wait for registration.");const e=H(),t=q();return{nodeId:this.nodeId,status:"online",lastHeartbeat:Date.now(),cpuCores:e.length,memTotal:t}}startHeartbeat(e,t){if(this.heartbeatTimer){this.logger?.debug?.("Heartbeat already running");return}const r=t??this.heartbeatInterval;try{const n=this.collectHeartbeat();e(n)}catch(n){this.logger?.error?.("Failed to collect initial heartbeat",n)}this.heartbeatTimer=setInterval(()=>{try{const n=this.collectHeartbeat();e(n)}catch(n){this.logger?.error?.("Failed to collect heartbeat",n)}},r),this.logger?.info?.(`Heartbeat started with interval ${r}ms`)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=void 0,this.logger?.info?.("Heartbeat stopped"))}isHeartbeatRunning(){return!!this.heartbeatTimer}}class ee{constructor(e){this.storagePath=e,this.nodeDir=e,this.indexPath=l(e,"nodes.json"),m(this.nodeDir)||U(this.nodeDir,{recursive:!0})}indexPath;nodeDir;async save(e){const t=l(this.nodeDir,`node-${e.id}.json`);await S(t,JSON.stringify(e,null,2),"utf-8"),await this.updateIndex(e.id,!0)}async delete(e){const t=l(this.nodeDir,`node-${e}.json`);try{await le(t)}catch{}await this.updateIndex(e,!1)}async load(e){const t=l(this.nodeDir,`node-${e}.json`);try{const r=await C(t,"utf-8");return JSON.parse(r)}catch{return}}async list(){try{const e=await C(this.indexPath,"utf-8"),t=JSON.parse(e),r=[];for(const n of t)try{const o=await this.load(n);o&&r.push(o)}catch{}return r}catch{return[]}}async updateIndex(e,t){let r=[];try{const n=await C(this.indexPath,"utf-8");r=JSON.parse(n)}catch{}t?r.includes(e)||r.push(e):r=r.filter(n=>n!==e),await S(this.indexPath,JSON.stringify(r,null,2),"utf-8")}}class te extends z{constructor(e,t={},r){super(),this.context=e,this.heartbeatTimeout=t.heartbeatTimeout??9e4,this.logger=t.logger,this.storage=r}nodes=new Map;heartbeatTimers=new Map;tunnelRegistry=new Map;storage;heartbeatTimeout;logger;async initialize(){if(this.storage)try{const e=await this.storage.list();for(const t of e)this.nodes.set(t.id,t),this.setupHeartbeatTimer(t.id);this.logger?.info?.(`Loaded ${e.length} nodes from storage`)}catch(e){this.logger?.error?.("Failed to load nodes from storage",{error:e})}}async registerNode(e){const t=Date.now(),r=j(),n={id:r,ip:e.ip,port:e.port,protocol:e.protocol,serverAddr:e.serverAddr,serverPort:e.serverPort,hostname:e.hostname,osType:e.osType,osRelease:e.osRelease,platform:e.platform,cpuCores:e.cpuCores,memTotal:e.memTotal,frpVersion:e.frpVersion,bridgeVersion:e.bridgeVersion,token:e.token,status:"online",connectedAt:t,lastHeartbeat:t,createdAt:t,updatedAt:t};if(this.nodes.set(r,n),this.setupHeartbeatTimer(r),this.storage)try{await this.storage.save(n)}catch(o){this.logger?.error?.("Failed to save node",{nodeId:r,error:o})}return this.emit("node:registered",{type:"node:registered",timestamp:t,payload:{nodeId:r,nodeInfo:n}}),n}async updateHeartbeat(e){const t=this.nodes.get(e.nodeId);if(!t)return;const r=t.status,n=Date.now();if(t.status=e.status,t.lastHeartbeat=n,t.updatedAt=n,e.cpuCores!==void 0&&(t.cpuCores=e.cpuCores),e.memTotal!==void 0&&(t.memTotal=e.memTotal),this.setupHeartbeatTimer(e.nodeId),this.storage)try{await this.storage.save(t)}catch(o){this.logger?.error?.("Failed to save node heartbeat",{nodeId:e.nodeId,error:o})}this.emit("node:heartbeat",{type:"node:heartbeat",timestamp:n,payload:{nodeId:e.nodeId}}),r!==e.status&&this.emit("node:statusChanged",{type:"node:statusChanged",timestamp:n,payload:{nodeId:e.nodeId,oldStatus:r,newStatus:e.status}})}async unregisterNode(e){if(!this.nodes.get(e))return;const t=Date.now();if(this.nodes.delete(e),this.clearHeartbeatTimer(e),this.clearNodeTunnels(e),this.storage)try{await this.storage.delete(e)}catch(r){this.logger?.error?.("Failed to delete node",{nodeId:e,error:r})}this.emit("node:unregistered",{type:"node:unregistered",timestamp:t,payload:{nodeId:e}})}async getNode(e){return this.nodes.get(e)}async listNodes(e){const t=e?.page??1,r=e?.pageSize??20,n=e?.status,o=e?.search?.toLowerCase();let i=Array.from(this.nodes.values());n&&(i=i.filter(d=>d.status===n)),e?.labels&&(i=i.filter(d=>d.labels?Object.entries(e.labels).every(([g,b])=>d.labels?.[g]===b):!1)),o&&(i=i.filter(d=>d.hostname?.toLowerCase().includes(o)||d.ip.toLowerCase().includes(o)||d.id.toLowerCase().includes(o)));const c=i.length,p=(t-1)*r,a=p+r;return{items:i.slice(p,a),total:c,page:t,pageSize:r,hasMore:a<c}}async getStatistics(){const e=Array.from(this.nodes.values());return{total:e.length,online:e.filter(t=>t.status==="online").length,offline:e.filter(t=>t.status==="offline").length,connecting:e.filter(t=>t.status==="connecting").length,error:e.filter(t=>t.status==="error").length}}hasNode(e){return this.nodes.has(e)}getOnlineNodes(){return Array.from(this.nodes.values()).filter(e=>e.status==="online")}getOfflineNodes(){return Array.from(this.nodes.values()).filter(e=>e.status==="offline")}getNodesByStatus(e){return Array.from(this.nodes.values()).filter(t=>t.status===e)}setupHeartbeatTimer(e){this.clearHeartbeatTimer(e);const t=setTimeout(()=>{this.handleHeartbeatTimeout(e)},this.heartbeatTimeout);this.heartbeatTimers.set(e,t)}clearHeartbeatTimer(e){const t=this.heartbeatTimers.get(e);t&&(clearTimeout(t),this.heartbeatTimers.delete(e))}async handleHeartbeatTimeout(e){const t=this.nodes.get(e);if(!t)return;const r=t.status;if(t.status="offline",t.updatedAt=Date.now(),this.storage)try{await this.storage.save(t)}catch(n){this.logger?.error?.("Failed to save node after timeout",{nodeId:e,error:n})}this.emit("node:statusChanged",{type:"node:statusChanged",timestamp:Date.now(),payload:{nodeId:e,oldStatus:r,newStatus:"offline",reason:"heartbeat_timeout"}})}async syncTunnels(e){const{nodeId:t,tunnels:r,timestamp:n}=e,o=this.nodes.get(t);if(!o){this.logger?.warn?.("Tunnel sync failed: node not found",{nodeId:t});return}if(this.tunnelRegistry.set(t,r),o.tunnels=r,o.updatedAt=n,this.storage)try{await this.storage.save(o)}catch(i){this.logger?.error?.("Failed to save node after tunnel sync",{nodeId:t,error:i})}this.emit("tunnel:synced",{type:"tunnel:synced",timestamp:Date.now(),payload:{nodeId:t,tunnelCount:r.length}}),this.logger?.info?.("Tunnels synced for node",{nodeId:t,tunnelCount:r.length})}getNodeTunnels(e){return this.tunnelRegistry.get(e)||[]}getAllTunnels(){return new Map(this.tunnelRegistry)}isRemotePortInUse(e,t){for(const[r,n]of this.tunnelRegistry.entries())if(!(t&&r===t))for(const o of n){const i=o.remotePort;if(i&&i===e)return{inUse:!0,nodeId:r,tunnelName:o.name}}return{inUse:!1}}clearNodeTunnels(e){this.tunnelRegistry.delete(e),this.logger?.info?.("Cleared tunnels for node",{nodeId:e})}async dispose(){for(const e of this.heartbeatTimers.values())clearTimeout(e);this.heartbeatTimers.clear(),this.tunnelRegistry.clear()}}class h extends Error{constructor(e,t,r){super(e),this.code=t,this.details=r,this.name="FrpBridgeError"}}var u=(s=>(s.BINARY_NOT_FOUND="BINARY_NOT_FOUND",s.DOWNLOAD_FAILED="DOWNLOAD_FAILED",s.EXTRACTION_FAILED="EXTRACTION_FAILED",s.CONFIG_NOT_FOUND="CONFIG_NOT_FOUND",s.CONFIG_INVALID="CONFIG_INVALID",s.PROCESS_ALREADY_RUNNING="PROCESS_ALREADY_RUNNING",s.PROCESS_NOT_RUNNING="PROCESS_NOT_RUNNING",s.PROCESS_START_FAILED="PROCESS_START_FAILED",s.UNSUPPORTED_PLATFORM="UNSUPPORTED_PLATFORM",s.VERSION_FETCH_FAILED="VERSION_FETCH_FAILED",s.MODE_ERROR="MODE_ERROR",s.NOT_FOUND="NOT_FOUND",s))(u||{});class re extends z{workDir;version=null;mode;specifiedVersion;logger;process=null;configPath;binaryPath;uptime=null;isManualStop=!1;constructor(e){super(),this.mode=e.mode,this.specifiedVersion=e.version,this.workDir=e.workDir||l(B(),".frp-bridge"),this.configPath=e.configPath||l(this.workDir,`frp${this.mode==="client"?"c":"s"}.toml`),this.logger=e.logger??I.withTag("FrpProcessManager"),f(this.workDir),this.binaryPath=""}async ensureVersion(){if(!this.version){this.version=this.specifiedVersion||await A();const e=this.mode==="client"?R.client:R.server;this.binaryPath=l(this.workDir,"bin",this.version,e)}}async downloadFrpBinary(){await this.ensureVersion();const e=L(),t=k(this.version,e),r=e.startsWith("windows_"),n=r?"zip":"tar.gz",o=l(this.workDir,`frp_${this.version}.${n}`),i=l(this.workDir,"bin",this.version);f(i),await D(t,o);const c=l(this.workDir,"temp");if(f(c),r){if(!await P("unzip"))throw new h("unzip is required for extraction on Windows",u.EXTRACTION_FAILED);await x(`unzip -o "${o}" -d "${c}"`)}else{const g=await P("gzip"),b=await P("tar");if(!g||!b)throw new h("gzip and tar are required for extraction",u.EXTRACTION_FAILED);await x(`tar -xzf "${o}" -C "${c}"`)}const p=l(c,`frp_${this.version}_${e}`),a=l(p,this.mode==="client"?R.client:R.server);if(!m(a))throw new h(`Binary not found: ${a}`,u.BINARY_NOT_FOUND);const d=await import("fs-extra");await d.copy(a,this.binaryPath),r||ae(this.binaryPath,493),await d.remove(o),await d.remove(c)}async updateFrpBinary(e){await this.ensureVersion();const t=e||await A();if(t===this.version)return;if(m(this.binaryPath)){const n=`${this.binaryPath}.bak`;await(await import("fs-extra")).copy(this.binaryPath,n)}this.version=t;const r=this.mode==="client"?R.client:R.server;this.binaryPath=l(this.workDir,"bin",this.version,r),await this.downloadFrpBinary()}hasBinary(){return m(this.binaryPath)}getConfig(){if(!m(this.configPath))return null;const e=v(this.configPath,"utf-8");return w(e)}updateConfig(e){const t={...this.getConfig(),...e},r=O(t);N(this.configPath,r,"utf-8")}async backupConfig(){if(!m(this.configPath))throw new h("Config file does not exist",u.CONFIG_NOT_FOUND);const e=Date.now(),t=`${this.configPath}.${e}.bak`;return await(await import("fs-extra")).copy(this.configPath,t),t}getConfigPath(){return this.configPath}getConfigRaw(){return m(this.configPath)?v(this.configPath,"utf-8"):null}updateConfigRaw(e){const t=this.configPath.includes("/")||this.configPath.includes("\\")?this.configPath.substring(0,Math.max(this.configPath.lastIndexOf("/"),this.configPath.lastIndexOf("\\"))):this.workDir;f(t),N(this.configPath,e,"utf-8")}async start(){if(await this.ensureVersion(),this.isRunning()&&await this.stop(),this.hasBinary()||await this.downloadFrpBinary(),!m(this.configPath))throw new h("Config file does not exist",u.CONFIG_NOT_FOUND);this.process=he(this.binaryPath,["-c",this.configPath],{stdio:"inherit"}),this.uptime=Date.now(),this.isManualStop=!1,this.setupProcessListeners(),this.emit("process:started",{type:"process:started",timestamp:Date.now(),payload:{pid:this.process?.pid,uptime:0}})}async stop(){if(!this.process)return;this.isManualStop=!0;const e=this.process;return new Promise(t=>{const r=()=>{const n=this.uptime?Date.now()-this.uptime:void 0;this.emit("process:stopped",{type:"process:stopped",timestamp:Date.now(),payload:{uptime:n}}),this.uptime=null,t()};e.exitCode===null?(e.once("exit",r),e.kill("SIGTERM"),setTimeout(()=>{e.exitCode===null&&(this.logger.warn("Process did not exit gracefully, forcing kill"),e.kill("SIGKILL"))},5e3)):r()}).finally(()=>{this.process=null})}isRunning(){if(!this.process)return!1;const e=this.process.exitCode===null&&this.process.signalCode===null;return e||(this.process=null),e}addNode(e){if(this.mode!=="client")throw new h("Nodes can only be added in client mode",u.MODE_ERROR);const t=this.getConfig()||{};t.serverAddr=e.serverAddr,t.serverPort=e.serverPort||7e3,e.token&&(t.auth={...t.auth,token:e.token}),e.config&&Object.assign(t,e.config),this.updateConfig(t)}getNode(){if(this.mode!=="client")throw new h("Nodes are only available in client mode",u.MODE_ERROR);const e=this.getConfig();return!e||!e.serverAddr?null:{id:"default",name:"default",serverAddr:e.serverAddr,serverPort:e.serverPort,token:e.auth?.token}}updateNode(e){if(this.mode!=="client")throw new h("Nodes can only be updated in client mode",u.MODE_ERROR);const t=this.getConfig()||{};e.serverAddr&&(t.serverAddr=e.serverAddr),e.serverPort&&(t.serverPort=e.serverPort),e.token&&(t.auth={...t.auth,token:e.token}),e.config&&Object.assign(t,e.config),this.updateConfig(t)}removeNode(){if(this.mode!=="client")throw new h("Nodes can only be removed in client mode",u.MODE_ERROR);m(this.configPath)&&ce(this.configPath)}addTunnel(e){if(this.mode!=="client")throw new Error("Tunnels can only be added in client mode");const t=m(this.configPath)?v(this.configPath,"utf-8"):"",r=t?w(t):{};if(Array.isArray(r.proxies)||(r.proxies=[]),r.proxies.findIndex(i=>i&&i.name===e.name)!==-1)throw new h(`Tunnel ${e.name} already exists`,u.CONFIG_INVALID);const n=e.remotePort;if(n&&this.typeUsesRemotePort(e.type)&&r.proxies.some(i=>{const c=i.remotePort;return i&&c===n&&this.typeUsesRemotePort(i.type)}))throw new h(`Remote port ${n} is already in use`,u.CONFIG_INVALID);r.proxies.push(e);const o=O(r);N(this.configPath,o,"utf-8")}typeUsesRemotePort(e){return["tcp","udp","stcp","xtcp","sudp","tcpmux"].includes(e)}getTunnel(e){if(this.mode!=="client")throw new Error("Tunnels are only available in client mode");if(!m(this.configPath))return null;const t=v(this.configPath,"utf-8"),r=w(t);return Array.isArray(r.proxies)?r.proxies.find(n=>n&&n.name===e)||null:r[e]||null}updateTunnel(e,t){if(this.mode!=="client")throw new h("Tunnels can only be updated in client mode",u.MODE_ERROR);if(!m(this.configPath))throw new h("Config file does not exist",u.CONFIG_NOT_FOUND);const r=v(this.configPath,"utf-8"),n=w(r);if(Array.isArray(n.proxies)){const i=n.proxies.findIndex(d=>d&&d.name===e);if(i===-1)throw new h(`Tunnel ${e} not found`,u.NOT_FOUND);const c=n.proxies[i],p={...c,...t},a=t.remotePort;if(a&&a!==c.remotePort&&this.typeUsesRemotePort(p.type)&&n.proxies.some((d,g)=>{if(g===i)return!1;const b=d.remotePort;return d&&b===a&&this.typeUsesRemotePort(d.type)}))throw new h(`Remote port ${a} is already in use`,u.CONFIG_INVALID);n.proxies[i]=p}else if(n[e])n[e]={...n[e],...t};else throw new h(`Tunnel ${e} not found`,u.NOT_FOUND);const o=O(n);N(this.configPath,o,"utf-8")}removeTunnel(e){if(this.mode!=="client")throw new h("Tunnels can only be removed in client mode",u.MODE_ERROR);if(!m(this.configPath))return;const t=v(this.configPath,"utf-8"),r=w(t);if(Array.isArray(r.proxies)){const o=r.proxies.findIndex(i=>i&&i.name===e);o!==-1&&r.proxies.splice(o,1)}else r[e]&&delete r[e];const n=O(r);N(this.configPath,n,"utf-8")}listTunnels(){if(this.mode!=="client")throw new h("Tunnels are only available in client mode",u.MODE_ERROR);if(!m(this.configPath))return this.logger.warn?.("Config file does not exist",{path:this.configPath}),[];const e=v(this.configPath,"utf-8"),t=w(e);this.logger.info?.("listTunnels - parsed config:",{hasProxies:"proxies"in t,isArray:Array.isArray(t.proxies),length:t.proxies?.length,proxies:t.proxies});const r=[];if(Array.isArray(t.proxies))for(const o of t.proxies)o&&typeof o=="object"&&"type"in o&&r.push(o);const n=new Set(r.map(o=>o.name));for(const[o,i]of Object.entries(t))if(o!=="proxies"&&typeof i=="object"&&i!==null&&"type"in i&&!Array.isArray(i)){const c={...i,name:i.name||o};n.has(c.name)||(r.push(c),n.add(c.name))}return this.logger.info?.("listTunnels - result:",{tunnelCount:r.length,tunnels:r}),r}queryProcess(){const e=this.uptime?Date.now()-this.uptime:0;return{pid:this.process?.pid,uptime:e}}setupProcessListeners(){this.process&&(this.process.on("exit",(e,t)=>{const r=this.uptime?Date.now()-this.uptime:void 0;this.isManualStop||this.emit("process:exited",{type:"process:exited",timestamp:Date.now(),payload:{code:e??void 0,signal:t??void 0,uptime:r}}),this.process=null,this.uptime=null}),this.process.on("error",e=>{this.emit("process:error",{type:"process:error",timestamp:Date.now(),payload:{error:e.message,pid:this.process?.pid}}),this.logger.error("FRP process error",{error:e})}))}}function Ve(s){return typeof s=="string"?s:T.isBuffer(s)?s.toString():s instanceof ArrayBuffer?T.from(new Uint8Array(s)).toString():Array.isArray(s)?T.concat(s.map(e=>T.isBuffer(e)?e:T.from(e))).toString():T.from(s).toString()}function se(s,e){try{const t=Ve(s);return JSON.parse(t)}catch(t){e?.warn?.("parse message failed",t);return}}class ne{constructor(e){this.options=e,this.reconnectInterval=e.reconnectInterval??5e3}ws=null;reconnectTimer;reconnectInterval;async connect(){await this.createConnection()}disconnect(){this.reconnectTimer&&clearTimeout(this.reconnectTimer),this.reconnectTimer=void 0,this.ws?.close(),this.ws=null}async createConnection(){return new Promise((e,t)=>{const r=new E(this.options.url);this.ws=r,r.on("open",async()=>{try{const n=await this.options.getRegisterPayload();this.send({type:"register",nodeId:this.options.nodeId,payload:n}),e()}catch(n){this.options.logger?.error?.("rpc client register failed",n),t(n)}}),r.on("message",n=>{this.handleMessage(n).catch(o=>{this.options.logger?.error?.("rpc client handle message failed",o)})}),r.on("close",()=>{this.scheduleReconnect()}),r.on("error",n=>{this.options.logger?.warn?.("rpc client socket error",n),this.scheduleReconnect(),t(n)})})}async handleMessage(e){const t=se(e,this.options.logger);if(t){if(t.type==="ping"){this.send({type:"pong",timestamp:Date.now()});return}t.method&&await this.handleRpcRequest(t)}}async handleRpcRequest(e){try{const t=await this.options.handleRequest(e);this.send({id:e.id,status:"success",result:t})}catch(t){this.send({id:e.id,status:"error",error:{code:"EXECUTION_ERROR",message:t instanceof Error?t.message:"Unknown error"}})}}send(e){this.ws?.readyState===E.OPEN&&this.ws.send(JSON.stringify(e))}scheduleReconnect(){this.reconnectTimer||(this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=void 0,this.createConnection().catch(e=>{this.options.logger?.error?.("rpc client reconnect failed",e),this.scheduleReconnect()})},this.reconnectInterval))}}class oe{constructor(e){this.options=e}clients=new Map;pendingRequests=new Map;wsToNode=new Map;heartbeatTimer;server;start(){this.server||(this.server=new we({port:this.options.port}),this.server.on("connection",(e,t)=>{const r=new URL(t.url??"/","ws://localhost").searchParams.get("token")??void 0;e.on("message",n=>{this.handleMessage(e,n,r).catch(o=>{this.options.logger?.error?.("rpc server handle message failed",o)})}),e.on("close",()=>{this.handleClose(e)})}),this.startHeartbeat(),this.options.logger?.info?.("RpcServer started",{port:this.options.port}))}stop(){this.heartbeatTimer&&clearInterval(this.heartbeatTimer),this.heartbeatTimer=void 0,this.pendingRequests.forEach(e=>clearTimeout(e.timer)),this.pendingRequests.clear(),this.clients.forEach(e=>e.close()),this.clients.clear(),this.wsToNode.clear(),this.server?.close(),this.server=void 0}async rpcCall(e,t,r,n=3e4){const o=this.clients.get(e);if(!o||o.readyState!==E.OPEN)throw new Error("Client not connected");if(this.options.authorize&&!await this.options.authorize(e,t))throw new Error("UNAUTHORIZED");const i=j(),c={id:i,method:t,params:r,timeout:n};return new Promise((p,a)=>{const d=setTimeout(()=>{this.pendingRequests.delete(i),a(new Error(`RPC timeout: ${t}`))},n);this.pendingRequests.set(i,{resolve:p,reject:a,timer:d}),o.send(JSON.stringify(c))})}async handleMessage(e,t,r){const n=se(t,this.options.logger);if(n){if(n.type==="register"){const o=n.nodeId;if(!o){e.close();return}if(!(!this.options.validateToken||await this.options.validateToken(r,o))){e.close();return}this.clients.set(o,e),this.wsToNode.set(e,o);const i=n.payload;i&&this.options.onRegister&&await this.options.onRegister(o,i);return}n.type!=="pong"&&n.id&&n.status&&this.handleRpcResponse(n)}}handleRpcResponse(e){const t=this.pendingRequests.get(e.id);t&&(clearTimeout(t.timer),this.pendingRequests.delete(e.id),e.status==="success"?t.resolve(e.result):t.reject(new Error(e.error?.message??"RPC error")))}handleClose(e){const t=this.wsToNode.get(e);t&&(this.clients.delete(t),this.wsToNode.delete(e))}startHeartbeat(){const e=this.options.heartbeatInterval??3e4;this.heartbeatTimer=setInterval(()=>{this.clients.forEach((t,r)=>{if(t.readyState===E.OPEN){const n={type:"ping",timestamp:Date.now()};t.send(JSON.stringify(n))}else this.clients.delete(r)})},e)}}class He{constructor(e){this.config=e}initialize(){const{rootWorkDir:e,runtimeDir:t,processDir:r}=this.setupDirectories(),n=this.createLoggers(),o=this.createProcessManager(r,n.processLogger),i=this.createRuntimeContext(t,n.runtimeLogger),c=this.createNodeManager(i,t,n.runtimeLogger),p=this.createClientCollector(),{rpcServer:a,rpcClient:d}=this.createRpcComponents();return{runtimeContext:i,process:o,nodeManager:c,clientCollector:p,rpcServer:a,rpcClient:d,rootWorkDir:e,runtimeDir:t,processDir:r}}setupDirectories(){const e=this.config.workDir??l(B(),".frp-bridge"),t=this.config.runtime?.workDir??l(e,"runtime"),r=this.config.process?.workDir??l(e,"process");return f(e),f(t),f(r),{rootWorkDir:e,runtimeDir:t,processDir:r}}createLoggers(){const e=this.config.runtime?.logger??I.withTag("FrpRuntime"),t=this.config.process?.logger??I.withTag("FrpProcessManager");return{runtimeLogger:e,processLogger:t}}createProcessManager(e,t){return new re({mode:this.config.process?.mode??this.config.mode,version:this.config.process?.version,workDir:e,configPath:this.config.configPath,logger:t})}createRuntimeContext(e,t){return{id:this.config.runtime?.id??"default",mode:this.config.runtime?.mode??this.config.mode,workDir:e,platform:this.config.runtime?.platform??y.platform,clock:this.config.runtime?.clock,logger:t}}createNodeManager(e,t,r){if(this.config.mode!=="server")return;const n=l(t,"nodes");f(n);const o=new ee(n);return new te(e,{heartbeatTimeout:9e4,logger:r},o)}createClientCollector(){if(this.config.mode==="client")return new Z({heartbeatInterval:3e4,logger:I.withTag("ClientNodeCollector")})}createRpcComponents(){const e=this.config.rpc,t={};if(this.config.mode==="server"&&e?.serverPort&&(t.rpcServer=new oe({port:e.serverPort,heartbeatInterval:e.serverHeartbeatInterval,validateToken:e.serverValidateToken,authorize:e.serverAuthorize,logger:I.withTag("RpcServer")})),this.config.mode==="client"&&e?.clientUrl&&e.clientNodeId){const r=this.appendToken(e.clientUrl,e.clientToken);t.rpcClient=new ne({url:r,nodeId:e.clientNodeId,reconnectInterval:e.clientReconnectInterval,getRegisterPayload:e.getRegisterPayload??(async()=>{throw new Error("rpc getRegisterPayload is required in client mode")}),handleRequest:e.handleRequest??(async()=>{}),logger:I.withTag("RpcClient")})}return t}appendToken(e,t){if(!t)return e;const r=new URL(e);return r.searchParams.set("token",t),r.toString()}}function qe(s,e){e&&(s.on("process:started",t=>{e(t)}),s.on("process:stopped",t=>{e(t)}),s.on("process:exited",t=>{e(t)}),s.on("process:error",t=>{e(t)}))}class Be{runtime;process;mode;eventSink;nodeManager;clientCollector;rpcServer;rpcClient;constructor(e){this.mode=e.mode;const t=new He(e).initialize();this.process=t.process,this.nodeManager=t.nodeManager,this.clientCollector=t.clientCollector,this.rpcServer=t.rpcServer,this.rpcClient=t.rpcClient;const r=e.storage??new Y(l(t.runtimeDir,"snapshots"));this.runtime=new W(t.runtimeContext,{storage:r,commands:{},queries:{}});const n={process:this.process,nodeManager:this.nodeManager,rpcServer:this.rpcServer,mode:this.mode},o={process:this.process,nodeManager:this.nodeManager,runtime:this.runtime,mode:this.mode},i=xe(n),c=Ue(o),p={...i,...e.commands??{}},a={...c,...e.queries??{}};Object.entries(p).forEach(([d,g])=>{this.runtime.registerCommand(d,g)}),Object.entries(a).forEach(([d,g])=>{this.runtime.registerQuery(d,g)}),this.eventSink=e.eventSink,qe(this.process,this.eventSink)}execute(e){return this.runtime.execute(e).finally(()=>{this.forwardEvents()})}query(e){return this.runtime.query(e).finally(()=>{this.forwardEvents()})}snapshot(){return this.runtime.snapshot()}drainEvents(){return this.runtime.drainEvents()}getProcessManager(){return this.process}getRuntime(){return this.runtime}getNodeManager(){return this.nodeManager}getClientCollector(){return this.clientCollector}getRpcServer(){return this.rpcServer}getRpcClient(){return this.rpcClient}async initialize(){this.nodeManager&&await this.nodeManager.initialize(),this.rpcServer&&this.rpcServer.start(),this.rpcClient&&await this.rpcClient.connect()}async dispose(){this.nodeManager&&this.nodeManager.dispose(),this.clientCollector&&this.clientCollector.stopHeartbeat(),this.rpcServer&&this.rpcServer.stop(),this.rpcClient&&this.rpcClient.disconnect()}forwardEvents(){this.eventSink&&this.runtime.drainEvents().forEach(e=>this.eventSink?.(e))}}export{G as ARCH_MAP,R as BINARY_NAMES,Z as ClientNodeCollector,u as ErrorCode,ee as FileNodeStorage,Y as FileSnapshotStorage,Be as FrpBridge,h as FrpBridgeError,re as FrpProcessManager,W as FrpRuntime,_ as GITHUB_OWNER,F as GITHUB_REPO,te as NodeManager,J as OS_MAP,ne as RpcClient,oe as RpcServer,P as commandExists,D as downloadFile,f as ensureDir,x as executeCommand,k as getDownloadUrl,A as getLatestVersion,L as getPlatform,w as parseToml,O as toToml};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frp-bridge/core",
3
3
  "type": "module",
4
- "version": "0.0.1",
4
+ "version": "0.0.2",
5
5
  "description": "Frp bridge core",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/frp-web/bridge#readme",
@@ -24,17 +24,17 @@
24
24
  "dist"
25
25
  ],
26
26
  "dependencies": {
27
- "axios": "^1.12.2",
28
- "consola": "^3.2.3",
27
+ "axios": "^1.13.2",
28
+ "consola": "^3.4.2",
29
29
  "execa": "^9.6.0",
30
30
  "fs-extra": "^11.3.2",
31
- "ini": "^5.0.0",
32
31
  "pathe": "^2.0.3",
33
- "@frp-bridge/types": "0.0.1"
32
+ "ws": "^8.18.0",
33
+ "@frp-bridge/types": "0.0.2"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/fs-extra": "^11.0.4",
37
- "@types/node": "^24.7.0"
37
+ "@types/ws": "^8.5.14"
38
38
  },
39
39
  "scripts": {
40
40
  "stub": "unbuild --stub",