spck 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/.oxlintrc.json +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +631 -0
  4. package/bin/cli.js +20 -0
  5. package/bin/validate-cwd.js +41 -0
  6. package/dist/config/__tests__/config.test.d.ts +2 -0
  7. package/dist/config/__tests__/config.test.js +262 -0
  8. package/dist/config/__tests__/credentials.test.d.ts +2 -0
  9. package/dist/config/__tests__/credentials.test.js +360 -0
  10. package/dist/config/config.d.ts +33 -0
  11. package/dist/config/config.js +185 -0
  12. package/dist/config/credentials.d.ts +75 -0
  13. package/dist/config/credentials.js +259 -0
  14. package/dist/config/server-selection.d.ts +40 -0
  15. package/dist/config/server-selection.js +130 -0
  16. package/dist/connection/__tests__/firebase-auth.test.d.ts +2 -0
  17. package/dist/connection/__tests__/firebase-auth.test.js +96 -0
  18. package/dist/connection/__tests__/hmac.test.d.ts +2 -0
  19. package/dist/connection/__tests__/hmac.test.js +372 -0
  20. package/dist/connection/auth.d.ts +13 -0
  21. package/dist/connection/auth.js +91 -0
  22. package/dist/connection/firebase-auth.d.ts +40 -0
  23. package/dist/connection/firebase-auth.js +429 -0
  24. package/dist/connection/hmac.d.ts +24 -0
  25. package/dist/connection/hmac.js +109 -0
  26. package/dist/i18n/index.d.ts +25 -0
  27. package/dist/i18n/index.js +101 -0
  28. package/dist/i18n/locales/en.json +313 -0
  29. package/dist/i18n/locales/es.json +302 -0
  30. package/dist/i18n/locales/fr.json +302 -0
  31. package/dist/i18n/locales/id.json +302 -0
  32. package/dist/i18n/locales/ja.json +302 -0
  33. package/dist/i18n/locales/ko.json +302 -0
  34. package/dist/i18n/locales/locales/en.json +309 -0
  35. package/dist/i18n/locales/locales/es.json +302 -0
  36. package/dist/i18n/locales/locales/fr.json +302 -0
  37. package/dist/i18n/locales/locales/id.json +302 -0
  38. package/dist/i18n/locales/locales/ja.json +302 -0
  39. package/dist/i18n/locales/locales/ko.json +302 -0
  40. package/dist/i18n/locales/locales/pt.json +302 -0
  41. package/dist/i18n/locales/locales/zh-Hans.json +302 -0
  42. package/dist/i18n/locales/pt.json +302 -0
  43. package/dist/i18n/locales/zh-Hans.json +302 -0
  44. package/dist/index.d.ts +25 -0
  45. package/dist/index.js +493 -0
  46. package/dist/proxy/ProxyClient.d.ts +125 -0
  47. package/dist/proxy/ProxyClient.js +781 -0
  48. package/dist/proxy/ProxySocketWrapper.d.ts +43 -0
  49. package/dist/proxy/ProxySocketWrapper.js +98 -0
  50. package/dist/proxy/__tests__/ProxyClient.test.d.ts +2 -0
  51. package/dist/proxy/__tests__/ProxyClient.test.js +445 -0
  52. package/dist/proxy/__tests__/ProxySocketWrapper.test.d.ts +2 -0
  53. package/dist/proxy/__tests__/ProxySocketWrapper.test.js +190 -0
  54. package/dist/proxy/__tests__/handshake-validation.test.d.ts +2 -0
  55. package/dist/proxy/__tests__/handshake-validation.test.js +282 -0
  56. package/dist/proxy/__tests__/token-refresh-race.test.d.ts +14 -0
  57. package/dist/proxy/__tests__/token-refresh-race.test.js +173 -0
  58. package/dist/proxy/chunking.d.ts +53 -0
  59. package/dist/proxy/chunking.js +127 -0
  60. package/dist/proxy/handshake-validation.d.ts +21 -0
  61. package/dist/proxy/handshake-validation.js +49 -0
  62. package/dist/rpc/__tests__/router.test.d.ts +2 -0
  63. package/dist/rpc/__tests__/router.test.js +262 -0
  64. package/dist/rpc/router.d.ts +37 -0
  65. package/dist/rpc/router.js +132 -0
  66. package/dist/services/BrowserProxyService.d.ts +13 -0
  67. package/dist/services/BrowserProxyService.js +139 -0
  68. package/dist/services/FilesystemService.d.ts +99 -0
  69. package/dist/services/FilesystemService.js +742 -0
  70. package/dist/services/GitService.d.ts +243 -0
  71. package/dist/services/GitService.js +1439 -0
  72. package/dist/services/SearchService.d.ts +93 -0
  73. package/dist/services/SearchService.js +670 -0
  74. package/dist/services/TerminalService.d.ts +62 -0
  75. package/dist/services/TerminalService.js +337 -0
  76. package/dist/services/__tests__/BrowserProxyService.test.d.ts +2 -0
  77. package/dist/services/__tests__/BrowserProxyService.test.js +145 -0
  78. package/dist/services/__tests__/FilesystemService.test.d.ts +2 -0
  79. package/dist/services/__tests__/FilesystemService.test.js +609 -0
  80. package/dist/services/__tests__/GitService.test.d.ts +2 -0
  81. package/dist/services/__tests__/GitService.test.js +953 -0
  82. package/dist/services/__tests__/SearchService.test.d.ts +2 -0
  83. package/dist/services/__tests__/SearchService.test.js +384 -0
  84. package/dist/services/__tests__/TerminalService.test.d.ts +2 -0
  85. package/dist/services/__tests__/TerminalService.test.js +513 -0
  86. package/dist/setup/wizard.d.ts +10 -0
  87. package/dist/setup/wizard.js +172 -0
  88. package/dist/types.d.ts +196 -0
  89. package/dist/types.js +44 -0
  90. package/dist/utils/__tests__/gitignore.test.d.ts +2 -0
  91. package/dist/utils/__tests__/gitignore.test.js +127 -0
  92. package/dist/utils/gitignore.d.ts +24 -0
  93. package/dist/utils/gitignore.js +77 -0
  94. package/dist/utils/logger.d.ts +96 -0
  95. package/dist/utils/logger.js +456 -0
  96. package/dist/utils/project-dir.d.ts +51 -0
  97. package/dist/utils/project-dir.js +191 -0
  98. package/dist/utils/ripgrep.d.ts +34 -0
  99. package/dist/utils/ripgrep.js +148 -0
  100. package/dist/utils/tool-detection.d.ts +17 -0
  101. package/dist/utils/tool-detection.js +126 -0
  102. package/dist/watcher/FileWatcher.d.ts +10 -0
  103. package/dist/watcher/FileWatcher.js +42 -0
  104. package/package.json +70 -0
  105. package/src/config/__tests__/config.test.ts +318 -0
  106. package/src/config/__tests__/credentials.test.ts +494 -0
  107. package/src/config/config.ts +206 -0
  108. package/src/config/credentials.ts +302 -0
  109. package/src/config/server-selection.ts +150 -0
  110. package/src/connection/__tests__/firebase-auth.test.ts +121 -0
  111. package/src/connection/__tests__/hmac.test.ts +509 -0
  112. package/src/connection/auth.ts +140 -0
  113. package/src/connection/firebase-auth.ts +504 -0
  114. package/src/connection/hmac.ts +139 -0
  115. package/src/i18n/index.ts +119 -0
  116. package/src/i18n/locales/en.json +313 -0
  117. package/src/i18n/locales/es.json +302 -0
  118. package/src/i18n/locales/fr.json +302 -0
  119. package/src/i18n/locales/id.json +302 -0
  120. package/src/i18n/locales/ja.json +302 -0
  121. package/src/i18n/locales/ko.json +302 -0
  122. package/src/i18n/locales/pt.json +302 -0
  123. package/src/i18n/locales/zh-Hans.json +302 -0
  124. package/src/index.ts +542 -0
  125. package/src/proxy/ProxyClient.ts +968 -0
  126. package/src/proxy/ProxySocketWrapper.ts +113 -0
  127. package/src/proxy/__tests__/ProxyClient.test.ts +575 -0
  128. package/src/proxy/__tests__/ProxySocketWrapper.test.ts +251 -0
  129. package/src/proxy/__tests__/handshake-validation.test.ts +367 -0
  130. package/src/proxy/chunking.ts +162 -0
  131. package/src/proxy/handshake-validation.ts +64 -0
  132. package/src/rpc/__tests__/router.test.ts +400 -0
  133. package/src/rpc/router.ts +183 -0
  134. package/src/services/BrowserProxyService.ts +179 -0
  135. package/src/services/FilesystemService.ts +841 -0
  136. package/src/services/GitService.ts +1639 -0
  137. package/src/services/SearchService.ts +809 -0
  138. package/src/services/TerminalService.ts +413 -0
  139. package/src/services/__tests__/BrowserProxyService.test.ts +155 -0
  140. package/src/services/__tests__/FilesystemService.test.ts +1002 -0
  141. package/src/services/__tests__/GitService.test.ts +1552 -0
  142. package/src/services/__tests__/SearchService.test.ts +484 -0
  143. package/src/services/__tests__/TerminalService.test.ts +702 -0
  144. package/src/setup/wizard.ts +242 -0
  145. package/src/types/fossil-delta.d.ts +4 -0
  146. package/src/types.ts +287 -0
  147. package/src/utils/__tests__/gitignore.test.ts +174 -0
  148. package/src/utils/gitignore.ts +91 -0
  149. package/src/utils/logger.ts +578 -0
  150. package/src/utils/project-dir.ts +218 -0
  151. package/src/utils/ripgrep.ts +180 -0
  152. package/src/utils/tool-detection.ts +141 -0
  153. package/src/watcher/FileWatcher.ts +53 -0
  154. package/tsconfig.json +24 -0
  155. package/vitest.config.ts +19 -0
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Terminal service - manages PTY sessions with xterm-headless
3
+ */
4
+ import { AuthenticatedSocket } from '../types.js';
5
+ export declare class TerminalService {
6
+ private getSocket;
7
+ private maxTerminals;
8
+ private maxBufferedLines;
9
+ private rootPath;
10
+ private sessions;
11
+ private activeTerminalId;
12
+ constructor(getSocket: () => AuthenticatedSocket, maxTerminals?: number, maxBufferedLines?: number, rootPath?: string);
13
+ /**
14
+ * Get UID from socket with fallback
15
+ */
16
+ private getUid;
17
+ /**
18
+ * Handle terminal RPC methods
19
+ */
20
+ handle(method: string, params: any): Promise<any>;
21
+ /**
22
+ * Create new terminal session
23
+ */
24
+ private create;
25
+ /**
26
+ * Destroy terminal session
27
+ */
28
+ private destroy;
29
+ /**
30
+ * Activate terminal (start streaming)
31
+ */
32
+ private activate;
33
+ /**
34
+ * Send input to terminal
35
+ */
36
+ private send;
37
+ /**
38
+ * Resize terminal
39
+ */
40
+ private resize;
41
+ /**
42
+ * Refresh terminal buffer
43
+ */
44
+ private refresh;
45
+ /**
46
+ * List terminals for current user
47
+ */
48
+ private list;
49
+ /**
50
+ * Send buffer refresh notification
51
+ */
52
+ private sendBufferRefresh;
53
+ /**
54
+ * Generate unique terminal ID
55
+ */
56
+ private generateId;
57
+ /**
58
+ * Cleanup all terminals
59
+ */
60
+ cleanup(): void;
61
+ }
62
+ //# sourceMappingURL=TerminalService.d.ts.map
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Terminal service - manages PTY sessions with xterm-headless
3
+ */
4
+ import * as pty from 'node-pty';
5
+ import XtermHeadlessModule from '@xterm/headless';
6
+ import SerializeAddonModule from '@xterm/addon-serialize';
7
+ import defaultShell from 'default-shell';
8
+ import { ErrorCode, createRPCError } from '../types.js';
9
+ import { logTerminalRead, logTerminalWrite } from '../utils/logger.js';
10
+ const { Terminal: XtermHeadless } = XtermHeadlessModule;
11
+ const { SerializeAddon } = SerializeAddonModule;
12
+ // Hard limit on terminal buffer size to prevent memory exhaustion (DoS)
13
+ const MAX_BUFFER_LINES_HARD_LIMIT = 50000;
14
+ export class TerminalService {
15
+ constructor(getSocket, maxTerminals = 10, maxBufferedLines = 10000, rootPath = process.cwd()) {
16
+ this.getSocket = getSocket;
17
+ this.maxTerminals = maxTerminals;
18
+ this.maxBufferedLines = maxBufferedLines;
19
+ this.rootPath = rootPath;
20
+ this.sessions = new Map();
21
+ this.activeTerminalId = null;
22
+ // Enforce hard limit on buffer size to prevent memory exhaustion
23
+ if (this.maxBufferedLines > MAX_BUFFER_LINES_HARD_LIMIT) {
24
+ console.warn(`Terminal buffer size ${this.maxBufferedLines} exceeds hard limit ${MAX_BUFFER_LINES_HARD_LIMIT}. ` +
25
+ `Capping at ${MAX_BUFFER_LINES_HARD_LIMIT} lines.`);
26
+ this.maxBufferedLines = MAX_BUFFER_LINES_HARD_LIMIT;
27
+ }
28
+ }
29
+ /**
30
+ * Get UID from socket with fallback
31
+ */
32
+ getUid() {
33
+ return this.getSocket().data?.uid || 'unknown';
34
+ }
35
+ /**
36
+ * Handle terminal RPC methods
37
+ */
38
+ async handle(method, params) {
39
+ const uid = this.getUid();
40
+ let result;
41
+ let error;
42
+ // Define read and write operations
43
+ const readOps = ['list', 'refresh'];
44
+ const writeOps = ['create', 'destroy', 'activate', 'send', 'resize'];
45
+ try {
46
+ switch (method) {
47
+ case 'create':
48
+ result = await this.create(params);
49
+ logTerminalWrite(method, params, uid, true, undefined, { terminalId: result });
50
+ return result;
51
+ case 'destroy':
52
+ result = await this.destroy(params);
53
+ logTerminalWrite(method, params, uid, true);
54
+ return result;
55
+ case 'activate':
56
+ result = await this.activate(params);
57
+ logTerminalWrite(method, params, uid, true);
58
+ return result;
59
+ case 'send':
60
+ result = await this.send(params);
61
+ logTerminalWrite(method, params, uid, true);
62
+ return result;
63
+ case 'resize':
64
+ result = await this.resize(params);
65
+ logTerminalWrite(method, params, uid, true);
66
+ return result;
67
+ case 'refresh':
68
+ result = await this.refresh(params);
69
+ logTerminalRead(method, params, uid, true);
70
+ return result;
71
+ case 'list':
72
+ result = await this.list();
73
+ logTerminalRead(method, params, uid, true, undefined, { count: result.length });
74
+ return result;
75
+ default:
76
+ throw createRPCError(ErrorCode.METHOD_NOT_FOUND, `Method not found: terminal.${method}`);
77
+ }
78
+ }
79
+ catch (err) {
80
+ error = err;
81
+ // Log error based on operation type
82
+ if (readOps.includes(method)) {
83
+ logTerminalRead(method, params, uid, false, error);
84
+ }
85
+ else if (writeOps.includes(method)) {
86
+ logTerminalWrite(method, params, uid, false, error);
87
+ }
88
+ throw error;
89
+ }
90
+ }
91
+ /**
92
+ * Create new terminal session
93
+ */
94
+ async create(params) {
95
+ const uid = this.getUid();
96
+ // Check terminal limit
97
+ const userTerminals = Array.from(this.sessions.values()).filter((s) => s.uid === uid);
98
+ if (userTerminals.length >= this.maxTerminals) {
99
+ throw createRPCError(ErrorCode.TERMINAL_LIMIT_EXCEEDED, `Terminal limit exceeded (max ${this.maxTerminals})`);
100
+ }
101
+ const terminalId = this.generateId();
102
+ // Get default shell for the platform
103
+ const shell = defaultShell;
104
+ // Spawn PTY process
105
+ let ptyProcess;
106
+ try {
107
+ ptyProcess = pty.spawn(shell, [], {
108
+ name: 'xterm-256color',
109
+ cols: params.cols || 80,
110
+ rows: params.rows || 24,
111
+ cwd: this.rootPath,
112
+ env: process.env,
113
+ });
114
+ }
115
+ catch (error) {
116
+ // Log full error details server-side for debugging
117
+ console.error('Failed to spawn terminal:', {
118
+ error: error.message,
119
+ shell,
120
+ cwd: this.rootPath,
121
+ stack: error.stack,
122
+ });
123
+ // Send sanitized error to client (no system paths or stack traces)
124
+ throw createRPCError(ErrorCode.INTERNAL_ERROR, 'Failed to spawn terminal. Please check that the shell is available and the working directory is accessible.');
125
+ }
126
+ // Create headless xterm for buffer management
127
+ const xterm = new XtermHeadless({
128
+ cols: params.cols || 80,
129
+ rows: params.rows || 24,
130
+ // Enforce hard limit as defense in depth (should already be capped in constructor)
131
+ scrollback: Math.min(this.maxBufferedLines, MAX_BUFFER_LINES_HARD_LIMIT),
132
+ allowProposedApi: true
133
+ });
134
+ const serializeAddon = new SerializeAddon();
135
+ xterm.loadAddon(serializeAddon);
136
+ // Connect PTY to xterm buffer
137
+ ptyProcess.onData((data) => {
138
+ const session = this.sessions.get(terminalId);
139
+ if (session) {
140
+ // Track pending write
141
+ session.pendingWrites++;
142
+ // Write to xterm with callback
143
+ xterm.write(data, () => {
144
+ // Decrement pending writes when complete
145
+ if (session.pendingWrites > 0) {
146
+ session.pendingWrites--;
147
+ }
148
+ });
149
+ }
150
+ // If this is the active terminal, stream to client
151
+ if (this.activeTerminalId === terminalId) {
152
+ this.getSocket().emit('rpc', {
153
+ jsonrpc: '2.0',
154
+ method: 'terminal.output',
155
+ params: { terminalId, data },
156
+ });
157
+ }
158
+ });
159
+ // Handle process exit
160
+ ptyProcess.onExit(({ exitCode }) => {
161
+ const session = this.sessions.get(terminalId);
162
+ if (session) {
163
+ session.exited = true;
164
+ session.exitCode = exitCode;
165
+ // Notify client
166
+ this.getSocket().emit('rpc', {
167
+ jsonrpc: '2.0',
168
+ method: 'terminal.exited',
169
+ params: { terminalId, exitCode },
170
+ });
171
+ }
172
+ });
173
+ // Store session
174
+ this.sessions.set(terminalId, {
175
+ id: terminalId,
176
+ pty: ptyProcess,
177
+ xterm,
178
+ serializeAddon,
179
+ cols: params.cols || 80,
180
+ rows: params.rows || 24,
181
+ exited: false,
182
+ uid,
183
+ pendingWrites: 0,
184
+ });
185
+ // Send initial buffer
186
+ await this.sendBufferRefresh(terminalId);
187
+ return terminalId;
188
+ }
189
+ /**
190
+ * Destroy terminal session
191
+ */
192
+ async destroy(params) {
193
+ const session = this.sessions.get(params.terminalId);
194
+ if (!session) {
195
+ throw createRPCError(ErrorCode.TERMINAL_NOT_FOUND, 'Terminal not found');
196
+ }
197
+ // Verify ownership
198
+ if (session.uid !== this.getUid()) {
199
+ throw createRPCError(ErrorCode.PERMISSION_DENIED, 'Permission denied');
200
+ }
201
+ // Kill PTY process if still running
202
+ if (!session.exited) {
203
+ session.pty.kill();
204
+ }
205
+ // Clean up
206
+ this.sessions.delete(params.terminalId);
207
+ if (this.activeTerminalId === params.terminalId) {
208
+ this.activeTerminalId = null;
209
+ }
210
+ }
211
+ /**
212
+ * Activate terminal (start streaming)
213
+ */
214
+ async activate(params) {
215
+ const session = this.sessions.get(params.terminalId);
216
+ if (!session) {
217
+ throw createRPCError(ErrorCode.TERMINAL_NOT_FOUND, 'Terminal not found');
218
+ }
219
+ // Verify ownership
220
+ if (session.uid !== this.getUid()) {
221
+ throw createRPCError(ErrorCode.PERMISSION_DENIED, 'Permission denied');
222
+ }
223
+ // Update active terminal
224
+ this.activeTerminalId = params.terminalId;
225
+ // Send full buffer refresh
226
+ await this.sendBufferRefresh(params.terminalId);
227
+ }
228
+ /**
229
+ * Send input to terminal
230
+ */
231
+ async send(params) {
232
+ const session = this.sessions.get(params.terminalId);
233
+ if (!session) {
234
+ throw createRPCError(ErrorCode.TERMINAL_NOT_FOUND, 'Terminal not found');
235
+ }
236
+ // Verify ownership
237
+ if (session.uid !== this.getUid()) {
238
+ throw createRPCError(ErrorCode.PERMISSION_DENIED, 'Permission denied');
239
+ }
240
+ if (session.exited) {
241
+ throw createRPCError(ErrorCode.TERMINAL_PROCESS_EXITED, 'Terminal process has exited');
242
+ }
243
+ // Write to PTY
244
+ session.pty.write(params.data);
245
+ }
246
+ /**
247
+ * Resize terminal
248
+ */
249
+ async resize(params) {
250
+ const session = this.sessions.get(params.terminalId);
251
+ if (!session) {
252
+ throw createRPCError(ErrorCode.TERMINAL_NOT_FOUND, 'Terminal not found');
253
+ }
254
+ // Verify ownership
255
+ if (session.uid !== this.getUid()) {
256
+ throw createRPCError(ErrorCode.PERMISSION_DENIED, 'Permission denied');
257
+ }
258
+ // Resize PTY
259
+ session.pty.resize(params.cols, params.rows);
260
+ // Resize xterm buffer
261
+ session.xterm.resize(params.cols, params.rows);
262
+ // Update session
263
+ session.cols = params.cols;
264
+ session.rows = params.rows;
265
+ }
266
+ /**
267
+ * Refresh terminal buffer
268
+ */
269
+ async refresh(params) {
270
+ const session = this.sessions.get(params.terminalId);
271
+ if (!session) {
272
+ throw createRPCError(ErrorCode.TERMINAL_NOT_FOUND, 'Terminal not found');
273
+ }
274
+ // Verify ownership
275
+ if (session.uid !== this.getUid()) {
276
+ throw createRPCError(ErrorCode.PERMISSION_DENIED, 'Permission denied');
277
+ }
278
+ await this.sendBufferRefresh(params.terminalId);
279
+ }
280
+ /**
281
+ * List terminals for current user
282
+ */
283
+ async list() {
284
+ const uid = this.getUid();
285
+ return Array.from(this.sessions.values())
286
+ .filter((s) => s.uid === uid)
287
+ .map((s) => s.id);
288
+ }
289
+ /**
290
+ * Send buffer refresh notification
291
+ */
292
+ async sendBufferRefresh(terminalId) {
293
+ const session = this.sessions.get(terminalId);
294
+ if (!session)
295
+ return;
296
+ // Wait for pending writes to complete
297
+ const waitForWrites = async () => {
298
+ const maxWaitTime = 5000; // 5 seconds max
299
+ const checkInterval = 10; // Check every 10ms
300
+ let elapsed = 0;
301
+ while (session.pendingWrites > 0 && elapsed < maxWaitTime) {
302
+ await new Promise(resolve => setTimeout(resolve, checkInterval));
303
+ elapsed += checkInterval;
304
+ }
305
+ if (elapsed >= maxWaitTime) {
306
+ console.warn(`Timeout waiting for pending writes (${session.pendingWrites} remaining)`);
307
+ }
308
+ };
309
+ await waitForWrites();
310
+ // Serialize xterm buffer
311
+ const buffer = session.serializeAddon.serialize();
312
+ // Send to client
313
+ this.getSocket().emit('rpc', {
314
+ jsonrpc: '2.0',
315
+ method: 'terminal.bufferRefresh',
316
+ params: { terminalId, buffer },
317
+ });
318
+ }
319
+ /**
320
+ * Generate unique terminal ID
321
+ */
322
+ generateId() {
323
+ return `term-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
324
+ }
325
+ /**
326
+ * Cleanup all terminals
327
+ */
328
+ cleanup() {
329
+ for (const session of this.sessions.values()) {
330
+ if (!session.exited) {
331
+ session.pty.kill();
332
+ }
333
+ }
334
+ this.sessions.clear();
335
+ }
336
+ }
337
+ //# sourceMappingURL=TerminalService.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=BrowserProxyService.test.d.ts.map
@@ -0,0 +1,145 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ /**
3
+ * Tests for BrowserProxyService edge cases
4
+ */
5
+ import http from 'http';
6
+ import { BrowserProxyService } from '../BrowserProxyService.js';
7
+ import { ErrorCode } from '../../types.js';
8
+ function makeService() {
9
+ return new BrowserProxyService();
10
+ }
11
+ function startServer(handler) {
12
+ return new Promise((resolve, reject) => {
13
+ const server = http.createServer(handler);
14
+ server.listen(0, '127.0.0.1', () => {
15
+ const { port } = server.address();
16
+ resolve({ port, close: () => new Promise((res) => server.close(() => res())) });
17
+ });
18
+ server.on('error', reject);
19
+ });
20
+ }
21
+ function makeParams(overrides = {}) {
22
+ return { requestId: 'req-1', url: 'http://localhost:3000/', method: 'GET', headers: {}, body: null, ...overrides };
23
+ }
24
+ function makeSocket(deviceId = 'test-device-1') {
25
+ return { data: { deviceId, uid: 'test-user' } };
26
+ }
27
+ describe('BrowserProxyService', () => {
28
+ let service;
29
+ beforeEach(() => { service = makeService(); });
30
+ it('rejects unknown RPC methods', async () => {
31
+ await expect(service.handle('unknown', makeParams(), makeSocket())).rejects.toMatchObject({ code: ErrorCode.METHOD_NOT_FOUND });
32
+ });
33
+ it.each([
34
+ ['non-localhost hostname', 'http://example.com/'],
35
+ ['internal IP', 'http://192.168.1.1:3000/'],
36
+ ['0.0.0.0', 'http://0.0.0.0:3000/'],
37
+ ])('rejects %s', async (_label, url) => {
38
+ await expect(service.handle('request', makeParams({ url }), makeSocket())).rejects.toMatchObject({ code: ErrorCode.PERMISSION_DENIED });
39
+ });
40
+ it.each([
41
+ ['malformed URL', 'not-a-url', ErrorCode.INVALID_PARAMS],
42
+ ['port 0', 'http://localhost:0/', ErrorCode.INVALID_PARAMS],
43
+ ['port > 65535', 'http://localhost:99999/', ErrorCode.INVALID_PARAMS],
44
+ ['TRACE method', 'http://localhost:3000/', ErrorCode.INVALID_PARAMS],
45
+ ])('rejects %s', async (_label, urlOrMethod, code) => {
46
+ const isMethod = _label.includes('method');
47
+ await expect(service.handle('request', makeParams(isMethod ? { method: 'TRACE' } : { url: urlOrMethod }), makeSocket())).rejects.toMatchObject({ code });
48
+ });
49
+ it('forwards POST body bytes and returns base64-encoded response', async () => {
50
+ let receivedBody = '';
51
+ const srv = await startServer((req, res) => {
52
+ req.on('data', (c) => { receivedBody += c.toString(); });
53
+ req.on('end', () => { res.writeHead(200); res.end('pong'); });
54
+ });
55
+ try {
56
+ const result = await service.handle('request', makeParams({
57
+ url: `http://localhost:${srv.port}/`,
58
+ method: 'POST',
59
+ body: Array.from(Buffer.from('ping')),
60
+ }), makeSocket());
61
+ expect(receivedBody).toBe('ping');
62
+ expect(result.bodyEncoding).toBe('base64');
63
+ expect(Buffer.from(result.body, 'base64').toString()).toBe('pong');
64
+ }
65
+ finally {
66
+ await srv.close();
67
+ }
68
+ });
69
+ it('correctly encodes binary response body', async () => {
70
+ const binary = Buffer.from([0x00, 0x01, 0xff, 0xfe]);
71
+ const srv = await startServer((_req, res) => { res.writeHead(200); res.end(binary); });
72
+ try {
73
+ const result = await service.handle('request', makeParams({ url: `http://localhost:${srv.port}/` }), makeSocket());
74
+ expect(Buffer.from(result.body, 'base64')).toEqual(binary);
75
+ }
76
+ finally {
77
+ await srv.close();
78
+ }
79
+ });
80
+ it('strips hop-by-hop headers and forwards custom headers', async () => {
81
+ let received = {};
82
+ const srv = await startServer((req, res) => { received = req.headers; res.writeHead(200); res.end(); });
83
+ try {
84
+ await service.handle('request', makeParams({
85
+ url: `http://localhost:${srv.port}/`,
86
+ headers: { 'transfer-encoding': 'chunked', 'upgrade': 'websocket', 'x-custom': 'yes' },
87
+ }), makeSocket());
88
+ expect(received['transfer-encoding']).toBeUndefined();
89
+ expect(received['upgrade']).toBeUndefined();
90
+ expect(received['x-custom']).toBe('yes');
91
+ }
92
+ finally {
93
+ await srv.close();
94
+ }
95
+ });
96
+ it('rejects with INTERNAL_ERROR on ECONNREFUSED', async () => {
97
+ await expect(service.handle('request', makeParams({ url: 'http://localhost:19999/' }), makeSocket())).rejects.toMatchObject({ code: ErrorCode.INTERNAL_ERROR });
98
+ });
99
+ it('rejects with OPERATION_TIMEOUT when server hangs', async () => {
100
+ const srv = await startServer(() => { });
101
+ try {
102
+ vi.spyOn(service, 'fetch').mockImplementation((...args) => {
103
+ const url = args[0];
104
+ return new Promise((_res, reject) => {
105
+ const req = http.request({ hostname: url.hostname, port: url.port, path: '/', timeout: 50 });
106
+ req.on('timeout', () => { req.destroy(); reject({ code: ErrorCode.OPERATION_TIMEOUT, message: 'timed out' }); });
107
+ req.on('error', reject);
108
+ req.end();
109
+ });
110
+ });
111
+ await expect(service.handle('request', makeParams({ url: `http://localhost:${srv.port}/` }), makeSocket()))
112
+ .rejects.toMatchObject({ code: ErrorCode.OPERATION_TIMEOUT });
113
+ }
114
+ finally {
115
+ vi.restoreAllMocks();
116
+ await srv.close();
117
+ }
118
+ });
119
+ it('rejects responses exceeding 10 MB', async () => {
120
+ const srv = await startServer((_req, res) => {
121
+ res.writeHead(200);
122
+ const chunk = Buffer.alloc(1024 * 1024);
123
+ let sent = 0;
124
+ const write = () => {
125
+ while (sent < 11) {
126
+ if (!res.write(chunk)) {
127
+ res.once('drain', write);
128
+ return;
129
+ }
130
+ sent++;
131
+ }
132
+ res.end();
133
+ };
134
+ write();
135
+ });
136
+ try {
137
+ await expect(service.handle('request', makeParams({ url: `http://localhost:${srv.port}/` }), makeSocket()))
138
+ .rejects.toMatchObject({ code: ErrorCode.INTERNAL_ERROR });
139
+ }
140
+ finally {
141
+ await srv.close();
142
+ }
143
+ });
144
+ });
145
+ //# sourceMappingURL=BrowserProxyService.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=FilesystemService.test.d.ts.map