@cloudflare/sandbox 0.0.0-e1fa354 → 0.0.0-e489cbb

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 (94) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/Dockerfile +107 -38
  3. package/README.md +89 -771
  4. package/dist/chunk-53JFOF7F.js +2352 -0
  5. package/dist/chunk-53JFOF7F.js.map +1 -0
  6. package/dist/chunk-BFVUNTP4.js +104 -0
  7. package/dist/chunk-BFVUNTP4.js.map +1 -0
  8. package/dist/chunk-EKSWCBCA.js +86 -0
  9. package/dist/chunk-EKSWCBCA.js.map +1 -0
  10. package/dist/chunk-JXZMAU2C.js +559 -0
  11. package/dist/chunk-JXZMAU2C.js.map +1 -0
  12. package/dist/chunk-Z532A7QC.js +78 -0
  13. package/dist/chunk-Z532A7QC.js.map +1 -0
  14. package/dist/file-stream.d.ts +43 -0
  15. package/dist/file-stream.js +9 -0
  16. package/dist/file-stream.js.map +1 -0
  17. package/dist/index.d.ts +9 -0
  18. package/dist/index.js +66 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/interpreter.d.ts +33 -0
  21. package/dist/interpreter.js +8 -0
  22. package/dist/interpreter.js.map +1 -0
  23. package/dist/request-handler.d.ts +18 -0
  24. package/dist/request-handler.js +12 -0
  25. package/dist/request-handler.js.map +1 -0
  26. package/dist/sandbox-D9K2ypln.d.ts +583 -0
  27. package/dist/sandbox.d.ts +4 -0
  28. package/dist/sandbox.js +12 -0
  29. package/dist/sandbox.js.map +1 -0
  30. package/dist/security.d.ts +31 -0
  31. package/dist/security.js +13 -0
  32. package/dist/security.js.map +1 -0
  33. package/dist/sse-parser.d.ts +28 -0
  34. package/dist/sse-parser.js +11 -0
  35. package/dist/sse-parser.js.map +1 -0
  36. package/package.json +13 -5
  37. package/src/clients/base-client.ts +280 -0
  38. package/src/clients/command-client.ts +115 -0
  39. package/src/clients/file-client.ts +269 -0
  40. package/src/clients/git-client.ts +92 -0
  41. package/src/clients/index.ts +63 -0
  42. package/src/{jupyter-client.ts → clients/interpreter-client.ts} +148 -168
  43. package/src/clients/port-client.ts +105 -0
  44. package/src/clients/process-client.ts +177 -0
  45. package/src/clients/sandbox-client.ts +41 -0
  46. package/src/clients/types.ts +84 -0
  47. package/src/clients/utility-client.ts +94 -0
  48. package/src/errors/adapter.ts +180 -0
  49. package/src/errors/classes.ts +469 -0
  50. package/src/errors/index.ts +105 -0
  51. package/src/file-stream.ts +164 -0
  52. package/src/index.ts +82 -53
  53. package/src/interpreter.ts +22 -13
  54. package/src/request-handler.ts +69 -43
  55. package/src/sandbox.ts +697 -527
  56. package/src/security.ts +14 -23
  57. package/src/sse-parser.ts +4 -8
  58. package/startup.sh +3 -0
  59. package/tests/base-client.test.ts +328 -0
  60. package/tests/command-client.test.ts +407 -0
  61. package/tests/file-client.test.ts +643 -0
  62. package/tests/file-stream.test.ts +306 -0
  63. package/tests/git-client.test.ts +328 -0
  64. package/tests/port-client.test.ts +301 -0
  65. package/tests/process-client.test.ts +658 -0
  66. package/tests/sandbox.test.ts +465 -0
  67. package/tests/sse-parser.test.ts +290 -0
  68. package/tests/utility-client.test.ts +266 -0
  69. package/tests/wrangler.jsonc +35 -0
  70. package/tsconfig.json +9 -1
  71. package/vitest.config.ts +31 -0
  72. package/container_src/bun.lock +0 -122
  73. package/container_src/circuit-breaker.ts +0 -121
  74. package/container_src/control-process.ts +0 -784
  75. package/container_src/handler/exec.ts +0 -185
  76. package/container_src/handler/file.ts +0 -406
  77. package/container_src/handler/git.ts +0 -130
  78. package/container_src/handler/ports.ts +0 -314
  79. package/container_src/handler/process.ts +0 -568
  80. package/container_src/handler/session.ts +0 -92
  81. package/container_src/index.ts +0 -601
  82. package/container_src/isolation.ts +0 -1038
  83. package/container_src/jupyter-server.ts +0 -579
  84. package/container_src/jupyter-service.ts +0 -461
  85. package/container_src/jupyter_config.py +0 -48
  86. package/container_src/mime-processor.ts +0 -255
  87. package/container_src/package.json +0 -18
  88. package/container_src/shell-escape.ts +0 -42
  89. package/container_src/startup.sh +0 -84
  90. package/container_src/types.ts +0 -131
  91. package/src/client.ts +0 -1009
  92. package/src/errors.ts +0 -218
  93. package/src/interpreter-types.ts +0 -383
  94. package/src/types.ts +0 -502
@@ -0,0 +1,92 @@
1
+ import type { GitCheckoutResult } from '@repo/shared';
2
+ import { BaseHttpClient } from './base-client';
3
+ import type { HttpClientOptions, SessionRequest } from './types';
4
+
5
+ // Re-export for convenience
6
+ export type { GitCheckoutResult };
7
+
8
+ /**
9
+ * Request interface for Git checkout operations
10
+ */
11
+ export interface GitCheckoutRequest extends SessionRequest {
12
+ repoUrl: string;
13
+ branch?: string;
14
+ targetDir?: string;
15
+ }
16
+
17
+ /**
18
+ * Client for Git repository operations
19
+ */
20
+ export class GitClient extends BaseHttpClient {
21
+
22
+ /**
23
+ * Clone a Git repository
24
+ * @param repoUrl - URL of the Git repository to clone
25
+ * @param sessionId - The session ID for this operation
26
+ * @param options - Optional settings (branch, targetDir)
27
+ */
28
+ async checkout(
29
+ repoUrl: string,
30
+ sessionId: string,
31
+ options?: {
32
+ branch?: string;
33
+ targetDir?: string;
34
+ }
35
+ ): Promise<GitCheckoutResult> {
36
+ try {
37
+ // Determine target directory - use provided path or generate from repo name
38
+ let targetDir = options?.targetDir;
39
+ if (!targetDir) {
40
+ const repoName = this.extractRepoName(repoUrl);
41
+ // Ensure absolute path in /workspace
42
+ targetDir = `/workspace/${repoName}`;
43
+ }
44
+
45
+ const data: GitCheckoutRequest = {
46
+ repoUrl,
47
+ sessionId,
48
+ targetDir,
49
+ };
50
+
51
+ // Only include branch if explicitly specified
52
+ // This allows Git to use the repository's default branch
53
+ if (options?.branch) {
54
+ data.branch = options.branch;
55
+ }
56
+
57
+ const response = await this.post<GitCheckoutResult>(
58
+ '/api/git/checkout',
59
+ data
60
+ );
61
+
62
+ this.logSuccess(
63
+ 'Repository cloned',
64
+ `${repoUrl} (branch: ${response.branch}) -> ${response.targetDir}`
65
+ );
66
+
67
+ return response;
68
+ } catch (error) {
69
+ this.logError('checkout', error);
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Extract repository name from URL for default directory name
76
+ */
77
+ private extractRepoName(repoUrl: string): string {
78
+ try {
79
+ const url = new URL(repoUrl);
80
+ const pathParts = url.pathname.split('/');
81
+ const repoName = pathParts[pathParts.length - 1];
82
+
83
+ // Remove .git extension if present
84
+ return repoName.replace(/\.git$/, '');
85
+ } catch {
86
+ // Fallback for invalid URLs
87
+ const parts = repoUrl.split('/');
88
+ const repoName = parts[parts.length - 1];
89
+ return repoName.replace(/\.git$/, '') || 'repo';
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,63 @@
1
+ // Main client exports
2
+
3
+
4
+ // Command client types
5
+ export type {
6
+ ExecuteRequest,
7
+ ExecuteResponse,
8
+ } from './command-client';
9
+
10
+ // Domain-specific clients
11
+ export { CommandClient } from './command-client';
12
+ // File client types
13
+ export type {
14
+ FileOperationRequest,
15
+ MkdirRequest,
16
+ ReadFileRequest,
17
+ WriteFileRequest,
18
+ } from './file-client';
19
+ export { FileClient } from './file-client';
20
+ // Git client types
21
+ export type {
22
+ GitCheckoutRequest,
23
+ GitCheckoutResult,
24
+ } from './git-client';
25
+ export { GitClient } from './git-client';
26
+ export { type ExecutionCallbacks, InterpreterClient } from './interpreter-client';
27
+ // Port client types
28
+ export type {
29
+ ExposePortRequest,
30
+ PortCloseResult,
31
+ PortExposeResult,
32
+ PortListResult,
33
+ UnexposePortRequest,
34
+ } from './port-client';
35
+ export { PortClient } from './port-client';
36
+ // Process client types
37
+ export type {
38
+ ProcessCleanupResult,
39
+ ProcessInfoResult,
40
+ ProcessKillResult,
41
+ ProcessListResult,
42
+ ProcessLogsResult,
43
+ ProcessStartResult,
44
+ StartProcessRequest,
45
+ } from './process-client';
46
+ export { ProcessClient } from './process-client';
47
+ export { SandboxClient } from './sandbox-client';
48
+ // Types and interfaces
49
+ export type {
50
+ BaseApiResponse,
51
+ ContainerStub,
52
+ ErrorResponse,
53
+ HttpClientOptions,
54
+ RequestConfig,
55
+ ResponseHandler,
56
+ SessionRequest,
57
+ } from './types';
58
+ // Utility client types
59
+ export type {
60
+ CommandsResponse,
61
+ PingResponse,
62
+ } from './utility-client';
63
+ export { UtilityClient } from './utility-client';
@@ -1,25 +1,17 @@
1
- import { HttpClient } from "./client.js";
2
- import { isRetryableError, parseErrorResponse } from "./errors.js";
3
- import type {
4
- CodeContext,
5
- CreateContextOptions,
6
- ExecutionError,
7
- OutputMessage,
8
- Result,
9
- } from "./interpreter-types.js";
10
-
11
- // API Response types
12
- interface ContextResponse {
13
- id: string;
14
- language: string;
15
- cwd: string;
16
- createdAt: string; // ISO date string from JSON
17
- lastUsed: string; // ISO date string from JSON
18
- }
19
-
20
- interface ContextListResponse {
21
- contexts: ContextResponse[];
22
- }
1
+ import {
2
+ type CodeContext,
3
+ type ContextCreateResult,
4
+ type ContextListResult,
5
+ type CreateContextOptions,
6
+ type ExecutionError,
7
+ type OutputMessage,
8
+ type Result,
9
+ ResultImpl,
10
+ } from '@repo/shared';
11
+ import type { ErrorResponse } from '../errors';
12
+ import { createErrorFromResponse, ErrorCode, InterpreterNotReadyError } from '../errors';
13
+ import { BaseHttpClient } from './base-client.js';
14
+ import type { HttpClientOptions } from './types.js';
23
15
 
24
16
  // Streaming execution data from the server
25
17
  interface StreamingExecutionData {
@@ -62,7 +54,7 @@ export interface ExecutionCallbacks {
62
54
  onError?: (error: ExecutionError) => void | Promise<void>;
63
55
  }
64
56
 
65
- export class JupyterClient extends HttpClient {
57
+ export class InterpreterClient extends BaseHttpClient {
66
58
  private readonly maxRetries = 3;
67
59
  private readonly retryDelayMs = 1000;
68
60
 
@@ -81,16 +73,21 @@ export class JupyterClient extends HttpClient {
81
73
  });
82
74
 
83
75
  if (!response.ok) {
84
- throw await parseErrorResponse(response);
76
+ const error = await this.parseErrorResponse(response);
77
+ throw error;
78
+ }
79
+
80
+ const data = (await response.json()) as ContextCreateResult;
81
+ if (!data.success) {
82
+ throw new Error(`Failed to create context: ${JSON.stringify(data)}`);
85
83
  }
86
84
 
87
- const data = (await response.json()) as ContextResponse;
88
85
  return {
89
- id: data.id,
86
+ id: data.contextId,
90
87
  language: data.language,
91
- cwd: data.cwd,
92
- createdAt: new Date(data.createdAt),
93
- lastUsed: new Date(data.lastUsed),
88
+ cwd: data.cwd || '/workspace',
89
+ createdAt: new Date(data.timestamp),
90
+ lastUsed: new Date(data.timestamp),
94
91
  };
95
92
  });
96
93
  }
@@ -99,7 +96,8 @@ export class JupyterClient extends HttpClient {
99
96
  contextId: string | undefined,
100
97
  code: string,
101
98
  language: string | undefined,
102
- callbacks: ExecutionCallbacks
99
+ callbacks: ExecutionCallbacks,
100
+ timeoutMs?: number
103
101
  ): Promise<void> {
104
102
  return this.executeWithRetry(async () => {
105
103
  const response = await this.doFetch("/api/execute/code", {
@@ -112,11 +110,13 @@ export class JupyterClient extends HttpClient {
112
110
  context_id: contextId,
113
111
  code,
114
112
  language,
113
+ ...(timeoutMs !== undefined && { timeout_ms: timeoutMs })
115
114
  }),
116
115
  });
117
116
 
118
117
  if (!response.ok) {
119
- throw await parseErrorResponse(response);
118
+ const error = await this.parseErrorResponse(response);
119
+ throw error;
120
120
  }
121
121
 
122
122
  if (!response.body) {
@@ -130,6 +130,112 @@ export class JupyterClient extends HttpClient {
130
130
  });
131
131
  }
132
132
 
133
+ async listCodeContexts(): Promise<CodeContext[]> {
134
+ return this.executeWithRetry(async () => {
135
+ const response = await this.doFetch("/api/contexts", {
136
+ method: "GET",
137
+ headers: { "Content-Type": "application/json" },
138
+ });
139
+
140
+ if (!response.ok) {
141
+ const error = await this.parseErrorResponse(response);
142
+ throw error;
143
+ }
144
+
145
+ const data = (await response.json()) as ContextListResult;
146
+ if (!data.success) {
147
+ throw new Error(`Failed to list contexts: ${JSON.stringify(data)}`);
148
+ }
149
+
150
+ return data.contexts.map((ctx) => ({
151
+ id: ctx.id,
152
+ language: ctx.language,
153
+ cwd: ctx.cwd || '/workspace',
154
+ createdAt: new Date(data.timestamp),
155
+ lastUsed: new Date(data.timestamp),
156
+ }));
157
+ });
158
+ }
159
+
160
+ async deleteCodeContext(contextId: string): Promise<void> {
161
+ return this.executeWithRetry(async () => {
162
+ const response = await this.doFetch(`/api/contexts/${contextId}`, {
163
+ method: "DELETE",
164
+ headers: { "Content-Type": "application/json" },
165
+ });
166
+
167
+ if (!response.ok) {
168
+ const error = await this.parseErrorResponse(response);
169
+ throw error;
170
+ }
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Execute an operation with automatic retry for transient errors
176
+ */
177
+ private async executeWithRetry<T>(operation: () => Promise<T>): Promise<T> {
178
+ let lastError: Error | undefined;
179
+
180
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
181
+ try {
182
+ return await operation();
183
+ } catch (error) {
184
+ this.logError('executeWithRetry', error);
185
+ lastError = error as Error;
186
+
187
+ // Check if it's a retryable error (interpreter not ready)
188
+ if (this.isRetryableError(error)) {
189
+ // Don't retry on the last attempt
190
+ if (attempt < this.maxRetries - 1) {
191
+ // Exponential backoff with jitter
192
+ const delay =
193
+ this.retryDelayMs * 2 ** attempt + Math.random() * 1000;
194
+ await new Promise((resolve) => setTimeout(resolve, delay));
195
+ continue;
196
+ }
197
+ }
198
+
199
+ // Not retryable or last attempt - throw the error
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ throw lastError || new Error("Execution failed after retries");
205
+ }
206
+
207
+ private isRetryableError(error: unknown): boolean {
208
+ if (error instanceof InterpreterNotReadyError) {
209
+ return true;
210
+ }
211
+
212
+ if (error instanceof Error) {
213
+ return (
214
+ error.message.includes("not ready") ||
215
+ error.message.includes("initializing")
216
+ );
217
+ }
218
+
219
+ return false;
220
+ }
221
+
222
+ private async parseErrorResponse(response: Response): Promise<Error> {
223
+ try {
224
+ const errorData = await response.json() as ErrorResponse;
225
+ return createErrorFromResponse(errorData);
226
+ } catch {
227
+ // Fallback if response isn't JSON
228
+ const errorResponse: ErrorResponse = {
229
+ code: ErrorCode.INTERNAL_ERROR,
230
+ message: `HTTP ${response.status}: ${response.statusText}`,
231
+ context: {},
232
+ httpStatus: response.status,
233
+ timestamp: new Date().toISOString()
234
+ };
235
+ return createErrorFromResponse(errorResponse);
236
+ }
237
+ }
238
+
133
239
  private async *readLines(
134
240
  stream: ReadableStream<Uint8Array>
135
241
  ): AsyncGenerator<string> {
@@ -167,8 +273,13 @@ export class JupyterClient extends HttpClient {
167
273
  ) {
168
274
  if (!line.trim()) return;
169
275
 
276
+ // Skip lines that don't start with "data: " (SSE format)
277
+ if (!line.startsWith('data: ')) return;
278
+
170
279
  try {
171
- const data = JSON.parse(line) as StreamingExecutionData;
280
+ // Strip "data: " prefix and parse JSON
281
+ const jsonData = line.substring(6); // "data: " is 6 characters
282
+ const data = JSON.parse(jsonData) as StreamingExecutionData;
172
283
 
173
284
  switch (data.type) {
174
285
  case "stdout":
@@ -191,34 +302,8 @@ export class JupyterClient extends HttpClient {
191
302
 
192
303
  case "result":
193
304
  if (callbacks.onResult) {
194
- // Convert raw result to Result interface
195
- const result: Result = {
196
- text: data.text,
197
- html: data.html,
198
- png: data.png,
199
- jpeg: data.jpeg,
200
- svg: data.svg,
201
- latex: data.latex,
202
- markdown: data.markdown,
203
- javascript: data.javascript,
204
- json: data.json,
205
- chart: data.chart,
206
- data: data.data,
207
- formats: () => {
208
- const formats: string[] = [];
209
- if (data.text) formats.push("text");
210
- if (data.html) formats.push("html");
211
- if (data.png) formats.push("png");
212
- if (data.jpeg) formats.push("jpeg");
213
- if (data.svg) formats.push("svg");
214
- if (data.latex) formats.push("latex");
215
- if (data.markdown) formats.push("markdown");
216
- if (data.javascript) formats.push("javascript");
217
- if (data.json) formats.push("json");
218
- if (data.chart) formats.push("chart");
219
- return formats;
220
- },
221
- };
305
+ // Create a ResultImpl instance from the raw data
306
+ const result = new ResultImpl(data);
222
307
  await callbacks.onResult(result);
223
308
  }
224
309
  break;
@@ -227,123 +312,18 @@ export class JupyterClient extends HttpClient {
227
312
  if (callbacks.onError) {
228
313
  await callbacks.onError({
229
314
  name: data.ename || "Error",
230
- value: data.evalue || data.text || "Unknown error",
315
+ message: data.evalue || "Unknown error",
231
316
  traceback: data.traceback || [],
232
- lineNumber: data.lineNumber,
233
317
  });
234
318
  }
235
319
  break;
236
320
 
237
321
  case "execution_complete":
238
- // Execution completed successfully
322
+ // Signal completion - callbacks can handle cleanup if needed
239
323
  break;
240
324
  }
241
325
  } catch (error) {
242
- console.error("[JupyterClient] Error parsing execution result:", error);
243
- }
244
- }
245
-
246
- async listCodeContexts(): Promise<CodeContext[]> {
247
- return this.executeWithRetry(async () => {
248
- const response = await this.doFetch("/api/contexts", {
249
- method: "GET",
250
- headers: { "Content-Type": "application/json" },
251
- });
252
-
253
- if (!response.ok) {
254
- throw await parseErrorResponse(response);
255
- }
256
-
257
- const data = (await response.json()) as ContextListResponse;
258
- return data.contexts.map((ctx) => ({
259
- id: ctx.id,
260
- language: ctx.language,
261
- cwd: ctx.cwd,
262
- createdAt: new Date(ctx.createdAt),
263
- lastUsed: new Date(ctx.lastUsed),
264
- }));
265
- });
266
- }
267
-
268
- async deleteCodeContext(contextId: string): Promise<void> {
269
- return this.executeWithRetry(async () => {
270
- const response = await this.doFetch(`/api/contexts/${contextId}`, {
271
- method: "DELETE",
272
- headers: { "Content-Type": "application/json" },
273
- });
274
-
275
- if (!response.ok) {
276
- throw await parseErrorResponse(response);
277
- }
278
- });
279
- }
280
-
281
- // Override parent doFetch to be public for this class
282
- public async doFetch(path: string, options?: RequestInit): Promise<Response> {
283
- return super.doFetch(path, options);
284
- }
285
-
286
- /**
287
- * Execute an operation with automatic retry for transient errors
288
- */
289
- private async executeWithRetry<T>(operation: () => Promise<T>): Promise<T> {
290
- let lastError: Error | undefined;
291
-
292
- for (let attempt = 0; attempt < this.maxRetries; attempt++) {
293
- try {
294
- return await operation();
295
- } catch (error) {
296
- lastError = error as Error;
297
-
298
- // Check if it's a retryable error (circuit breaker or Jupyter not ready)
299
- if (this.isRetryableError(error)) {
300
- // Don't retry on the last attempt
301
- if (attempt < this.maxRetries - 1) {
302
- // Exponential backoff with jitter
303
- const delay =
304
- this.retryDelayMs * 2 ** attempt + Math.random() * 1000;
305
- await new Promise((resolve) => setTimeout(resolve, delay));
306
- continue;
307
- }
308
- }
309
-
310
- // Non-retryable error or last attempt - throw immediately
311
- throw error;
312
- }
313
- }
314
-
315
- // All retries exhausted - throw a clean error without implementation details
316
- if (lastError?.message.includes("Code execution")) {
317
- // If the error already has a clean message about code execution, use it
318
- throw lastError;
319
- }
320
-
321
- // Otherwise, throw a generic but user-friendly error
322
- throw new Error("Unable to execute code at this time");
323
- }
324
-
325
- /**
326
- * Check if an error is retryable
327
- */
328
- private isRetryableError(error: unknown): boolean {
329
- // Use the SDK's built-in retryable check
330
- if (isRetryableError(error)) {
331
- return true;
332
- }
333
-
334
- // Also check for circuit breaker specific errors
335
- if (error instanceof Error) {
336
- // Circuit breaker errors (from the container's response)
337
- if (error.message.includes("Circuit breaker is open")) {
338
- return true;
339
- }
340
-
341
- // Check if error has a status property
342
- if ("status" in error && error.status === "circuit_open") {
343
- return true;
344
- }
326
+ this.logError('parseExecutionResult', error);
345
327
  }
346
-
347
- return false;
348
328
  }
349
329
  }
@@ -0,0 +1,105 @@
1
+ import type {
2
+ PortCloseResult,
3
+ PortExposeResult,
4
+ PortListResult,
5
+ } from '@repo/shared';
6
+ import { BaseHttpClient } from './base-client';
7
+ import type { HttpClientOptions } from './types';
8
+
9
+ // Re-export for convenience
10
+ export type {
11
+ PortExposeResult,
12
+ PortCloseResult,
13
+ PortListResult,
14
+ };
15
+
16
+ /**
17
+ * Request interface for exposing ports
18
+ */
19
+ export interface ExposePortRequest {
20
+ port: number;
21
+ name?: string;
22
+ }
23
+
24
+ /**
25
+ * Request interface for unexposing ports
26
+ */
27
+ export interface UnexposePortRequest {
28
+ port: number;
29
+ }
30
+
31
+ /**
32
+ * Client for port management and preview URL operations
33
+ */
34
+ export class PortClient extends BaseHttpClient {
35
+
36
+ /**
37
+ * Expose a port and get a preview URL
38
+ * @param port - Port number to expose
39
+ * @param sessionId - The session ID for this operation
40
+ * @param name - Optional name for the port
41
+ */
42
+ async exposePort(
43
+ port: number,
44
+ sessionId: string,
45
+ name?: string
46
+ ): Promise<PortExposeResult> {
47
+ try {
48
+ const data = { port, sessionId, name };
49
+
50
+ const response = await this.post<PortExposeResult>(
51
+ '/api/expose-port',
52
+ data
53
+ );
54
+
55
+ this.logSuccess(
56
+ 'Port exposed',
57
+ `${port} exposed at ${response.url}${name ? ` (${name})` : ''}`
58
+ );
59
+
60
+ return response;
61
+ } catch (error) {
62
+ this.logError('exposePort', error);
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Unexpose a port and remove its preview URL
69
+ * @param port - Port number to unexpose
70
+ * @param sessionId - The session ID for this operation
71
+ */
72
+ async unexposePort(port: number, sessionId: string): Promise<PortCloseResult> {
73
+ try {
74
+ const url = `/api/exposed-ports/${port}?session=${encodeURIComponent(sessionId)}`;
75
+ const response = await this.delete<PortCloseResult>(url);
76
+
77
+ this.logSuccess('Port unexposed', `${port}`);
78
+ return response;
79
+ } catch (error) {
80
+ this.logError('unexposePort', error);
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Get all currently exposed ports
87
+ * @param sessionId - The session ID for this operation
88
+ */
89
+ async getExposedPorts(sessionId: string): Promise<PortListResult> {
90
+ try {
91
+ const url = `/api/exposed-ports?session=${encodeURIComponent(sessionId)}`;
92
+ const response = await this.get<PortListResult>(url);
93
+
94
+ this.logSuccess(
95
+ 'Exposed ports retrieved',
96
+ `${response.ports.length} ports exposed`
97
+ );
98
+
99
+ return response;
100
+ } catch (error) {
101
+ this.logError('getExposedPorts', error);
102
+ throw error;
103
+ }
104
+ }
105
+ }