@cloudflare/sandbox 0.4.14 → 0.4.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/sandbox",
3
- "version": "0.4.14",
3
+ "version": "0.4.16",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cloudflare/sandbox-sdk"
@@ -1,4 +1,5 @@
1
1
  import type { GitCheckoutResult } from '@repo/shared';
2
+ import { GitLogger } from '@repo/shared';
2
3
  import { BaseHttpClient } from './base-client';
3
4
  import type { HttpClientOptions, SessionRequest } from './types';
4
5
 
@@ -18,6 +19,12 @@ export interface GitCheckoutRequest extends SessionRequest {
18
19
  * Client for Git repository operations
19
20
  */
20
21
  export class GitClient extends BaseHttpClient {
22
+ constructor(options: HttpClientOptions = {}) {
23
+ super(options);
24
+ // Wrap logger with GitLogger to auto-redact credentials
25
+ this.logger = new GitLogger(this.logger);
26
+ }
27
+
21
28
  /**
22
29
  * Clone a Git repository
23
30
  * @param repoUrl - URL of the Git repository to clone
@@ -54,6 +54,10 @@ export type {
54
54
  // Utility client types
55
55
  export type {
56
56
  CommandsResponse,
57
+ CreateSessionRequest,
58
+ CreateSessionResponse,
59
+ DeleteSessionRequest,
60
+ DeleteSessionResponse,
57
61
  PingResponse,
58
62
  VersionResponse
59
63
  } from './utility-client';
@@ -41,6 +41,20 @@ export interface CreateSessionResponse extends BaseApiResponse {
41
41
  message: string;
42
42
  }
43
43
 
44
+ /**
45
+ * Request interface for deleting sessions
46
+ */
47
+ export interface DeleteSessionRequest {
48
+ sessionId: string;
49
+ }
50
+
51
+ /**
52
+ * Response interface for deleting sessions
53
+ */
54
+ export interface DeleteSessionResponse extends BaseApiResponse {
55
+ sessionId: string;
56
+ }
57
+
44
58
  /**
45
59
  * Client for health checks and utility operations
46
60
  */
@@ -100,6 +114,25 @@ export class UtilityClient extends BaseHttpClient {
100
114
  }
101
115
  }
102
116
 
117
+ /**
118
+ * Delete an execution session
119
+ * @param sessionId - Session ID to delete
120
+ */
121
+ async deleteSession(sessionId: string): Promise<DeleteSessionResponse> {
122
+ try {
123
+ const response = await this.post<DeleteSessionResponse>(
124
+ '/api/session/delete',
125
+ { sessionId }
126
+ );
127
+
128
+ this.logSuccess('Session deleted', `ID: ${sessionId}`);
129
+ return response;
130
+ } catch (error) {
131
+ this.logError('deleteSession', error);
132
+ throw error;
133
+ }
134
+ }
135
+
103
136
  /**
104
137
  * Get the container version
105
138
  * Returns the version embedded in the Docker image during build
package/src/index.ts CHANGED
@@ -38,6 +38,12 @@ export type {
38
38
  BaseApiResponse,
39
39
  CommandsResponse,
40
40
  ContainerStub,
41
+
42
+ // Utility client types
43
+ CreateSessionRequest,
44
+ CreateSessionResponse,
45
+ DeleteSessionRequest,
46
+ DeleteSessionResponse,
41
47
  ErrorResponse,
42
48
 
43
49
  // Command client types
@@ -56,8 +62,6 @@ export type {
56
62
 
57
63
  // File client types
58
64
  MkdirRequest,
59
-
60
- // Utility client types
61
65
  PingResponse,
62
66
  PortCloseResult,
63
67
  PortExposeResult,
package/src/sandbox.ts CHANGED
@@ -17,7 +17,12 @@ import type {
17
17
  SessionOptions,
18
18
  StreamOptions
19
19
  } from '@repo/shared';
20
- import { createLogger, runWithLogger, TraceContext } from '@repo/shared';
20
+ import {
21
+ createLogger,
22
+ runWithLogger,
23
+ type SessionDeleteResult,
24
+ TraceContext
25
+ } from '@repo/shared';
21
26
  import { type ExecuteResponse, SandboxClient } from './clients';
22
27
  import type { ErrorResponse } from './errors';
23
28
  import { CustomDomainRequiredError, ErrorCode } from './errors';
@@ -54,9 +59,9 @@ export function getSandbox(
54
59
  });
55
60
  }
56
61
 
57
- export function connect(
58
- stub: { fetch: (request: Request) => Promise<Response> }
59
- ) {
62
+ export function connect(stub: {
63
+ fetch: (request: Request) => Promise<Response>;
64
+ }) {
60
65
  return async (request: Request, port: number) => {
61
66
  // Validate port before routing
62
67
  if (!validatePort(port)) {
@@ -1110,6 +1115,34 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
1110
1115
  return this.getSessionWrapper(sessionId);
1111
1116
  }
1112
1117
 
1118
+ /**
1119
+ * Delete an execution session
1120
+ * Cleans up session resources and removes it from the container
1121
+ * Note: Cannot delete the default session. To reset the default session,
1122
+ * use sandbox.destroy() to terminate the entire sandbox.
1123
+ *
1124
+ * @param sessionId - The ID of the session to delete
1125
+ * @returns Result with success status, sessionId, and timestamp
1126
+ * @throws Error if attempting to delete the default session
1127
+ */
1128
+ async deleteSession(sessionId: string): Promise<SessionDeleteResult> {
1129
+ // Prevent deletion of default session
1130
+ if (this.defaultSession && sessionId === this.defaultSession) {
1131
+ throw new Error(
1132
+ `Cannot delete default session '${sessionId}'. Use sandbox.destroy() to terminate the sandbox.`
1133
+ );
1134
+ }
1135
+
1136
+ const response = await this.client.utils.deleteSession(sessionId);
1137
+
1138
+ // Map HTTP response to result type
1139
+ return {
1140
+ success: response.success,
1141
+ sessionId: response.sessionId,
1142
+ timestamp: response.timestamp
1143
+ };
1144
+ }
1145
+
1113
1146
  /**
1114
1147
  * Internal helper to create ExecutionSession wrapper for a given sessionId
1115
1148
  * Used by both createSession and getSession
package/src/version.ts CHANGED
@@ -3,4 +3,4 @@
3
3
  * This file is auto-updated by .github/changeset-version.ts during releases
4
4
  * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
5
5
  */
6
- export const SDK_VERSION = '0.4.14';
6
+ export const SDK_VERSION = '0.4.16';
@@ -412,4 +412,76 @@ describe('GitClient', () => {
412
412
  expect(fullOptionsClient).toBeInstanceOf(GitClient);
413
413
  });
414
414
  });
415
+
416
+ describe('credential redaction in logs', () => {
417
+ it('should redact credentials from URLs but leave public URLs unchanged', async () => {
418
+ const mockLogger = {
419
+ info: vi.fn(),
420
+ warn: vi.fn(),
421
+ error: vi.fn(),
422
+ debug: vi.fn(),
423
+ child: vi.fn()
424
+ };
425
+
426
+ const clientWithLogger = new GitClient({
427
+ baseUrl: 'http://test.com',
428
+ port: 3000,
429
+ logger: mockLogger
430
+ });
431
+
432
+ // Test with credentials
433
+ mockFetch.mockResolvedValueOnce(
434
+ new Response(
435
+ JSON.stringify({
436
+ success: true,
437
+ stdout: "Cloning into 'private-repo'...\nDone.",
438
+ stderr: '',
439
+ exitCode: 0,
440
+ repoUrl:
441
+ 'https://oauth2:ghp_token123@github.com/user/private-repo.git',
442
+ branch: 'main',
443
+ targetDir: '/workspace/private-repo',
444
+ timestamp: '2023-01-01T00:00:00Z'
445
+ }),
446
+ { status: 200 }
447
+ )
448
+ );
449
+
450
+ await clientWithLogger.checkout(
451
+ 'https://oauth2:ghp_token123@github.com/user/private-repo.git',
452
+ 'test-session'
453
+ );
454
+
455
+ let logDetails = mockLogger.info.mock.calls[0]?.[1]?.details;
456
+ expect(logDetails).not.toContain('ghp_token123');
457
+ expect(logDetails).toContain(
458
+ 'https://******@github.com/user/private-repo.git'
459
+ );
460
+
461
+ // Test without credentials
462
+ mockFetch.mockResolvedValueOnce(
463
+ new Response(
464
+ JSON.stringify({
465
+ success: true,
466
+ stdout: "Cloning into 'react'...\nDone.",
467
+ stderr: '',
468
+ exitCode: 0,
469
+ repoUrl: 'https://github.com/facebook/react.git',
470
+ branch: 'main',
471
+ targetDir: '/workspace/react',
472
+ timestamp: '2023-01-01T00:00:00Z'
473
+ }),
474
+ { status: 200 }
475
+ )
476
+ );
477
+
478
+ await clientWithLogger.checkout(
479
+ 'https://github.com/facebook/react.git',
480
+ 'test-session'
481
+ );
482
+
483
+ logDetails = mockLogger.info.mock.calls[1]?.[1]?.details;
484
+ expect(logDetails).toContain('https://github.com/facebook/react.git');
485
+ });
486
+ });
415
487
  });
@@ -703,4 +703,37 @@ describe('Sandbox - Automatic Session Management', () => {
703
703
  expect(url.searchParams.get('room')).toBe('lobby');
704
704
  });
705
705
  });
706
+
707
+ describe('deleteSession', () => {
708
+ it('should prevent deletion of default session', async () => {
709
+ // Trigger creation of default session
710
+ await sandbox.exec('echo "test"');
711
+
712
+ // Verify default session exists
713
+ expect((sandbox as any).defaultSession).toBeTruthy();
714
+ const defaultSessionId = (sandbox as any).defaultSession;
715
+
716
+ // Attempt to delete default session should throw
717
+ await expect(sandbox.deleteSession(defaultSessionId)).rejects.toThrow(
718
+ `Cannot delete default session '${defaultSessionId}'. Use sandbox.destroy() to terminate the sandbox.`
719
+ );
720
+ });
721
+
722
+ it('should allow deletion of non-default sessions', async () => {
723
+ // Mock the deleteSession API response
724
+ vi.spyOn(sandbox.client.utils, 'deleteSession').mockResolvedValue({
725
+ success: true,
726
+ sessionId: 'custom-session',
727
+ timestamp: new Date().toISOString()
728
+ });
729
+
730
+ // Create a custom session
731
+ await sandbox.createSession({ id: 'custom-session' });
732
+
733
+ // Should successfully delete non-default session
734
+ const result = await sandbox.deleteSession('custom-session');
735
+ expect(result.success).toBe(true);
736
+ expect(result.sessionId).toBe('custom-session');
737
+ });
738
+ });
706
739
  });