@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 +286 -12
- package/dist/index.d.ts +286 -12
- package/dist/index.mjs +3 -5
- package/package.json +6 -6
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ClientConfig, ServerConfig,
|
|
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
|
-
|
|
326
|
+
getConfigRaw(): string | null;
|
|
175
327
|
/** Overwrite config file with provided content */
|
|
176
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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,
|
|
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
|
-
|
|
326
|
+
getConfigRaw(): string | null;
|
|
175
327
|
/** Overwrite config file with provided content */
|
|
176
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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{
|
|
2
|
-
`),
|
|
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.
|
|
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.
|
|
28
|
-
"consola": "^3.2
|
|
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
|
-
"
|
|
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/
|
|
37
|
+
"@types/ws": "^8.5.14"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"stub": "unbuild --stub",
|