@f2a/network 0.1.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.
Files changed (234) hide show
  1. package/.github/workflows/ci.yml +113 -0
  2. package/.github/workflows/publish.yml +60 -0
  3. package/LICENSE +21 -0
  4. package/MONOREPO.md +58 -0
  5. package/README.md +280 -0
  6. package/SKILL.md +137 -0
  7. package/dist/adapters/openclaw.d.ts +103 -0
  8. package/dist/adapters/openclaw.d.ts.map +1 -0
  9. package/dist/adapters/openclaw.js +297 -0
  10. package/dist/adapters/openclaw.js.map +1 -0
  11. package/dist/cli/commands.d.ts +17 -0
  12. package/dist/cli/commands.d.ts.map +1 -0
  13. package/dist/cli/commands.js +107 -0
  14. package/dist/cli/commands.js.map +1 -0
  15. package/dist/cli/index.d.ts +6 -0
  16. package/dist/cli/index.d.ts.map +1 -0
  17. package/dist/cli/index.js +203 -0
  18. package/dist/cli/index.js.map +1 -0
  19. package/dist/core/autonomous-economy.d.ts +136 -0
  20. package/dist/core/autonomous-economy.d.ts.map +1 -0
  21. package/dist/core/autonomous-economy.js +255 -0
  22. package/dist/core/autonomous-economy.js.map +1 -0
  23. package/dist/core/connection-manager.d.ts +80 -0
  24. package/dist/core/connection-manager.d.ts.map +1 -0
  25. package/dist/core/connection-manager.js +235 -0
  26. package/dist/core/connection-manager.js.map +1 -0
  27. package/dist/core/connection-manager.test.d.ts +2 -0
  28. package/dist/core/connection-manager.test.d.ts.map +1 -0
  29. package/dist/core/connection-manager.test.js +52 -0
  30. package/dist/core/connection-manager.test.js.map +1 -0
  31. package/dist/core/e2ee-crypto.d.ts +90 -0
  32. package/dist/core/e2ee-crypto.d.ts.map +1 -0
  33. package/dist/core/e2ee-crypto.js +190 -0
  34. package/dist/core/e2ee-crypto.js.map +1 -0
  35. package/dist/core/f2a.d.ts +126 -0
  36. package/dist/core/f2a.d.ts.map +1 -0
  37. package/dist/core/f2a.js +425 -0
  38. package/dist/core/f2a.js.map +1 -0
  39. package/dist/core/identity.d.ts +47 -0
  40. package/dist/core/identity.d.ts.map +1 -0
  41. package/dist/core/identity.js +130 -0
  42. package/dist/core/identity.js.map +1 -0
  43. package/dist/core/identity.test.d.ts +2 -0
  44. package/dist/core/identity.test.d.ts.map +1 -0
  45. package/dist/core/identity.test.js +43 -0
  46. package/dist/core/identity.test.js.map +1 -0
  47. package/dist/core/p2p-network.d.ts +242 -0
  48. package/dist/core/p2p-network.d.ts.map +1 -0
  49. package/dist/core/p2p-network.js +1182 -0
  50. package/dist/core/p2p-network.js.map +1 -0
  51. package/dist/core/reputation-security.d.ts +168 -0
  52. package/dist/core/reputation-security.d.ts.map +1 -0
  53. package/dist/core/reputation-security.js +369 -0
  54. package/dist/core/reputation-security.js.map +1 -0
  55. package/dist/core/reputation.d.ts +179 -0
  56. package/dist/core/reputation.d.ts.map +1 -0
  57. package/dist/core/reputation.js +472 -0
  58. package/dist/core/reputation.js.map +1 -0
  59. package/dist/core/review-committee.d.ts +130 -0
  60. package/dist/core/review-committee.d.ts.map +1 -0
  61. package/dist/core/review-committee.js +251 -0
  62. package/dist/core/review-committee.js.map +1 -0
  63. package/dist/core/serverless.d.ts +155 -0
  64. package/dist/core/serverless.d.ts.map +1 -0
  65. package/dist/core/serverless.js +615 -0
  66. package/dist/core/serverless.js.map +1 -0
  67. package/dist/core/token-manager.d.ts +42 -0
  68. package/dist/core/token-manager.d.ts.map +1 -0
  69. package/dist/core/token-manager.js +122 -0
  70. package/dist/core/token-manager.js.map +1 -0
  71. package/dist/daemon/control-server.d.ts +55 -0
  72. package/dist/daemon/control-server.d.ts.map +1 -0
  73. package/dist/daemon/control-server.js +262 -0
  74. package/dist/daemon/control-server.js.map +1 -0
  75. package/dist/daemon/index.d.ts +35 -0
  76. package/dist/daemon/index.d.ts.map +1 -0
  77. package/dist/daemon/index.js +69 -0
  78. package/dist/daemon/index.js.map +1 -0
  79. package/dist/daemon/main.d.ts +6 -0
  80. package/dist/daemon/main.d.ts.map +1 -0
  81. package/dist/daemon/main.js +38 -0
  82. package/dist/daemon/main.js.map +1 -0
  83. package/dist/daemon/start.d.ts +6 -0
  84. package/dist/daemon/start.d.ts.map +1 -0
  85. package/dist/daemon/start.js +25 -0
  86. package/dist/daemon/start.js.map +1 -0
  87. package/dist/daemon/webhook.d.ts +30 -0
  88. package/dist/daemon/webhook.d.ts.map +1 -0
  89. package/dist/daemon/webhook.js +86 -0
  90. package/dist/daemon/webhook.js.map +1 -0
  91. package/dist/daemon/webhook.test.d.ts +2 -0
  92. package/dist/daemon/webhook.test.d.ts.map +1 -0
  93. package/dist/daemon/webhook.test.js +24 -0
  94. package/dist/daemon/webhook.test.js.map +1 -0
  95. package/dist/index.d.ts +24 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +25 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/protocol/messages.d.ts +739 -0
  100. package/dist/protocol/messages.d.ts.map +1 -0
  101. package/dist/protocol/messages.js +188 -0
  102. package/dist/protocol/messages.js.map +1 -0
  103. package/dist/protocol/messages.test.d.ts +2 -0
  104. package/dist/protocol/messages.test.d.ts.map +1 -0
  105. package/dist/protocol/messages.test.js +55 -0
  106. package/dist/protocol/messages.test.js.map +1 -0
  107. package/dist/types/index.d.ts +247 -0
  108. package/dist/types/index.d.ts.map +1 -0
  109. package/dist/types/index.js +10 -0
  110. package/dist/types/index.js.map +1 -0
  111. package/dist/types/result.d.ts +28 -0
  112. package/dist/types/result.d.ts.map +1 -0
  113. package/dist/types/result.js +16 -0
  114. package/dist/types/result.js.map +1 -0
  115. package/dist/utils/benchmark.d.ts +67 -0
  116. package/dist/utils/benchmark.d.ts.map +1 -0
  117. package/dist/utils/benchmark.js +179 -0
  118. package/dist/utils/benchmark.js.map +1 -0
  119. package/dist/utils/logger.d.ts +105 -0
  120. package/dist/utils/logger.d.ts.map +1 -0
  121. package/dist/utils/logger.js +275 -0
  122. package/dist/utils/logger.js.map +1 -0
  123. package/dist/utils/middleware.d.ts +85 -0
  124. package/dist/utils/middleware.d.ts.map +1 -0
  125. package/dist/utils/middleware.js +173 -0
  126. package/dist/utils/middleware.js.map +1 -0
  127. package/dist/utils/rate-limiter.d.ts +71 -0
  128. package/dist/utils/rate-limiter.d.ts.map +1 -0
  129. package/dist/utils/rate-limiter.js +160 -0
  130. package/dist/utils/rate-limiter.js.map +1 -0
  131. package/dist/utils/signature.d.ts +57 -0
  132. package/dist/utils/signature.d.ts.map +1 -0
  133. package/dist/utils/signature.js +102 -0
  134. package/dist/utils/signature.js.map +1 -0
  135. package/dist/utils/validation.d.ts +504 -0
  136. package/dist/utils/validation.d.ts.map +1 -0
  137. package/dist/utils/validation.js +159 -0
  138. package/dist/utils/validation.js.map +1 -0
  139. package/docs/F2A-PROTOCOL.md +61 -0
  140. package/docs/MOBILE_BOOTSTRAP_DESIGN.md +126 -0
  141. package/docs/a2a-lessons.md +316 -0
  142. package/docs/middleware-guide.md +448 -0
  143. package/docs/readme-update-checklist.md +90 -0
  144. package/docs/reputation-guide.md +396 -0
  145. package/docs/rfcs/001-reputation-system.md +712 -0
  146. package/docs/security-design.md +247 -0
  147. package/install.sh +231 -0
  148. package/package.json +64 -0
  149. package/packages/openclaw-adapter/README.md +510 -0
  150. package/packages/openclaw-adapter/openclaw.plugin.json +106 -0
  151. package/packages/openclaw-adapter/package.json +40 -0
  152. package/packages/openclaw-adapter/src/announcement-queue.test.ts +449 -0
  153. package/packages/openclaw-adapter/src/announcement-queue.ts +403 -0
  154. package/packages/openclaw-adapter/src/capability-detector.test.ts +99 -0
  155. package/packages/openclaw-adapter/src/capability-detector.ts +183 -0
  156. package/packages/openclaw-adapter/src/claim-handlers.test.ts +974 -0
  157. package/packages/openclaw-adapter/src/claim-handlers.ts +482 -0
  158. package/packages/openclaw-adapter/src/connector.business.test.ts +583 -0
  159. package/packages/openclaw-adapter/src/connector.ts +795 -0
  160. package/packages/openclaw-adapter/src/index.test.ts +82 -0
  161. package/packages/openclaw-adapter/src/index.ts +18 -0
  162. package/packages/openclaw-adapter/src/integration.e2e.test.ts +829 -0
  163. package/packages/openclaw-adapter/src/logger.ts +51 -0
  164. package/packages/openclaw-adapter/src/network-client.test.ts +266 -0
  165. package/packages/openclaw-adapter/src/network-client.ts +251 -0
  166. package/packages/openclaw-adapter/src/network-recovery.test.ts +465 -0
  167. package/packages/openclaw-adapter/src/node-manager.test.ts +136 -0
  168. package/packages/openclaw-adapter/src/node-manager.ts +429 -0
  169. package/packages/openclaw-adapter/src/plugin.test.ts +439 -0
  170. package/packages/openclaw-adapter/src/plugin.ts +104 -0
  171. package/packages/openclaw-adapter/src/reputation.test.ts +221 -0
  172. package/packages/openclaw-adapter/src/reputation.ts +368 -0
  173. package/packages/openclaw-adapter/src/task-guard.test.ts +502 -0
  174. package/packages/openclaw-adapter/src/task-guard.ts +860 -0
  175. package/packages/openclaw-adapter/src/task-queue.concurrency.test.ts +462 -0
  176. package/packages/openclaw-adapter/src/task-queue.edge-cases.test.ts +284 -0
  177. package/packages/openclaw-adapter/src/task-queue.persistence.test.ts +408 -0
  178. package/packages/openclaw-adapter/src/task-queue.ts +668 -0
  179. package/packages/openclaw-adapter/src/tool-handlers.test.ts +906 -0
  180. package/packages/openclaw-adapter/src/tool-handlers.ts +574 -0
  181. package/packages/openclaw-adapter/src/types.ts +361 -0
  182. package/packages/openclaw-adapter/src/webhook-pusher.test.ts +188 -0
  183. package/packages/openclaw-adapter/src/webhook-pusher.ts +220 -0
  184. package/packages/openclaw-adapter/src/webhook-server.test.ts +580 -0
  185. package/packages/openclaw-adapter/src/webhook-server.ts +202 -0
  186. package/packages/openclaw-adapter/tsconfig.json +20 -0
  187. package/src/cli/commands.test.ts +157 -0
  188. package/src/cli/commands.ts +129 -0
  189. package/src/cli/index.test.ts +77 -0
  190. package/src/cli/index.ts +234 -0
  191. package/src/core/autonomous-economy.test.ts +291 -0
  192. package/src/core/autonomous-economy.ts +428 -0
  193. package/src/core/e2ee-crypto.test.ts +125 -0
  194. package/src/core/e2ee-crypto.ts +246 -0
  195. package/src/core/f2a.test.ts +269 -0
  196. package/src/core/f2a.ts +618 -0
  197. package/src/core/p2p-network.test.ts +199 -0
  198. package/src/core/p2p-network.ts +1432 -0
  199. package/src/core/reputation-security.test.ts +403 -0
  200. package/src/core/reputation-security.ts +562 -0
  201. package/src/core/reputation.test.ts +260 -0
  202. package/src/core/reputation.ts +576 -0
  203. package/src/core/review-committee.test.ts +380 -0
  204. package/src/core/review-committee.ts +401 -0
  205. package/src/core/token-manager.test.ts +133 -0
  206. package/src/core/token-manager.ts +140 -0
  207. package/src/daemon/control-server.test.ts +216 -0
  208. package/src/daemon/control-server.ts +292 -0
  209. package/src/daemon/index.test.ts +85 -0
  210. package/src/daemon/index.ts +89 -0
  211. package/src/daemon/main.ts +44 -0
  212. package/src/daemon/start.ts +29 -0
  213. package/src/daemon/webhook.test.ts +68 -0
  214. package/src/daemon/webhook.ts +105 -0
  215. package/src/index.test.ts +436 -0
  216. package/src/index.ts +72 -0
  217. package/src/types/index.test.ts +87 -0
  218. package/src/types/index.ts +341 -0
  219. package/src/types/result.ts +68 -0
  220. package/src/utils/benchmark.ts +237 -0
  221. package/src/utils/logger.ts +331 -0
  222. package/src/utils/middleware.ts +229 -0
  223. package/src/utils/rate-limiter.ts +207 -0
  224. package/src/utils/signature.ts +136 -0
  225. package/src/utils/validation.ts +186 -0
  226. package/tests/docker/Dockerfile.node +23 -0
  227. package/tests/docker/Dockerfile.runner +18 -0
  228. package/tests/docker/docker-compose.test.yml +73 -0
  229. package/tests/integration/message-passing.test.ts +109 -0
  230. package/tests/integration/multi-node.test.ts +92 -0
  231. package/tests/integration/p2p-connection.test.ts +83 -0
  232. package/tests/integration/test-config.ts +32 -0
  233. package/tsconfig.json +21 -0
  234. package/vitest.config.ts +26 -0
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Webhook Server
3
+ * 接收 F2A Node 的事件通知
4
+ */
5
+
6
+ import { createServer, IncomingMessage, ServerResponse } from 'http';
7
+ import type {
8
+ WebhookEvent,
9
+ DiscoverWebhookPayload,
10
+ DelegateWebhookPayload,
11
+ AgentCapability,
12
+ TaskResponse
13
+ } from './types.js';
14
+ import { webhookLogger as logger } from './logger.js';
15
+
16
+ /** 默认请求体大小限制 (64KB) - 元数据交换足够,防止 DoS */
17
+ const DEFAULT_MAX_BODY_SIZE = 64 * 1024;
18
+
19
+ export interface WebhookHandler {
20
+ onDiscover(payload: DiscoverWebhookPayload): Promise<{
21
+ capabilities: AgentCapability[];
22
+ reputation?: number;
23
+ }>;
24
+
25
+ onDelegate(payload: DelegateWebhookPayload): Promise<{
26
+ accepted: boolean;
27
+ taskId: string;
28
+ reason?: string;
29
+ }>;
30
+
31
+ onStatus(): Promise<{
32
+ status: 'available' | 'busy' | 'offline';
33
+ load?: number;
34
+ }>;
35
+ }
36
+
37
+ export class WebhookServer {
38
+ private port: number;
39
+ private handler: WebhookHandler;
40
+ private server?: ReturnType<typeof createServer>;
41
+ private maxBodySize: number;
42
+ /** 允许的 CORS 来源列表 */
43
+ private allowedOrigins: string[];
44
+
45
+ constructor(port: number, handler: WebhookHandler, options?: {
46
+ maxBodySize?: number;
47
+ allowedOrigins?: string[];
48
+ }) {
49
+ this.port = port;
50
+ this.handler = handler;
51
+ this.maxBodySize = options?.maxBodySize || DEFAULT_MAX_BODY_SIZE;
52
+ // 默认只允许 localhost
53
+ this.allowedOrigins = options?.allowedOrigins ?? ['http://localhost'];
54
+ }
55
+
56
+ /**
57
+ * 启动 Webhook 服务器
58
+ */
59
+ async start(): Promise<void> {
60
+ return new Promise((resolve, reject) => {
61
+ this.server = createServer(this.handleRequest.bind(this));
62
+
63
+ this.server.listen(this.port, () => {
64
+ logger.info('服务器启动在端口 %d', this.port);
65
+ resolve();
66
+ });
67
+
68
+ this.server.on('error', (err) => {
69
+ reject(err);
70
+ });
71
+ });
72
+ }
73
+
74
+ /**
75
+ * 停止 Webhook 服务器
76
+ */
77
+ async stop(): Promise<void> {
78
+ if (this.server) {
79
+ return new Promise((resolve) => {
80
+ this.server?.close(() => {
81
+ logger.info('服务器已停止');
82
+ resolve();
83
+ });
84
+ });
85
+ }
86
+ }
87
+
88
+ /**
89
+ * 处理 HTTP 请求
90
+ */
91
+ private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
92
+ // 设置 CORS - 使用配置的允许来源
93
+ const origin = req.headers.origin;
94
+ // 当 allowedOrigins 为空数组时,使用默认值 'http://localhost'
95
+ // 当 origin 不在允许列表中时,使用第一个允许的来源或默认值
96
+ const defaultOrigin = 'http://localhost';
97
+ let allowOrigin: string;
98
+
99
+ if (this.allowedOrigins.length === 0) {
100
+ // 没有配置允许来源,使用默认值
101
+ allowOrigin = defaultOrigin;
102
+ } else if (origin && this.allowedOrigins.includes(origin)) {
103
+ // origin 在允许列表中
104
+ allowOrigin = origin;
105
+ } else {
106
+ // origin 不在允许列表中,使用第一个允许的来源
107
+ allowOrigin = this.allowedOrigins[0];
108
+ }
109
+
110
+ res.setHeader('Access-Control-Allow-Origin', allowOrigin);
111
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
112
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
113
+
114
+ if (req.method === 'OPTIONS') {
115
+ res.writeHead(200);
116
+ res.end();
117
+ return;
118
+ }
119
+
120
+ if (req.method !== 'POST') {
121
+ res.writeHead(405, { 'Content-Type': 'application/json' });
122
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
123
+ return;
124
+ }
125
+
126
+ try {
127
+ const body = await this.parseBody(req);
128
+ const event = body as WebhookEvent;
129
+
130
+ logger.info('收到事件: %s', event.type);
131
+
132
+ let result: unknown;
133
+
134
+ switch (event.type) {
135
+ case 'discover':
136
+ result = await this.handler.onDiscover(event.payload as DiscoverWebhookPayload);
137
+ break;
138
+
139
+ case 'delegate':
140
+ result = await this.handler.onDelegate(event.payload as DelegateWebhookPayload);
141
+ break;
142
+
143
+ case 'status':
144
+ result = await this.handler.onStatus();
145
+ break;
146
+
147
+ default:
148
+ res.writeHead(400, { 'Content-Type': 'application/json' });
149
+ res.end(JSON.stringify({ error: `Unknown event type: ${event.type}` }));
150
+ return;
151
+ }
152
+
153
+ res.writeHead(200, { 'Content-Type': 'application/json' });
154
+ res.end(JSON.stringify(result));
155
+
156
+ } catch (error) {
157
+ logger.error('处理错误: %s', error);
158
+ res.writeHead(500, { 'Content-Type': 'application/json' });
159
+ res.end(JSON.stringify({
160
+ error: error instanceof Error ? error.message : 'Internal error'
161
+ }));
162
+ }
163
+ }
164
+
165
+ /**
166
+ * 解析请求体
167
+ * 带大小限制,防止 DoS 攻击
168
+ */
169
+ private parseBody(req: IncomingMessage): Promise<unknown> {
170
+ return new Promise((resolve, reject) => {
171
+ let body = '';
172
+ let size = 0;
173
+
174
+ req.on('data', (chunk) => {
175
+ size += chunk.length;
176
+
177
+ // 检查请求体大小
178
+ if (size > this.maxBodySize) {
179
+ req.destroy();
180
+ reject(new Error(`Request body too large: ${size} bytes (max: ${this.maxBodySize})`));
181
+ return;
182
+ }
183
+
184
+ body += chunk.toString();
185
+ });
186
+
187
+ req.on('end', () => {
188
+ try {
189
+ resolve(JSON.parse(body));
190
+ } catch (e) {
191
+ reject(new Error('Invalid JSON'));
192
+ }
193
+ });
194
+
195
+ req.on('error', reject);
196
+ });
197
+ }
198
+
199
+ getUrl(): string {
200
+ return `http://localhost:${this.port}/webhook`;
201
+ }
202
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "resolveJsonModule": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
20
+ }
@@ -0,0 +1,157 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { listPending, confirm, reject } from './commands.js';
3
+ import { request, RequestOptions } from 'http';
4
+
5
+ // Mock http module
6
+ vi.mock('http', () => ({
7
+ request: vi.fn(),
8
+ }));
9
+
10
+ describe('CLI Commands', () => {
11
+ const mockRequest = {
12
+ on: vi.fn(),
13
+ write: vi.fn(),
14
+ end: vi.fn(),
15
+ };
16
+
17
+ const mockResponse = {
18
+ statusCode: 200,
19
+ on: vi.fn(),
20
+ };
21
+
22
+ beforeEach(() => {
23
+ vi.clearAllMocks();
24
+ process.env.F2A_CONTROL_PORT = '9001';
25
+ process.env.F2A_CONTROL_TOKEN = 'test-token';
26
+ });
27
+
28
+ afterEach(() => {
29
+ delete process.env.F2A_CONTROL_PORT;
30
+ delete process.env.F2A_CONTROL_TOKEN;
31
+ });
32
+
33
+ describe('listPending', () => {
34
+ it('should list pending connections', async () => {
35
+ const pendingData = {
36
+ success: true,
37
+ pending: [
38
+ { index: 1, agentIdShort: 'abc123', address: '192.168.1.1', port: 9000, remainingMinutes: 30 }
39
+ ]
40
+ };
41
+
42
+ mockResponse.on.mockImplementation((event: string, callback: Function) => {
43
+ if (event === 'data') callback(Buffer.from(JSON.stringify(pendingData)));
44
+ if (event === 'end') callback();
45
+ });
46
+
47
+ (request as any).mockImplementation((options: RequestOptions, callback: Function) => {
48
+ callback(mockResponse);
49
+ return mockRequest;
50
+ });
51
+
52
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
53
+ await listPending();
54
+
55
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('待确认连接'));
56
+ consoleSpy.mockRestore();
57
+ });
58
+
59
+ it('should show no pending message when empty', async () => {
60
+ mockResponse.on.mockImplementation((event: string, callback: Function) => {
61
+ if (event === 'data') callback(Buffer.from(JSON.stringify({ success: true, pending: [] })));
62
+ if (event === 'end') callback();
63
+ });
64
+
65
+ (request as any).mockImplementation((options: RequestOptions, callback: Function) => {
66
+ callback(mockResponse);
67
+ return mockRequest;
68
+ });
69
+
70
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
71
+ await listPending();
72
+
73
+ expect(consoleSpy).toHaveBeenCalledWith('没有待确认的连接请求');
74
+ consoleSpy.mockRestore();
75
+ });
76
+
77
+ it('should handle connection error', async () => {
78
+ (request as any).mockImplementation(() => {
79
+ const req = {
80
+ ...mockRequest,
81
+ on: (event: string, callback: Function) => {
82
+ if (event === 'error') callback(new Error('Connection refused'));
83
+ }
84
+ };
85
+ return req;
86
+ });
87
+
88
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
89
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('exit'); });
90
+
91
+ await expect(listPending()).rejects.toThrow('exit');
92
+
93
+ consoleSpy.mockRestore();
94
+ exitSpy.mockRestore();
95
+ });
96
+ });
97
+
98
+ describe('confirm', () => {
99
+ it('should confirm connection successfully', async () => {
100
+ mockResponse.on.mockImplementation((event: string, callback: Function) => {
101
+ if (event === 'data') callback(Buffer.from(JSON.stringify({ success: true, message: '已确认' })));
102
+ if (event === 'end') callback();
103
+ });
104
+
105
+ (request as any).mockImplementation((options: RequestOptions, callback: Function) => {
106
+ callback(mockResponse);
107
+ return mockRequest;
108
+ });
109
+
110
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
111
+ await confirm(1);
112
+
113
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('已确认'));
114
+ consoleSpy.mockRestore();
115
+ });
116
+
117
+ it('should handle confirm error', async () => {
118
+ mockResponse.on.mockImplementation((event: string, callback: Function) => {
119
+ if (event === 'data') callback(Buffer.from(JSON.stringify({ success: false, error: 'Invalid ID' })));
120
+ if (event === 'end') callback();
121
+ });
122
+
123
+ (request as any).mockImplementation((options: RequestOptions, callback: Function) => {
124
+ callback(mockResponse);
125
+ return mockRequest;
126
+ });
127
+
128
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
129
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('exit'); });
130
+
131
+ await expect(confirm(999)).rejects.toThrow('exit');
132
+
133
+ consoleSpy.mockRestore();
134
+ exitSpy.mockRestore();
135
+ });
136
+ });
137
+
138
+ describe('reject', () => {
139
+ it('should reject connection with reason', async () => {
140
+ mockResponse.on.mockImplementation((event: string, callback: Function) => {
141
+ if (event === 'data') callback(Buffer.from(JSON.stringify({ success: true, message: '已拒绝' })));
142
+ if (event === 'end') callback();
143
+ });
144
+
145
+ (request as any).mockImplementation((options: RequestOptions, callback: Function) => {
146
+ callback(mockResponse);
147
+ return mockRequest;
148
+ });
149
+
150
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
151
+ await reject(1, '可疑连接');
152
+
153
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('已拒绝'));
154
+ consoleSpy.mockRestore();
155
+ });
156
+ });
157
+ });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * F2A CLI
3
+ * 命令行工具
4
+ */
5
+
6
+ import { request, RequestOptions } from 'http';
7
+
8
+ const CONTROL_PORT = parseInt(process.env.F2A_CONTROL_PORT || '9001');
9
+ const CONTROL_TOKEN = process.env.F2A_CONTROL_TOKEN || 'f2a-default-token';
10
+
11
+ interface ControlResponse {
12
+ success: boolean;
13
+ message?: string;
14
+ pending?: Array<{
15
+ index: number;
16
+ agentIdShort: string;
17
+ address: string;
18
+ port: number;
19
+ remainingMinutes: number;
20
+ }>;
21
+ error?: string;
22
+ }
23
+
24
+ /**
25
+ * 发送控制命令
26
+ */
27
+ async function sendControlCommand(
28
+ action: string,
29
+ idOrIndex?: string | number,
30
+ reason?: string
31
+ ): Promise<ControlResponse> {
32
+ return new Promise((resolve, reject) => {
33
+ const payload = JSON.stringify({ action, idOrIndex, reason });
34
+
35
+ const options: RequestOptions = {
36
+ hostname: '127.0.0.1',
37
+ port: CONTROL_PORT,
38
+ path: '/control',
39
+ method: 'POST',
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ 'X-F2A-Token': CONTROL_TOKEN,
43
+ 'Content-Length': Buffer.byteLength(payload)
44
+ }
45
+ };
46
+
47
+ const req = request(options, (res) => {
48
+ let data = '';
49
+ res.on('data', chunk => data += chunk);
50
+ res.on('end', () => {
51
+ try {
52
+ resolve(JSON.parse(data));
53
+ } catch {
54
+ resolve({ success: false, error: 'Invalid response' });
55
+ }
56
+ });
57
+ });
58
+
59
+ req.on('error', (err) => {
60
+ reject(err);
61
+ });
62
+
63
+ req.write(payload);
64
+ req.end();
65
+ });
66
+ }
67
+
68
+ /**
69
+ * 列出待确认连接
70
+ */
71
+ export async function listPending(): Promise<void> {
72
+ try {
73
+ const result = await sendControlCommand('list-pending');
74
+
75
+ if (result.success && result.pending && result.pending.length > 0) {
76
+ console.log(`待确认连接 (${result.pending.length}个):`);
77
+ result.pending.forEach(p => {
78
+ console.log(`${p.index}. ${p.agentIdShort} 来自 ${p.address}:${p.port} [剩余${p.remainingMinutes}分钟]`);
79
+ });
80
+ } else {
81
+ console.log('没有待确认的连接请求');
82
+ }
83
+ } catch (err) {
84
+ const message = err instanceof Error ? err.message : String(err);
85
+ console.error(`[F2A] 无法连接到 Daemon: ${message}`);
86
+ console.error('[F2A] 请确保 Daemon 正在运行');
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * 确认连接
93
+ */
94
+ export async function confirm(idOrIndex: string | number): Promise<void> {
95
+ try {
96
+ const result = await sendControlCommand('confirm', idOrIndex);
97
+
98
+ if (result.success) {
99
+ console.log(`[F2A] ${result.message}`);
100
+ } else {
101
+ console.error(`[F2A] 错误: ${result.error}`);
102
+ process.exit(1);
103
+ }
104
+ } catch (err) {
105
+ const message = err instanceof Error ? err.message : String(err);
106
+ console.error(`[F2A] 无法连接到 Daemon: ${message}`);
107
+ process.exit(1);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * 拒绝连接
113
+ */
114
+ export async function reject(idOrIndex: string | number, reason?: string): Promise<void> {
115
+ try {
116
+ const result = await sendControlCommand('reject', idOrIndex, reason);
117
+
118
+ if (result.success) {
119
+ console.log(`[F2A] ${result.message}`);
120
+ } else {
121
+ console.error(`[F2A] 错误: ${result.error}`);
122
+ process.exit(1);
123
+ }
124
+ } catch (err) {
125
+ const message = err instanceof Error ? err.message : String(err);
126
+ console.error(`[F2A] 无法连接到 Daemon: ${message}`);
127
+ process.exit(1);
128
+ }
129
+ }
@@ -0,0 +1,77 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+
3
+ // Mock dependencies before importing
4
+ const mockRequest = vi.fn();
5
+ const mockExistsSync = vi.fn();
6
+ const mockReadFileSync = vi.fn();
7
+
8
+ vi.mock('http', () => ({
9
+ request: (...args: any[]) => mockRequest(...args),
10
+ }));
11
+
12
+ vi.mock('fs', () => ({
13
+ existsSync: (...args: any[]) => mockExistsSync(...args),
14
+ readFileSync: (...args: any[]) => mockReadFileSync(...args),
15
+ }));
16
+
17
+ vi.mock('os', () => ({
18
+ homedir: vi.fn().mockReturnValue('/home/test'),
19
+ }));
20
+
21
+ describe('CLI Index', () => {
22
+ const mockReq = {
23
+ on: vi.fn(),
24
+ write: vi.fn(),
25
+ end: vi.fn(),
26
+ };
27
+
28
+ const mockRes = {
29
+ statusCode: 200,
30
+ on: vi.fn(),
31
+ };
32
+
33
+ beforeEach(() => {
34
+ vi.clearAllMocks();
35
+
36
+ mockRequest.mockImplementation((options: any, callback: Function) => {
37
+ callback(mockRes);
38
+ return mockReq;
39
+ });
40
+
41
+ mockRes.on.mockImplementation((event: string, callback: Function) => {
42
+ if (event === 'data') callback(Buffer.from(JSON.stringify({ success: true, status: 'ok' })));
43
+ if (event === 'end') callback();
44
+ });
45
+
46
+ mockExistsSync.mockReturnValue(false);
47
+ });
48
+
49
+ afterEach(() => {
50
+ vi.resetModules();
51
+ delete process.env.F2A_CONTROL_PORT;
52
+ delete process.env.F2A_CONTROL_TOKEN;
53
+ });
54
+
55
+ describe('getControlToken', () => {
56
+ it('should use environment variable token', async () => {
57
+ process.env.F2A_CONTROL_TOKEN = 'env-token';
58
+
59
+ vi.resetModules();
60
+ // Just verify env is set correctly
61
+ expect(process.env.F2A_CONTROL_TOKEN).toBe('env-token');
62
+ });
63
+
64
+ it('should read token from file when env not set', async () => {
65
+ delete process.env.F2A_CONTROL_TOKEN;
66
+
67
+ mockExistsSync.mockReturnValue(true);
68
+ mockReadFileSync.mockReturnValue('file-token');
69
+
70
+ vi.resetModules();
71
+ // Import will trigger the token reading
72
+ await import('./index');
73
+
74
+ expect(mockExistsSync).toHaveBeenCalled();
75
+ });
76
+ });
77
+ });