@sandbox-kit/sandbox 0.0.0-stage → 0.1.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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -2
  3. package/dist/client/client.d.ts +17 -0
  4. package/dist/client/client.js +222 -0
  5. package/dist/client/errors.d.ts +35 -0
  6. package/dist/client/errors.js +88 -0
  7. package/dist/client/operation.d.ts +12 -0
  8. package/dist/client/operation.js +73 -0
  9. package/dist/client/sandbox.d.ts +33 -0
  10. package/dist/client/sandbox.js +160 -0
  11. package/dist/client/types.d.ts +63 -0
  12. package/dist/client/types.js +3 -0
  13. package/dist/client/validation.d.ts +9 -0
  14. package/dist/client/validation.js +221 -0
  15. package/dist/contracts/config_paths.d.ts +44 -0
  16. package/dist/contracts/config_paths.js +50 -0
  17. package/dist/contracts/field_paths.d.ts +216 -0
  18. package/dist/contracts/field_paths.js +222 -0
  19. package/dist/contracts/sandbox_data_contracts.d.ts +206 -0
  20. package/dist/contracts/sandbox_data_contracts.js +22 -0
  21. package/dist/index.d.ts +6 -0
  22. package/dist/index.js +6 -0
  23. package/dist/internal/duration_ms.d.ts +15 -0
  24. package/dist/internal/duration_ms.js +41 -0
  25. package/dist/internal/input_remap.d.ts +36 -0
  26. package/dist/internal/input_remap.js +1 -0
  27. package/dist/internal/net_validate.d.ts +4 -0
  28. package/dist/internal/net_validate.js +53 -0
  29. package/dist/internal/oneof_input.d.ts +32 -0
  30. package/dist/internal/oneof_input.js +1 -0
  31. package/dist/internal/proto_enum.d.ts +25 -0
  32. package/dist/internal/proto_enum.js +25 -0
  33. package/dist/internal/strip_proto.d.ts +46 -0
  34. package/dist/internal/strip_proto.js +1 -0
  35. package/dist/metadata/metadata_object.d.ts +7 -0
  36. package/dist/metadata/metadata_object.js +103 -0
  37. package/dist/metadata/metadata_plain.d.ts +9 -0
  38. package/dist/metadata/metadata_plain.js +1 -0
  39. package/package.json +71 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sandbox-kit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,130 @@
1
- # Temporary Holding Version
1
+ # `@sandbox-kit/sandbox`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Create and manage cloud sandboxes from one TypeScript API. Pick a provider
4
+ package (`@sandbox-kit/daytona` or `@sandbox-kit/modal`), construct a client,
5
+ then create or open sandboxes through the same methods.
6
+
7
+ Closing a client or handle releases **local** SDK resources only. It does not
8
+ stop, terminate, or delete the remote sandbox.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm install @sandbox-kit/sandbox @sandbox-kit/daytona
14
+ # or
15
+ npm install @sandbox-kit/sandbox @sandbox-kit/modal
16
+ ```
17
+
18
+ Requires Node.js 20, 22, or 24.15+.
19
+
20
+ ## Quick start
21
+
22
+ ```ts
23
+ import { daytona } from "@sandbox-kit/daytona";
24
+ import { Client } from "@sandbox-kit/sandbox";
25
+
26
+ await using client = new Client({
27
+ provider: daytona(),
28
+ auth: { apiKey: { key: process.env.DAYTONA_API_KEY! } },
29
+ timeout: 120_000,
30
+ });
31
+
32
+ const sandbox = await client.create();
33
+ console.log(sandbox.id, client.providerName);
34
+ ```
35
+
36
+ Prefer `await using` when available. Otherwise call `await client.close()` in a
37
+ `finally` block.
38
+
39
+ ## Client configuration
40
+
41
+ Pass the provider on `ClientConfig`, then shared settings:
42
+
43
+ | Field | Purpose |
44
+ | ---------- | --------------------------------------------------------------- |
45
+ | `provider` | Provider factory (`daytona()`, `modal()`, …) |
46
+ | `auth` | One credential shape (`apiKey`, `bearerToken`, `tokenPair`, …) |
47
+ | `scope` | App, environment, organization, or project (provider-dependent) |
48
+ | `endpoint` | API URL when the provider supports it |
49
+ | `region` | Target or placement preference |
50
+ | `timeout` | Default operation deadline in **milliseconds** |
51
+
52
+ Omit `auth` when the provider SDK should resolve credentials from its own
53
+ environment. Provider-specific auth and scope rules live in each provider
54
+ package README.
55
+
56
+ ## Create a sandbox
57
+
58
+ ```ts
59
+ const sandbox = await client.create({
60
+ source: { image: { reference: "daytonaio/playground:latest" } },
61
+ provisioning: { timeout: 120_000 },
62
+ });
63
+ ```
64
+
65
+ `create()` with no options uses the provider default when that is valid (for
66
+ example Daytona's default snapshot). Timeouts and delays are millisecond
67
+ numbers. Nested exclusive fields use shapes like `source: { image: … }` or
68
+ `auth: { apiKey: … }`.
69
+
70
+ ## Open an existing sandbox
71
+
72
+ ```ts
73
+ const attached = await client.attach(id); // provider must support attach
74
+ const fetched = await client.get(id); // provider-verified lookup
75
+ ```
76
+
77
+ ## Sandbox handle
78
+
79
+ | Method | Purpose |
80
+ | ---------------- | --------------------------------------------- |
81
+ | `id` | Sandbox identifier |
82
+ | `info()` | Copied metadata |
83
+ | `capabilities()` | Lifecycle operations this handle can run |
84
+ | `supports(...)` | Check one capability |
85
+ | `refresh` | Refresh metadata from the provider |
86
+ | `start` | Start |
87
+ | `stop` | Stop (`force` when supported) |
88
+ | `pause` | Pause |
89
+ | `archive` | Archive |
90
+ | `delete` | Delete (`wait` when supported) |
91
+ | `terminate` | Terminate (`wait` when supported) |
92
+ | `close` | Drop the local handle (not the cloud sandbox) |
93
+
94
+ Unsupported operations throw before the provider call. Use `supports(...)` when
95
+ the sandbox class may differ (for example Daytona container vs VM).
96
+
97
+ ## Errors
98
+
99
+ Failures throw `SandboxError` with `kind`, `provider`, `operation`, `field`,
100
+ and `message`:
101
+
102
+ ```ts
103
+ import { Client, SandboxError } from "@sandbox-kit/sandbox";
104
+
105
+ try {
106
+ await client.create({ resources: { cpuCores: -1 } });
107
+ } catch (error) {
108
+ if (error instanceof SandboxError) {
109
+ console.error(error.kind, error.field, error.message);
110
+ } else {
111
+ throw error;
112
+ }
113
+ }
114
+ ```
115
+
116
+ ## Providers
117
+
118
+ | Package | Role |
119
+ | ---------------------- | ----------------- |
120
+ | `@sandbox-kit/daytona` | Daytona sandboxes |
121
+ | `@sandbox-kit/modal` | Modal sandboxes |
122
+
123
+ ## Examples
124
+
125
+ Runnable projects:
126
+ [examples/typescript](https://github.com/sandbox-kit/kit/tree/main/examples/typescript).
127
+
128
+ ## License
129
+
130
+ MIT
@@ -0,0 +1,17 @@
1
+ import type { CreateOptions } from "#contracts/sandbox_data_contracts.js";
2
+ import { Sandbox } from "#client/sandbox.js";
3
+ import type { ClientConfig, OperationOptions } from "#client/types.js";
4
+ export type { APIKeyCredentials, AuthInput, Backend, BearerTokenCredentials, ClientConfig, Config, DeleteCallOptions, OAuthCredentials, OperationOptions, Provider, Scope, StopCallOptions, TerminateCallOptions, TokenPairCredentials, } from "#client/types.js";
5
+ export { ErrorKind, SandboxError, sandboxError } from "#client/errors.js";
6
+ export { Sandbox } from "#client/sandbox.js";
7
+ export { validateConfig, validateCreateOptions } from "#client/validation.js";
8
+ export declare class Client implements AsyncDisposable {
9
+ #private;
10
+ constructor(config: ClientConfig);
11
+ get providerName(): string;
12
+ attach(id: string, options?: OperationOptions): Promise<Sandbox>;
13
+ get(id: string, options?: OperationOptions): Promise<Sandbox>;
14
+ create(request?: CreateOptions, options?: OperationOptions): Promise<Sandbox>;
15
+ close(options?: OperationOptions): Promise<void>;
16
+ [Symbol.asyncDispose](): Promise<void>;
17
+ }
@@ -0,0 +1,222 @@
1
+ // Client runtime behavior is handwritten. Portable request and response data
2
+ // remain generated from protobuf. This module owns construction and create /
3
+ // attach / get / close; validation, errors, and the Sandbox handle live beside it.
4
+ import { InfoFieldId } from "#contracts/field_paths.js";
5
+ import { ConfigFieldProvider } from "#contracts/config_paths.js";
6
+ import { ErrorKind, sandboxError, unsupported, withContext, } from "#client/errors.js";
7
+ import { awaitWithSignal, operationOptions, operationSignal, } from "#client/operation.js";
8
+ import { Sandbox, sandboxConstructorToken, } from "#client/sandbox.js";
9
+ import { validateConfig, validateCreateOptions } from "#client/validation.js";
10
+ export { ErrorKind, SandboxError, sandboxError } from "#client/errors.js";
11
+ export { Sandbox } from "#client/sandbox.js";
12
+ export { validateConfig, validateCreateOptions } from "#client/validation.js";
13
+ export class Client {
14
+ #backend;
15
+ #providerName;
16
+ #timeout;
17
+ #lifecycle;
18
+ #active = 0;
19
+ #closing = false;
20
+ #closed = false;
21
+ #closePromise;
22
+ #drain;
23
+ #resolveDrain;
24
+ constructor(config) {
25
+ if (!config.provider?.name?.trim())
26
+ throw sandboxError({
27
+ kind: ErrorKind.InvalidArgument,
28
+ provider: "",
29
+ operation: "initialize",
30
+ field: ConfigFieldProvider,
31
+ message: "sandbox-kit: provider is required",
32
+ });
33
+ validateConfig(config, config.provider.name, "initialize");
34
+ this.#providerName = config.provider.name;
35
+ this.#timeout = config.timeout;
36
+ this.#lifecycle = {
37
+ begin: (operation) => this.#begin(operation),
38
+ isClosed: () => this.#closing || this.#closed,
39
+ };
40
+ try {
41
+ const { provider: _provider, ...shared } = config;
42
+ this.#backend = config.provider.initialize(shared);
43
+ }
44
+ catch (error) {
45
+ throw withContext(error, config.provider.name, "initialize");
46
+ }
47
+ if (!this.#backend)
48
+ throw sandboxError({
49
+ kind: ErrorKind.InvalidResponse,
50
+ provider: config.provider.name,
51
+ operation: "initialize",
52
+ field: ConfigFieldProvider,
53
+ message: "sandbox-kit: provider returned no backend",
54
+ });
55
+ if (this.#backend.name !== this.#providerName) {
56
+ void this.#backend.close({}).catch(() => { });
57
+ throw sandboxError({
58
+ kind: ErrorKind.InvalidResponse,
59
+ provider: this.#providerName,
60
+ operation: "initialize",
61
+ field: ConfigFieldProvider,
62
+ message: "sandbox-kit: initialized backend identity differs from selected provider",
63
+ });
64
+ }
65
+ }
66
+ get providerName() {
67
+ return this.#providerName;
68
+ }
69
+ async attach(id, options = {}) {
70
+ return this.#openById("attach", id, options, (fetchOptions) => {
71
+ if (!this.#backend.attach)
72
+ unsupported(this.providerName, "attach");
73
+ return this.#backend.attach(id, fetchOptions);
74
+ });
75
+ }
76
+ async get(id, options = {}) {
77
+ return this.#openById("get", id, options, (fetchOptions) => {
78
+ if (!this.#backend.refresh)
79
+ unsupported(this.providerName, "get");
80
+ return this.#backend.refresh(id, fetchOptions);
81
+ });
82
+ }
83
+ async #openById(operation, id, options, fetch) {
84
+ const release = this.#begin(operation);
85
+ let scoped;
86
+ try {
87
+ scoped = operationSignal(options, this.#timeout);
88
+ if (!id.trim())
89
+ throw sandboxError({
90
+ kind: ErrorKind.InvalidArgument,
91
+ provider: this.providerName,
92
+ operation,
93
+ field: InfoFieldId,
94
+ message: "sandbox-kit: sandbox ID must not be blank",
95
+ });
96
+ const info = await awaitWithSignal(() => fetch(operationOptions(options, scoped?.signal)), scoped.signal);
97
+ return new Sandbox(sandboxConstructorToken, {
98
+ backend: this.#backend,
99
+ owner: this.#lifecycle,
100
+ info: { ...info, id, provider: this.providerName },
101
+ ...(this.#timeout === undefined ? {} : { timeout: this.#timeout }),
102
+ });
103
+ }
104
+ catch (error) {
105
+ throw withContext(error, this.providerName, operation);
106
+ }
107
+ finally {
108
+ scoped?.dispose();
109
+ release();
110
+ }
111
+ }
112
+ async create(request = {}, options = {}) {
113
+ const release = this.#begin("create");
114
+ const timeout = options.timeout ?? request.provisioning?.timeout ?? this.#timeout;
115
+ let scoped;
116
+ try {
117
+ // Validate the wire projection first; providers receive the public request.
118
+ validateCreateOptions(request, this.providerName, "create");
119
+ scoped = operationSignal(options, timeout);
120
+ const create = this.#backend.create;
121
+ if (!create)
122
+ unsupported(this.providerName, "create");
123
+ const providerRequest = {
124
+ ...request,
125
+ ...(timeout === undefined
126
+ ? {}
127
+ : {
128
+ provisioning: {
129
+ ...request.provisioning,
130
+ timeout,
131
+ },
132
+ }),
133
+ };
134
+ const result = await awaitWithSignal(() => create.call(this.#backend, providerRequest, operationOptions({ ...(timeout === undefined ? {} : { timeout: timeout }) }, scoped?.signal)), scoped.signal);
135
+ if (!result.sandbox?.id)
136
+ throw sandboxError({
137
+ kind: ErrorKind.InvalidResponse,
138
+ provider: this.providerName,
139
+ operation: "create",
140
+ field: InfoFieldId,
141
+ message: "sandbox-kit: provider returned no sandbox identity",
142
+ });
143
+ if (result.sandbox.provider &&
144
+ result.sandbox.provider !== this.providerName)
145
+ throw sandboxError({
146
+ kind: ErrorKind.InvalidResponse,
147
+ provider: this.providerName,
148
+ operation: "create",
149
+ field: ConfigFieldProvider,
150
+ message: "sandbox-kit: response provider differs from selected provider",
151
+ });
152
+ return new Sandbox(sandboxConstructorToken, {
153
+ backend: this.#backend,
154
+ owner: this.#lifecycle,
155
+ info: { ...result.sandbox, provider: this.providerName },
156
+ ...(this.#timeout === undefined ? {} : { timeout: this.#timeout }),
157
+ });
158
+ }
159
+ catch (error) {
160
+ throw withContext(error, this.providerName, "create");
161
+ }
162
+ finally {
163
+ scoped?.dispose();
164
+ release();
165
+ }
166
+ }
167
+ close(options = {}) {
168
+ if (this.#closePromise)
169
+ return this.#closePromise;
170
+ this.#closing = true;
171
+ this.#closePromise = this.#close(options);
172
+ return this.#closePromise;
173
+ }
174
+ async #close(options) {
175
+ let scoped;
176
+ try {
177
+ scoped = operationSignal(options, this.#timeout);
178
+ if (this.#active > 0) {
179
+ this.#drain ??= new Promise((resolve) => {
180
+ this.#resolveDrain = resolve;
181
+ });
182
+ await awaitWithSignal(() => this.#drain, scoped.signal);
183
+ }
184
+ await awaitWithSignal(() => this.#backend.close(operationOptions(options, scoped?.signal)), scoped.signal);
185
+ this.#closed = true;
186
+ }
187
+ catch (error) {
188
+ this.#closePromise = undefined;
189
+ this.#closing = false;
190
+ throw withContext(error, this.providerName, "close");
191
+ }
192
+ finally {
193
+ scoped?.dispose();
194
+ }
195
+ }
196
+ async [Symbol.asyncDispose]() {
197
+ await this.close();
198
+ }
199
+ #begin(operation) {
200
+ if (this.#closing || this.#closed)
201
+ throw sandboxError({
202
+ kind: ErrorKind.FailedPrecondition,
203
+ provider: this.providerName,
204
+ operation,
205
+ field: "client",
206
+ message: "sandbox-kit: client is closed",
207
+ });
208
+ this.#active++;
209
+ let released = false;
210
+ return () => {
211
+ if (released)
212
+ return;
213
+ released = true;
214
+ this.#active--;
215
+ if (this.#active === 0) {
216
+ this.#resolveDrain?.();
217
+ this.#resolveDrain = undefined;
218
+ this.#drain = undefined;
219
+ }
220
+ };
221
+ }
222
+ }
@@ -0,0 +1,35 @@
1
+ import { ErrorKind as ProtoErrorKind } from "@sandbox-kit/contracts/sandbox";
2
+ import type { ErrorInfo } from "#contracts/sandbox_data_contracts.js";
3
+ export declare const ErrorKind: {
4
+ readonly Unknown: ProtoErrorKind.UNKNOWN;
5
+ readonly InvalidArgument: ProtoErrorKind.INVALID_ARGUMENT;
6
+ readonly Unsupported: ProtoErrorKind.UNSUPPORTED;
7
+ readonly Authentication: ProtoErrorKind.AUTHENTICATION;
8
+ readonly PermissionDenied: ProtoErrorKind.PERMISSION_DENIED;
9
+ readonly NotFound: ProtoErrorKind.NOT_FOUND;
10
+ readonly AlreadyExists: ProtoErrorKind.ALREADY_EXISTS;
11
+ readonly Conflict: ProtoErrorKind.CONFLICT;
12
+ readonly RateLimited: ProtoErrorKind.RATE_LIMITED;
13
+ readonly ResourceExhausted: ProtoErrorKind.RESOURCE_EXHAUSTED;
14
+ readonly Timeout: ProtoErrorKind.TIMEOUT;
15
+ readonly Canceled: ProtoErrorKind.CANCELED;
16
+ readonly Unavailable: ProtoErrorKind.UNAVAILABLE;
17
+ readonly InvalidResponse: ProtoErrorKind.INVALID_RESPONSE;
18
+ readonly Internal: ProtoErrorKind.INTERNAL;
19
+ readonly FailedPrecondition: ProtoErrorKind.FAILED_PRECONDITION;
20
+ };
21
+ export type ErrorKind = (typeof ErrorKind)[keyof typeof ErrorKind];
22
+ export declare class SandboxError extends Error {
23
+ readonly kind: ErrorInfo["kind"];
24
+ readonly provider: string;
25
+ readonly operation: string;
26
+ readonly field: string;
27
+ readonly rule: string | undefined;
28
+ readonly statusCode: number | undefined;
29
+ readonly providerCode: string | undefined;
30
+ readonly providerSource: string | undefined;
31
+ constructor(info: ErrorInfo, options?: ErrorOptions);
32
+ }
33
+ export declare function sandboxError(info: ErrorInfo, cause?: unknown): SandboxError;
34
+ export declare function unsupported(provider: string, operation: string): never;
35
+ export declare function withContext(error: unknown, provider: string, operation: string): SandboxError;
@@ -0,0 +1,88 @@
1
+ // Portable error wrappers are handwritten. ErrorInfo / ErrorKind remain
2
+ // protobuf-owned contract data.
3
+ import { ErrorKind as ProtoErrorKind } from "@sandbox-kit/contracts/sandbox";
4
+ import { protoEnum } from "#internal/proto_enum.js";
5
+ // Ergonomic PascalCase names over protobuf enum values; not a second contract.
6
+ export const ErrorKind = protoEnum(ProtoErrorKind);
7
+ export class SandboxError extends Error {
8
+ kind;
9
+ provider;
10
+ operation;
11
+ field;
12
+ rule;
13
+ statusCode;
14
+ providerCode;
15
+ providerSource;
16
+ constructor(info, options) {
17
+ super(info.message, options);
18
+ this.name = "SandboxError";
19
+ this.kind = info.kind;
20
+ this.provider = info.provider;
21
+ this.operation = info.operation;
22
+ this.field = info.field;
23
+ this.rule = info.rule;
24
+ this.statusCode = info.statusCode;
25
+ this.providerCode = info.providerCode;
26
+ this.providerSource = info.providerSource;
27
+ }
28
+ }
29
+ export function sandboxError(info, cause) {
30
+ return new SandboxError(info, cause === undefined ? undefined : { cause });
31
+ }
32
+ export function unsupported(provider, operation) {
33
+ throw sandboxError({
34
+ kind: ErrorKind.Unsupported,
35
+ provider,
36
+ operation,
37
+ field: "",
38
+ message: "sandbox-kit: provider does not support " + operation,
39
+ });
40
+ }
41
+ export function withContext(error, provider, operation) {
42
+ if (error instanceof SandboxError) {
43
+ if (error.provider === provider && error.operation === operation)
44
+ return error;
45
+ return sandboxError({
46
+ kind: error.kind,
47
+ provider: error.provider || provider,
48
+ operation: error.operation || operation,
49
+ field: error.field,
50
+ message: error.message,
51
+ ...(error.rule === undefined ? {} : { rule: error.rule }),
52
+ ...(error.statusCode === undefined
53
+ ? {}
54
+ : { statusCode: error.statusCode }),
55
+ ...(error.providerCode === undefined
56
+ ? {}
57
+ : { providerCode: error.providerCode }),
58
+ ...(error.providerSource === undefined
59
+ ? {}
60
+ : { providerSource: error.providerSource }),
61
+ }, error.cause);
62
+ }
63
+ if (error instanceof DOMException && error.name === "AbortError")
64
+ return sandboxError({
65
+ kind: ErrorKind.Canceled,
66
+ provider,
67
+ operation,
68
+ field: "",
69
+ message: error.message,
70
+ }, error);
71
+ if (error instanceof DOMException && error.name === "TimeoutError")
72
+ return sandboxError({
73
+ kind: ErrorKind.Timeout,
74
+ provider,
75
+ operation,
76
+ field: "",
77
+ message: error.message,
78
+ }, error);
79
+ return sandboxError({
80
+ kind: ErrorKind.Unknown,
81
+ provider,
82
+ operation,
83
+ field: "",
84
+ message: error instanceof Error
85
+ ? error.message
86
+ : "sandbox-kit: provider operation failed",
87
+ }, error);
88
+ }
@@ -0,0 +1,12 @@
1
+ import type { OperationOptions } from "#client/types.js";
2
+ export declare function clone<T>(value: T): T;
3
+ export declare function operationSignal(options: OperationOptions, inheritedTimeoutMs?: number): {
4
+ signal?: AbortSignal;
5
+ dispose(): void;
6
+ };
7
+ export declare function operationOptions(options: OperationOptions, signal: AbortSignal | undefined): OperationOptions;
8
+ export declare function awaitWithSignal<T>(operation: () => Promise<T>, signal?: AbortSignal): Promise<T>;
9
+ /** Lifecycle request timeouts stay milliseconds for Backend / providers. */
10
+ export declare function publicTimeoutOptions(options: OperationOptions): {
11
+ timeout?: number;
12
+ };
@@ -0,0 +1,73 @@
1
+ // Operation deadline / AbortSignal helpers shared by Client and Sandbox.
2
+ import { ConfigFieldTimeout } from "#contracts/config_paths.js";
3
+ import { ErrorKind, sandboxError } from "#client/errors.js";
4
+ export function clone(value) {
5
+ return structuredClone(value);
6
+ }
7
+ export function operationSignal(options, inheritedTimeoutMs) {
8
+ const timeout = options.timeout ?? inheritedTimeoutMs;
9
+ if (timeout !== undefined && (!Number.isFinite(timeout) || timeout < 0)) {
10
+ throw sandboxError({
11
+ kind: ErrorKind.InvalidArgument,
12
+ provider: "",
13
+ operation: "",
14
+ field: ConfigFieldTimeout,
15
+ message: "sandbox-kit: timeout must be a finite non-negative number",
16
+ });
17
+ }
18
+ if ((timeout === undefined || timeout === 0) && options.signal === undefined)
19
+ return { dispose() { } };
20
+ const controller = new AbortController();
21
+ const abort = () => controller.abort(options.signal?.reason);
22
+ if (options.signal?.aborted)
23
+ abort();
24
+ else
25
+ options.signal?.addEventListener("abort", abort, { once: true });
26
+ const timer = timeout !== undefined && timeout > 0
27
+ ? setTimeout(() => controller.abort(new DOMException("The operation timed out", "TimeoutError")), timeout)
28
+ : undefined;
29
+ return {
30
+ signal: controller.signal,
31
+ dispose() {
32
+ if (timer !== undefined)
33
+ clearTimeout(timer);
34
+ options.signal?.removeEventListener("abort", abort);
35
+ },
36
+ };
37
+ }
38
+ export function operationOptions(options, signal) {
39
+ return {
40
+ ...(options.timeout === undefined ? {} : { timeout: options.timeout }),
41
+ ...(signal === undefined ? {} : { signal }),
42
+ };
43
+ }
44
+ function abortReason(signal) {
45
+ const reason = signal.reason;
46
+ return reason instanceof DOMException &&
47
+ (reason.name === "AbortError" || reason.name === "TimeoutError")
48
+ ? reason
49
+ : new DOMException("The operation was aborted", "AbortError");
50
+ }
51
+ export async function awaitWithSignal(operation, signal) {
52
+ if (signal?.aborted)
53
+ throw abortReason(signal);
54
+ const pending = operation();
55
+ if (signal === undefined)
56
+ return pending;
57
+ return new Promise((resolve, reject) => {
58
+ const aborted = () => reject(abortReason(signal));
59
+ const cleanup = () => signal.removeEventListener("abort", aborted);
60
+ signal.addEventListener("abort", aborted, { once: true });
61
+ pending.then((value) => {
62
+ cleanup();
63
+ resolve(value);
64
+ }, (error) => {
65
+ cleanup();
66
+ reject(error);
67
+ });
68
+ });
69
+ }
70
+ /** Lifecycle request timeouts stay milliseconds for Backend / providers. */
71
+ export function publicTimeoutOptions(options) {
72
+ return options.timeout === undefined ? {} : { timeout: options.timeout };
73
+ }
@@ -0,0 +1,33 @@
1
+ import { type ExitResult, type LifecycleCapability, type SandboxInfo } from "#contracts/sandbox_data_contracts.js";
2
+ import type { Backend, DeleteCallOptions, OperationOptions, StopCallOptions, TerminateCallOptions } from "#client/types.js";
3
+ /** Coordinates sandbox operations with client closure. */
4
+ export interface ClientLifecycle {
5
+ begin(operation: string): () => void;
6
+ isClosed(): boolean;
7
+ }
8
+ export declare const sandboxConstructorToken: unique symbol;
9
+ /** Internal construction inputs for {@link Sandbox}; only Client may call. */
10
+ export interface SandboxInit {
11
+ backend: Backend;
12
+ owner: ClientLifecycle;
13
+ info: SandboxInfo;
14
+ timeout?: number;
15
+ }
16
+ export declare class Sandbox implements AsyncDisposable {
17
+ #private;
18
+ constructor(token: typeof sandboxConstructorToken, init: SandboxInit);
19
+ get id(): string;
20
+ get providerName(): string;
21
+ info(): SandboxInfo;
22
+ capabilities(): readonly LifecycleCapability[];
23
+ supports(capability: LifecycleCapability): boolean;
24
+ refresh(options?: OperationOptions): Promise<SandboxInfo>;
25
+ start(options?: OperationOptions): Promise<SandboxInfo>;
26
+ stop(options?: StopCallOptions): Promise<SandboxInfo>;
27
+ pause(options?: OperationOptions): Promise<SandboxInfo>;
28
+ terminate(options?: TerminateCallOptions): Promise<ExitResult>;
29
+ archive(options?: OperationOptions): Promise<SandboxInfo>;
30
+ delete(options?: DeleteCallOptions): Promise<SandboxInfo>;
31
+ close(options?: OperationOptions): Promise<void>;
32
+ [Symbol.asyncDispose](): Promise<void>;
33
+ }