@brimble/sandbox 0.1.0
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/CODEX.md +188 -0
- package/PLAN.md +364 -0
- package/README.md +147 -0
- package/dist/package.json +23 -0
- package/dist/src/client.d.ts +23 -0
- package/dist/src/client.js +46 -0
- package/dist/src/constants.d.ts +14 -0
- package/dist/src/constants.js +21 -0
- package/dist/src/enums/code-language.d.ts +4 -0
- package/dist/src/enums/code-language.js +8 -0
- package/dist/src/enums/destroy-reason.d.ts +8 -0
- package/dist/src/enums/destroy-reason.js +12 -0
- package/dist/src/enums/destroy-timeout.d.ts +8 -0
- package/dist/src/enums/destroy-timeout.js +12 -0
- package/dist/src/enums/index.d.ts +7 -0
- package/dist/src/enums/index.js +17 -0
- package/dist/src/enums/sandbox-status.d.ts +9 -0
- package/dist/src/enums/sandbox-status.js +13 -0
- package/dist/src/enums/snapshot-mode.d.ts +4 -0
- package/dist/src/enums/snapshot-mode.js +8 -0
- package/dist/src/enums/snapshot-status.d.ts +5 -0
- package/dist/src/enums/snapshot-status.js +9 -0
- package/dist/src/enums/volume-type.d.ts +3 -0
- package/dist/src/enums/volume-type.js +7 -0
- package/dist/src/errors/index.d.ts +2 -0
- package/dist/src/errors/index.js +9 -0
- package/dist/src/errors/sandbox-api-error.d.ts +29 -0
- package/dist/src/errors/sandbox-api-error.js +48 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +40 -0
- package/dist/src/resources/exec.d.ts +19 -0
- package/dist/src/resources/exec.js +45 -0
- package/dist/src/resources/files.d.ts +16 -0
- package/dist/src/resources/files.js +41 -0
- package/dist/src/resources/index.d.ts +8 -0
- package/dist/src/resources/index.js +20 -0
- package/dist/src/resources/path.d.ts +7 -0
- package/dist/src/resources/path.js +19 -0
- package/dist/src/resources/sandbox-handle.d.ts +78 -0
- package/dist/src/resources/sandbox-handle.js +151 -0
- package/dist/src/resources/sandboxes.d.ts +64 -0
- package/dist/src/resources/sandboxes.js +224 -0
- package/dist/src/resources/scoped-sandbox.d.ts +39 -0
- package/dist/src/resources/scoped-sandbox.js +51 -0
- package/dist/src/resources/snapshots.d.ts +26 -0
- package/dist/src/resources/snapshots.js +88 -0
- package/dist/src/resources/stats.d.ts +11 -0
- package/dist/src/resources/stats.js +26 -0
- package/dist/src/resources/volumes.d.ts +21 -0
- package/dist/src/resources/volumes.js +80 -0
- package/dist/src/transport/auth.d.ts +2 -0
- package/dist/src/transport/auth.js +7 -0
- package/dist/src/transport/http.d.ts +73 -0
- package/dist/src/transport/http.js +354 -0
- package/dist/src/transport/pagination.d.ts +3 -0
- package/dist/src/transport/pagination.js +11 -0
- package/dist/src/types/exec.d.ts +34 -0
- package/dist/src/types/exec.js +2 -0
- package/dist/src/types/files.d.ts +1 -0
- package/dist/src/types/files.js +2 -0
- package/dist/src/types/index.d.ts +9 -0
- package/dist/src/types/index.js +2 -0
- package/dist/src/types/pagination.d.ts +14 -0
- package/dist/src/types/pagination.js +2 -0
- package/dist/src/types/region.d.ts +17 -0
- package/dist/src/types/region.js +2 -0
- package/dist/src/types/sandbox.d.ts +90 -0
- package/dist/src/types/sandbox.js +2 -0
- package/dist/src/types/snapshot.d.ts +15 -0
- package/dist/src/types/snapshot.js +2 -0
- package/dist/src/types/stats.d.ts +31 -0
- package/dist/src/types/stats.js +2 -0
- package/dist/src/types/template.d.ts +5 -0
- package/dist/src/types/template.js +2 -0
- package/dist/src/types/volume.d.ts +24 -0
- package/dist/src/types/volume.js +2 -0
- package/package.json +26 -0
- package/src/client.ts +61 -0
- package/src/constants.ts +17 -0
- package/src/enums/code-language.ts +4 -0
- package/src/enums/destroy-reason.ts +8 -0
- package/src/enums/destroy-timeout.ts +8 -0
- package/src/enums/index.ts +7 -0
- package/src/enums/sandbox-status.ts +9 -0
- package/src/enums/snapshot-mode.ts +4 -0
- package/src/enums/snapshot-status.ts +5 -0
- package/src/enums/volume-type.ts +3 -0
- package/src/errors/index.ts +2 -0
- package/src/errors/sandbox-api-error.ts +54 -0
- package/src/index.ts +71 -0
- package/src/resources/exec.ts +56 -0
- package/src/resources/files.ts +46 -0
- package/src/resources/index.ts +8 -0
- package/src/resources/path.ts +16 -0
- package/src/resources/sandbox-handle.ts +215 -0
- package/src/resources/sandboxes.ts +297 -0
- package/src/resources/scoped-sandbox.ts +65 -0
- package/src/resources/snapshots.ts +104 -0
- package/src/resources/stats.ts +30 -0
- package/src/resources/volumes.ts +95 -0
- package/src/transport/auth.ts +4 -0
- package/src/transport/http.ts +501 -0
- package/src/transport/pagination.ts +10 -0
- package/src/types/exec.ts +42 -0
- package/src/types/files.ts +1 -0
- package/src/types/index.ts +23 -0
- package/src/types/pagination.ts +16 -0
- package/src/types/region.ts +19 -0
- package/src/types/sandbox.ts +103 -0
- package/src/types/snapshot.ts +17 -0
- package/src/types/stats.ts +35 -0
- package/src/types/template.ts +5 -0
- package/src/types/volume.ts +26 -0
- package/test/integration/sandbox.integration.test.ts +269 -0
- package/test/unit/client.test.ts +87 -0
- package/test/unit/sandboxes.test.ts +69 -0
- package/test/unit/transport.test.ts +126 -0
- package/test/unit/volumes.test.ts +122 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +12 -0
- package/vitest.integration.config.ts +15 -0
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@brimble/sandbox",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for the Brimble Sandbox API",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "rm -rf dist && tsc -p .",
|
|
12
|
+
"test": "vitest run --config vitest.config.ts",
|
|
13
|
+
"test:watch": "vitest --config vitest.config.ts",
|
|
14
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
15
|
+
"test:all": "npm run test && npm run test:integration"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^20.19.0",
|
|
23
|
+
"typescript": "^5.9.3",
|
|
24
|
+
"vitest": "^3.2.4"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, SANDBOX_API_KEY_ENV_NAME } from './constants';
|
|
2
|
+
import { SandboxesResource, SnapshotsResource, VolumesResource } from './resources';
|
|
3
|
+
import { HttpTransport } from './transport/http';
|
|
4
|
+
import type { RetryOptions } from './transport/http';
|
|
5
|
+
|
|
6
|
+
export type SandboxClientOptions = {
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
retry?: RetryOptions;
|
|
11
|
+
fetchImpl?: typeof fetch;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolves the API key from the constructor options first,
|
|
16
|
+
* then falls back to BRIMBLE_SANDBOX_KEY.
|
|
17
|
+
*/
|
|
18
|
+
function resolveApiKey(options: SandboxClientOptions): string {
|
|
19
|
+
if (options.apiKey) {
|
|
20
|
+
return options.apiKey;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const apiKeyFromEnv = process.env[SANDBOX_API_KEY_ENV_NAME];
|
|
24
|
+
|
|
25
|
+
if (apiKeyFromEnv) {
|
|
26
|
+
return apiKeyFromEnv;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Sandbox API key is required. Pass "apiKey" explicitly or set ${SANDBOX_API_KEY_ENV_NAME} in your environment.`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class SandboxClient {
|
|
35
|
+
/** Access sandbox lifecycle and per-sandbox scoped operations. */
|
|
36
|
+
public readonly sandboxes: SandboxesResource;
|
|
37
|
+
/** Access account-level snapshot operations. */
|
|
38
|
+
public readonly snapshots: SnapshotsResource;
|
|
39
|
+
/** Access volume lifecycle operations. */
|
|
40
|
+
public readonly volumes: VolumesResource;
|
|
41
|
+
|
|
42
|
+
private readonly transport: HttpTransport;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates a client instance for the Brimble Sandbox API.
|
|
46
|
+
* Pass `apiKey` directly or set `BRIMBLE_SANDBOX_KEY` in your environment.
|
|
47
|
+
*/
|
|
48
|
+
public constructor(options: SandboxClientOptions = {}) {
|
|
49
|
+
this.transport = new HttpTransport({
|
|
50
|
+
apiKey: resolveApiKey(options),
|
|
51
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
52
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
53
|
+
retry: options.retry,
|
|
54
|
+
fetchImpl: options.fetchImpl,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
this.sandboxes = new SandboxesResource(this.transport);
|
|
58
|
+
this.snapshots = new SnapshotsResource(this.transport);
|
|
59
|
+
this.volumes = new VolumesResource(this.transport);
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import packageJson from '../package.json';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_BASE_URL = 'https://sandbox.brimble.io';
|
|
4
|
+
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
5
|
+
export const SANDBOX_API_KEY_ENV_NAME = 'BRIMBLE_SANDBOX_KEY';
|
|
6
|
+
export const SDK_PACKAGE_VERSION = packageJson.version;
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_PAGE = 1;
|
|
9
|
+
export const DEFAULT_PAGE_LIMIT = 15;
|
|
10
|
+
export const MAX_PAGE_LIMIT = 100;
|
|
11
|
+
export const MIN_VOLUME_SIZE_GB = 10;
|
|
12
|
+
export const DEFAULT_SANDBOX_READY_TIMEOUT_MS = 60_000;
|
|
13
|
+
export const DEFAULT_SANDBOX_READY_POLL_INTERVAL_MS = 2_000;
|
|
14
|
+
export const DEFAULT_RETRY_MAX_ATTEMPTS = 1;
|
|
15
|
+
export const DEFAULT_RETRY_BASE_DELAY_MS = 300;
|
|
16
|
+
export const DEFAULT_RETRY_MAX_DELAY_MS = 3_000;
|
|
17
|
+
export const DEFAULT_RETRY_STATUSES = [408, 429, 500, 502, 503, 504] as const;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { CodeLanguage } from './code-language';
|
|
2
|
+
export { DestroyReason } from './destroy-reason';
|
|
3
|
+
export { DestroyTimeout } from './destroy-timeout';
|
|
4
|
+
export { SandboxStatus } from './sandbox-status';
|
|
5
|
+
export { SnapshotMode } from './snapshot-mode';
|
|
6
|
+
export { SnapshotStatus } from './snapshot-status';
|
|
7
|
+
export { VolumeType } from './volume-type';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type SandboxApiErrorArgs = {
|
|
2
|
+
status: number;
|
|
3
|
+
message: string;
|
|
4
|
+
endpoint: string;
|
|
5
|
+
responseBody: unknown;
|
|
6
|
+
requestId?: string | null;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export class SandboxApiError extends Error {
|
|
10
|
+
public readonly status: number;
|
|
11
|
+
public readonly endpoint: string;
|
|
12
|
+
public readonly responseBody: unknown;
|
|
13
|
+
public readonly requestId: string | null;
|
|
14
|
+
|
|
15
|
+
public constructor(args: SandboxApiErrorArgs) {
|
|
16
|
+
super(args.message);
|
|
17
|
+
this.name = 'SandboxApiError';
|
|
18
|
+
this.status = args.status;
|
|
19
|
+
this.endpoint = args.endpoint;
|
|
20
|
+
this.responseBody = args.responseBody;
|
|
21
|
+
this.requestId = args.requestId ?? null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class AuthError extends SandboxApiError {
|
|
26
|
+
public constructor(args: SandboxApiErrorArgs) {
|
|
27
|
+
super(args);
|
|
28
|
+
this.name = 'AuthError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ValidationError extends SandboxApiError {
|
|
33
|
+
public constructor(args: SandboxApiErrorArgs) {
|
|
34
|
+
super(args);
|
|
35
|
+
this.name = 'ValidationError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class NotFoundError extends SandboxApiError {
|
|
40
|
+
public constructor(args: SandboxApiErrorArgs) {
|
|
41
|
+
super(args);
|
|
42
|
+
this.name = 'NotFoundError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class RateLimitError extends SandboxApiError {
|
|
47
|
+
public readonly retryAfterSeconds: number | null;
|
|
48
|
+
|
|
49
|
+
public constructor(args: SandboxApiErrorArgs & { retryAfterSeconds?: number | null }) {
|
|
50
|
+
super(args);
|
|
51
|
+
this.name = 'RateLimitError';
|
|
52
|
+
this.retryAfterSeconds = args.retryAfterSeconds ?? null;
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DEFAULT_BASE_URL,
|
|
3
|
+
DEFAULT_PAGE,
|
|
4
|
+
DEFAULT_PAGE_LIMIT,
|
|
5
|
+
DEFAULT_RETRY_BASE_DELAY_MS,
|
|
6
|
+
DEFAULT_RETRY_MAX_ATTEMPTS,
|
|
7
|
+
DEFAULT_RETRY_MAX_DELAY_MS,
|
|
8
|
+
DEFAULT_RETRY_STATUSES,
|
|
9
|
+
DEFAULT_TIMEOUT_MS,
|
|
10
|
+
MAX_PAGE_LIMIT,
|
|
11
|
+
SANDBOX_API_KEY_ENV_NAME,
|
|
12
|
+
} from './constants';
|
|
13
|
+
export { SandboxClient } from './client';
|
|
14
|
+
export type { SandboxClientOptions } from './client';
|
|
15
|
+
|
|
16
|
+
export { AuthError, NotFoundError, RateLimitError, SandboxApiError, ValidationError } from './errors';
|
|
17
|
+
export type { SandboxApiErrorArgs } from './errors';
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
CodeLanguage,
|
|
21
|
+
DestroyReason,
|
|
22
|
+
DestroyTimeout,
|
|
23
|
+
SandboxStatus,
|
|
24
|
+
SnapshotMode,
|
|
25
|
+
SnapshotStatus,
|
|
26
|
+
VolumeType,
|
|
27
|
+
} from './enums';
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
ExecResource,
|
|
31
|
+
FilesResource,
|
|
32
|
+
SandboxHandle,
|
|
33
|
+
SandboxesResource,
|
|
34
|
+
ScopedSandboxResource,
|
|
35
|
+
SnapshotScopeResource,
|
|
36
|
+
SnapshotsResource,
|
|
37
|
+
StatsResource,
|
|
38
|
+
VolumesResource,
|
|
39
|
+
} from './resources';
|
|
40
|
+
|
|
41
|
+
export type {
|
|
42
|
+
AckMessage,
|
|
43
|
+
CodeInput,
|
|
44
|
+
CreateSandboxInput,
|
|
45
|
+
CreateSandboxResult,
|
|
46
|
+
CreateSnapshotInput,
|
|
47
|
+
CreateVolumeInput,
|
|
48
|
+
ExecInput,
|
|
49
|
+
ExecResult,
|
|
50
|
+
ExecStreamFrame,
|
|
51
|
+
FileUploadBody,
|
|
52
|
+
Paginated,
|
|
53
|
+
Pagination,
|
|
54
|
+
RegionSummary,
|
|
55
|
+
SandboxRegion,
|
|
56
|
+
SandboxRegionsResult,
|
|
57
|
+
Sandbox,
|
|
58
|
+
SandboxSpecs,
|
|
59
|
+
Snapshot,
|
|
60
|
+
Stats,
|
|
61
|
+
StatsAverageNetwork,
|
|
62
|
+
StatsAverageNumeric,
|
|
63
|
+
StatsQuery,
|
|
64
|
+
StatsTimelinePoint,
|
|
65
|
+
TeamScopedPagination,
|
|
66
|
+
WaitUntilReadyOptions,
|
|
67
|
+
Volume,
|
|
68
|
+
} from './types';
|
|
69
|
+
|
|
70
|
+
export type { RequestOptions } from './transport/http';
|
|
71
|
+
export type { RetryOptions } from './transport/http';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { CodeInput, ExecInput, ExecResult } from '../types';
|
|
2
|
+
import type { RequestOptions } from '../transport/http';
|
|
3
|
+
import { HttpTransport } from '../transport/http';
|
|
4
|
+
|
|
5
|
+
export class ExecResource {
|
|
6
|
+
private readonly transport: HttpTransport;
|
|
7
|
+
private readonly sandboxId: string;
|
|
8
|
+
|
|
9
|
+
/** @internal Create the exec/code runner wrapper for one sandbox. */
|
|
10
|
+
public constructor(transport: HttpTransport, sandboxId: string) {
|
|
11
|
+
this.transport = transport;
|
|
12
|
+
this.sandboxId = sandboxId;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Run a shell command in the sandbox. */
|
|
16
|
+
public exec(input: ExecInput & { stream: true }, options?: RequestOptions): Promise<ReadableStream<Uint8Array>>;
|
|
17
|
+
public exec(input: ExecInput, options?: RequestOptions): Promise<ExecResult>;
|
|
18
|
+
public exec(input: ExecInput, options?: RequestOptions): Promise<ExecResult | ReadableStream<Uint8Array>> {
|
|
19
|
+
if (input.stream === true) {
|
|
20
|
+
return this.transport.requestJsonStream({
|
|
21
|
+
endpoint: `/sandboxes/${this.sandboxId}/exec`,
|
|
22
|
+
method: 'POST',
|
|
23
|
+
body: input,
|
|
24
|
+
...options,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return this.transport.requestJson<ExecResult>({
|
|
29
|
+
endpoint: `/sandboxes/${this.sandboxId}/exec`,
|
|
30
|
+
method: 'POST',
|
|
31
|
+
body: input,
|
|
32
|
+
...options,
|
|
33
|
+
}) as Promise<ExecResult>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Run a code snippet in the sandbox. */
|
|
37
|
+
public runCode(input: CodeInput & { stream: true }, options?: RequestOptions): Promise<ReadableStream<Uint8Array>>;
|
|
38
|
+
public runCode(input: CodeInput, options?: RequestOptions): Promise<ExecResult>;
|
|
39
|
+
public runCode(input: CodeInput, options?: RequestOptions): Promise<ExecResult | ReadableStream<Uint8Array>> {
|
|
40
|
+
if (input.stream === true) {
|
|
41
|
+
return this.transport.requestJsonStream({
|
|
42
|
+
endpoint: `/sandboxes/${this.sandboxId}/code`,
|
|
43
|
+
method: 'POST',
|
|
44
|
+
body: input,
|
|
45
|
+
...options,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return this.transport.requestJson<ExecResult>({
|
|
50
|
+
endpoint: `/sandboxes/${this.sandboxId}/code`,
|
|
51
|
+
method: 'POST',
|
|
52
|
+
body: input,
|
|
53
|
+
...options,
|
|
54
|
+
}) as Promise<ExecResult>;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { FileUploadBody } from '../types';
|
|
2
|
+
import type { RequestOptions } from '../transport/http';
|
|
3
|
+
import { HttpTransport } from '../transport/http';
|
|
4
|
+
import { encodeFilePath } from './path';
|
|
5
|
+
|
|
6
|
+
export class FilesResource {
|
|
7
|
+
private readonly transport: HttpTransport;
|
|
8
|
+
private readonly sandboxId: string;
|
|
9
|
+
|
|
10
|
+
/** @internal Create the files wrapper for one sandbox. */
|
|
11
|
+
public constructor(transport: HttpTransport, sandboxId: string) {
|
|
12
|
+
this.transport = transport;
|
|
13
|
+
this.sandboxId = sandboxId;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Upload file bytes to a path inside the sandbox.
|
|
18
|
+
* Tip: pass a Buffer/Uint8Array when you can so Content-Length is set automatically.
|
|
19
|
+
*/
|
|
20
|
+
public async put(path: string, body: FileUploadBody, options?: RequestOptions): Promise<void> {
|
|
21
|
+
const headers: Record<string, string> = {
|
|
22
|
+
'content-type': 'application/octet-stream',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (Buffer.isBuffer(body) || body instanceof Uint8Array) {
|
|
26
|
+
headers['content-length'] = String(body.byteLength);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await this.transport.requestBinary({
|
|
30
|
+
endpoint: `/sandboxes/${this.sandboxId}/files/${encodeFilePath(path)}`,
|
|
31
|
+
method: 'PUT',
|
|
32
|
+
body,
|
|
33
|
+
headers,
|
|
34
|
+
...options,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Download a file from the sandbox as a stream. */
|
|
39
|
+
public get(path: string, options?: RequestOptions): Promise<ReadableStream<Uint8Array>> {
|
|
40
|
+
return this.transport.requestStream({
|
|
41
|
+
endpoint: `/sandboxes/${this.sandboxId}/files/${encodeFilePath(path)}`,
|
|
42
|
+
method: 'GET',
|
|
43
|
+
...options,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { ExecResource } from './exec';
|
|
2
|
+
export { FilesResource } from './files';
|
|
3
|
+
export { SandboxHandle } from './sandbox-handle';
|
|
4
|
+
export { SandboxesResource } from './sandboxes';
|
|
5
|
+
export { ScopedSandboxResource } from './scoped-sandbox';
|
|
6
|
+
export { SnapshotScopeResource, SnapshotsResource } from './snapshots';
|
|
7
|
+
export { StatsResource } from './stats';
|
|
8
|
+
export { VolumesResource } from './volumes';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Encode a single path segment safely for URL usage. */
|
|
2
|
+
export function encodePathSegment(value: string): string {
|
|
3
|
+
return encodeURIComponent(value);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Encode a sandbox file path without encoding forward slashes.
|
|
8
|
+
* Example: `tmp/my file.txt` -> `tmp/my%20file.txt`
|
|
9
|
+
*/
|
|
10
|
+
export function encodeFilePath(path: string): string {
|
|
11
|
+
return path
|
|
12
|
+
.split('/')
|
|
13
|
+
.filter((segment) => segment.length > 0)
|
|
14
|
+
.map(encodePathSegment)
|
|
15
|
+
.join('/');
|
|
16
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_SANDBOX_READY_POLL_INTERVAL_MS,
|
|
3
|
+
DEFAULT_SANDBOX_READY_TIMEOUT_MS,
|
|
4
|
+
} from '../constants';
|
|
5
|
+
import { SandboxStatus } from '../enums';
|
|
6
|
+
import type { RequestOptions } from '../transport/http';
|
|
7
|
+
import type {
|
|
8
|
+
AckMessage,
|
|
9
|
+
CodeInput,
|
|
10
|
+
CreateSandboxResult,
|
|
11
|
+
CreateSnapshotInput,
|
|
12
|
+
ExecInput,
|
|
13
|
+
ExecResult,
|
|
14
|
+
FileUploadBody,
|
|
15
|
+
Paginated,
|
|
16
|
+
Pagination,
|
|
17
|
+
Sandbox,
|
|
18
|
+
SandboxRuntimeOptions,
|
|
19
|
+
Snapshot,
|
|
20
|
+
Stats,
|
|
21
|
+
StatsQuery,
|
|
22
|
+
WaitPreference,
|
|
23
|
+
WaitUntilReadyOptions,
|
|
24
|
+
} from '../types';
|
|
25
|
+
import { ScopedSandboxResource } from './scoped-sandbox';
|
|
26
|
+
import type { SandboxesResource } from './sandboxes';
|
|
27
|
+
|
|
28
|
+
function delay(ms: number): Promise<void> {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
setTimeout(resolve, ms);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class SandboxHandle {
|
|
35
|
+
private readonly sandboxes: SandboxesResource;
|
|
36
|
+
private readonly scope: ScopedSandboxResource;
|
|
37
|
+
private sandboxState: Sandbox | CreateSandboxResult;
|
|
38
|
+
|
|
39
|
+
/** Snapshot operations grouped under a dedicated namespace. */
|
|
40
|
+
public readonly snapshots: {
|
|
41
|
+
create: (input: CreateSnapshotInput, options?: RequestOptions) => Promise<Snapshot>;
|
|
42
|
+
list: (query?: Pagination, options?: RequestOptions) => Promise<Paginated<Snapshot>>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** @internal Create a sandbox handle from create/get responses. */
|
|
46
|
+
public constructor(sandboxes: SandboxesResource, state: Sandbox | CreateSandboxResult) {
|
|
47
|
+
this.sandboxes = sandboxes;
|
|
48
|
+
this.sandboxState = state;
|
|
49
|
+
this.scope = this.sandboxes.use(state.id);
|
|
50
|
+
|
|
51
|
+
this.snapshots = {
|
|
52
|
+
create: (input, options) => this.createSnapshot(input, options),
|
|
53
|
+
list: (query = {}, options) => this.listSnapshots(query, options),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Current sandbox id. */
|
|
58
|
+
public get id(): string {
|
|
59
|
+
return this.sandboxState.id;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Current cached sandbox status. */
|
|
63
|
+
public get status(): SandboxStatus {
|
|
64
|
+
return this.sandboxState.status;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Current cached sandbox payload. */
|
|
68
|
+
public get data(): Sandbox | CreateSandboxResult {
|
|
69
|
+
return this.sandboxState;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Refresh sandbox details from the API and update local state. */
|
|
73
|
+
public async refresh(options?: RequestOptions): Promise<Sandbox> {
|
|
74
|
+
const sandbox = await this.sandboxes.getData(this.id, options);
|
|
75
|
+
this.sandboxState = sandbox;
|
|
76
|
+
return sandbox;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Destroy this sandbox (idempotent). */
|
|
80
|
+
public async destroy(options?: RequestOptions): Promise<void> {
|
|
81
|
+
await this.sandboxes.destroy(this.id, options);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Request pause for this sandbox and refresh cached state. */
|
|
85
|
+
public async pause(options?: RequestOptions): Promise<AckMessage | undefined> {
|
|
86
|
+
const response = await this.sandboxes.pause(this.id, options);
|
|
87
|
+
await this.refresh(options);
|
|
88
|
+
return response;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Request resume for this sandbox and refresh cached state. */
|
|
92
|
+
public async resume(options?: RequestOptions): Promise<AckMessage | undefined> {
|
|
93
|
+
const response = await this.sandboxes.resume(this.id, options);
|
|
94
|
+
await this.refresh(options);
|
|
95
|
+
return response;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Poll sandbox status until it becomes `ready`.
|
|
100
|
+
* Throws on timeout or when `signal` is aborted.
|
|
101
|
+
*/
|
|
102
|
+
public async waitUntilReady(options: WaitUntilReadyOptions = {}): Promise<Sandbox> {
|
|
103
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_SANDBOX_READY_TIMEOUT_MS;
|
|
104
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_SANDBOX_READY_POLL_INTERVAL_MS;
|
|
105
|
+
const deadline = Date.now() + timeoutMs;
|
|
106
|
+
|
|
107
|
+
while (true) {
|
|
108
|
+
if (options.signal?.aborted) {
|
|
109
|
+
throw new Error('waitUntilReady aborted by signal');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const sandbox = await this.refresh({ signal: options.signal });
|
|
113
|
+
if (sandbox.status === SandboxStatus.Ready) {
|
|
114
|
+
return sandbox;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (Date.now() >= deadline) {
|
|
118
|
+
throw new Error(`Sandbox ${this.id} did not become ready within ${timeoutMs}ms`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await delay(pollIntervalMs);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Run a shell command in this sandbox.
|
|
127
|
+
* By default this throws when not ready; set `waitUntilReady` to auto-wait.
|
|
128
|
+
*/
|
|
129
|
+
public exec(input: ExecInput & { stream: true }, options?: SandboxRuntimeOptions): Promise<ReadableStream<Uint8Array>>;
|
|
130
|
+
public exec(input: ExecInput, options?: SandboxRuntimeOptions): Promise<ExecResult>;
|
|
131
|
+
public async exec(input: ExecInput, options: SandboxRuntimeOptions = {}): Promise<ExecResult | ReadableStream<Uint8Array>> {
|
|
132
|
+
await this.ensureReady(options.waitUntilReady);
|
|
133
|
+
return this.scope.exec(input, options);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Run a code snippet in this sandbox.
|
|
138
|
+
* By default this throws when not ready; set `waitUntilReady` to auto-wait.
|
|
139
|
+
*/
|
|
140
|
+
public runCode(input: CodeInput & { stream: true }, options?: SandboxRuntimeOptions): Promise<ReadableStream<Uint8Array>>;
|
|
141
|
+
public runCode(input: CodeInput, options?: SandboxRuntimeOptions): Promise<ExecResult>;
|
|
142
|
+
public async runCode(input: CodeInput, options: SandboxRuntimeOptions = {}): Promise<ExecResult | ReadableStream<Uint8Array>> {
|
|
143
|
+
await this.ensureReady(options.waitUntilReady);
|
|
144
|
+
return this.scope.runCode(input, options);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Upload a file into this sandbox.
|
|
149
|
+
* By default this throws when not ready; set `waitUntilReady` to auto-wait.
|
|
150
|
+
*/
|
|
151
|
+
public async putFile(path: string, body: FileUploadBody, options: SandboxRuntimeOptions = {}): Promise<void> {
|
|
152
|
+
await this.ensureReady(options.waitUntilReady);
|
|
153
|
+
await this.scope.putFile(path, body, options);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Download a file from this sandbox.
|
|
158
|
+
* By default this throws when not ready; set `waitUntilReady` to auto-wait.
|
|
159
|
+
*/
|
|
160
|
+
public async getFile(path: string, options: SandboxRuntimeOptions = {}): Promise<ReadableStream<Uint8Array>> {
|
|
161
|
+
await this.ensureReady(options.waitUntilReady);
|
|
162
|
+
return this.scope.getFile(path, options);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Fetch usage stats for this sandbox.
|
|
167
|
+
* By default this throws when not ready; set `waitUntilReady` to auto-wait.
|
|
168
|
+
*/
|
|
169
|
+
public async stats(query: StatsQuery = {}, options: SandboxRuntimeOptions = {}): Promise<Stats> {
|
|
170
|
+
await this.ensureReady(options.waitUntilReady);
|
|
171
|
+
return this.scope.stats(query, options);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Create a snapshot for this sandbox.
|
|
176
|
+
* By default this throws when not ready; set `waitUntilReady` to auto-wait.
|
|
177
|
+
*/
|
|
178
|
+
public async createSnapshot(input: CreateSnapshotInput, options: SandboxRuntimeOptions = {}): Promise<Snapshot> {
|
|
179
|
+
await this.ensureReady(options.waitUntilReady);
|
|
180
|
+
return this.scope.createSnapshot(input, options);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* List snapshots for this sandbox.
|
|
185
|
+
* By default this throws when not ready; set `waitUntilReady` to auto-wait.
|
|
186
|
+
*/
|
|
187
|
+
public async listSnapshots(query: Pagination = {}, options: SandboxRuntimeOptions = {}): Promise<Paginated<Snapshot>> {
|
|
188
|
+
await this.ensureReady(options.waitUntilReady);
|
|
189
|
+
return this.scope.listSnapshots(query, options);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async ensureReady(waitUntilReady: WaitPreference | undefined): Promise<void> {
|
|
193
|
+
if (this.status === SandboxStatus.Ready) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (waitUntilReady) {
|
|
198
|
+
if (typeof waitUntilReady === 'object') {
|
|
199
|
+
await this.waitUntilReady(waitUntilReady);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await this.waitUntilReady();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.assertReady();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private assertReady(): void {
|
|
211
|
+
if (this.status !== SandboxStatus.Ready) {
|
|
212
|
+
throw new Error(`Sandbox ${this.id} is ${this.status}. Call waitUntilReady() or refresh() before runtime operations.`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|