@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.
- package/LICENSE +21 -0
- package/README.md +129 -2
- package/dist/client/client.d.ts +17 -0
- package/dist/client/client.js +222 -0
- package/dist/client/errors.d.ts +35 -0
- package/dist/client/errors.js +88 -0
- package/dist/client/operation.d.ts +12 -0
- package/dist/client/operation.js +73 -0
- package/dist/client/sandbox.d.ts +33 -0
- package/dist/client/sandbox.js +160 -0
- package/dist/client/types.d.ts +63 -0
- package/dist/client/types.js +3 -0
- package/dist/client/validation.d.ts +9 -0
- package/dist/client/validation.js +221 -0
- package/dist/contracts/config_paths.d.ts +44 -0
- package/dist/contracts/config_paths.js +50 -0
- package/dist/contracts/field_paths.d.ts +216 -0
- package/dist/contracts/field_paths.js +222 -0
- package/dist/contracts/sandbox_data_contracts.d.ts +206 -0
- package/dist/contracts/sandbox_data_contracts.js +22 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/internal/duration_ms.d.ts +15 -0
- package/dist/internal/duration_ms.js +41 -0
- package/dist/internal/input_remap.d.ts +36 -0
- package/dist/internal/input_remap.js +1 -0
- package/dist/internal/net_validate.d.ts +4 -0
- package/dist/internal/net_validate.js +53 -0
- package/dist/internal/oneof_input.d.ts +32 -0
- package/dist/internal/oneof_input.js +1 -0
- package/dist/internal/proto_enum.d.ts +25 -0
- package/dist/internal/proto_enum.js +25 -0
- package/dist/internal/strip_proto.d.ts +46 -0
- package/dist/internal/strip_proto.js +1 -0
- package/dist/metadata/metadata_object.d.ts +7 -0
- package/dist/metadata/metadata_object.js +103 -0
- package/dist/metadata/metadata_plain.d.ts +9 -0
- package/dist/metadata/metadata_plain.js +1 -0
- 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
|
-
#
|
|
1
|
+
# `@sandbox-kit/sandbox`
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
}
|