@cloudflare/sandbox 0.4.15 → 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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +12 -0
- package/Dockerfile +2 -2
- package/dist/index.d.ts +386 -322
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +136 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/clients/git-client.ts +7 -0
- package/src/clients/index.ts +4 -0
- package/src/clients/utility-client.ts +33 -0
- package/src/index.ts +6 -2
- package/src/sandbox.ts +37 -4
- package/src/version.ts +1 -1
- package/tests/git-client.test.ts +72 -0
- package/tests/sandbox.test.ts +33 -0
package/package.json
CHANGED
|
@@ -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
|
package/src/clients/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
package/tests/git-client.test.ts
CHANGED
|
@@ -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
|
});
|
package/tests/sandbox.test.ts
CHANGED
|
@@ -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
|
});
|