@sandbox-kit/sandbox 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +69 -2
- package/dist/client/client.d.ts +136 -0
- package/dist/client/client.js +749 -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 +203 -0
- package/dist/contracts/sandbox_data_contracts.js +22 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -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/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 +66 -4
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
// Client and sandbox runtime behavior is handwritten. Portable client data
|
|
2
|
+
// contracts come from standard protobuf generation.
|
|
3
|
+
import { create, } from "@bufbuild/protobuf";
|
|
4
|
+
import { pathToString } from "@bufbuild/protobuf/reflect";
|
|
5
|
+
import { durationFromMs } from "@bufbuild/protobuf/wkt";
|
|
6
|
+
import { createValidator } from "@bufbuild/protovalidate";
|
|
7
|
+
import { ConfigSchema, } from "@sandbox-kit/contracts/client";
|
|
8
|
+
import { CreateOptionsSchema, ErrorKind as ProtoErrorKind, } from "@sandbox-kit/contracts/sandbox";
|
|
9
|
+
import { SandboxState, } from "#contracts/sandbox_data_contracts.js";
|
|
10
|
+
import { materializeDurationFields, } from "#internal/duration_ms.js";
|
|
11
|
+
import { metadataObjectFromMap } from "#metadata/metadata_object.js";
|
|
12
|
+
import { protoEnum } from "#internal/proto_enum.js";
|
|
13
|
+
import { InfoFieldId } from "#contracts/field_paths.js";
|
|
14
|
+
import { ConfigFieldProvider, ConfigFieldTimeout, } from "#contracts/config_paths.js";
|
|
15
|
+
const contractValidator = createValidator({ failFast: true });
|
|
16
|
+
const stableRuleIDs = {
|
|
17
|
+
"enum.defined_only": "known_enum",
|
|
18
|
+
"duration.gte": "minimum",
|
|
19
|
+
"oneof.required": "exactly_one",
|
|
20
|
+
auth_credentials_required: "exactly_one",
|
|
21
|
+
cpu_limit: "limit",
|
|
22
|
+
memory_limit: "limit",
|
|
23
|
+
cpu_requires_positive: "requires_positive",
|
|
24
|
+
memory_requires_positive: "requires_positive",
|
|
25
|
+
restricted_requires_allowlist: "requires_any",
|
|
26
|
+
block_all_forbids_network: "forbids",
|
|
27
|
+
allow_all_forbids_allowlists: "forbids",
|
|
28
|
+
after_requires_duration: "requires_any",
|
|
29
|
+
after_forbids_duration: "forbids",
|
|
30
|
+
};
|
|
31
|
+
// Ergonomic PascalCase names over protobuf enum values; not a second contract.
|
|
32
|
+
export const ErrorKind = protoEnum(ProtoErrorKind);
|
|
33
|
+
export class SandboxError extends Error {
|
|
34
|
+
kind;
|
|
35
|
+
provider;
|
|
36
|
+
operation;
|
|
37
|
+
field;
|
|
38
|
+
rule;
|
|
39
|
+
statusCode;
|
|
40
|
+
providerCode;
|
|
41
|
+
providerSource;
|
|
42
|
+
constructor(info, options) {
|
|
43
|
+
super(info.message, options);
|
|
44
|
+
this.name = "SandboxError";
|
|
45
|
+
this.kind = info.kind;
|
|
46
|
+
this.provider = info.provider;
|
|
47
|
+
this.operation = info.operation;
|
|
48
|
+
this.field = info.field;
|
|
49
|
+
this.rule = info.rule;
|
|
50
|
+
this.statusCode = info.statusCode;
|
|
51
|
+
this.providerCode = info.providerCode;
|
|
52
|
+
this.providerSource = info.providerSource;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function sandboxError(info, cause) {
|
|
56
|
+
return new SandboxError(info, cause === undefined ? undefined : { cause });
|
|
57
|
+
}
|
|
58
|
+
function validationError(provider, operation, field, rule, message) {
|
|
59
|
+
throw sandboxError({
|
|
60
|
+
kind: ErrorKind.InvalidArgument,
|
|
61
|
+
provider,
|
|
62
|
+
operation,
|
|
63
|
+
field,
|
|
64
|
+
rule,
|
|
65
|
+
message: "sandbox-kit: " + message,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function validSecureEndpoint(value) {
|
|
69
|
+
try {
|
|
70
|
+
const url = new URL(value);
|
|
71
|
+
if (!url.hostname || url.username || url.password || url.hash)
|
|
72
|
+
return false;
|
|
73
|
+
if (url.protocol === "https:")
|
|
74
|
+
return true;
|
|
75
|
+
if (url.protocol !== "http:")
|
|
76
|
+
return false;
|
|
77
|
+
const host = url.hostname.toLowerCase();
|
|
78
|
+
return (host === "localhost" ||
|
|
79
|
+
host === "[::1]" ||
|
|
80
|
+
host === "::1" ||
|
|
81
|
+
/^127(?:\.[0-9]{1,3}){3}$/.test(host));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Rejects public {@link CreateOptions} the shared contract cannot represent.
|
|
89
|
+
* Materializes wire form privately for Protovalidate. Provider-specific checks
|
|
90
|
+
* remain in each provider adapter.
|
|
91
|
+
*/
|
|
92
|
+
export function validateCreateOptions(value, provider = "", operation = "create") {
|
|
93
|
+
const message = materializeCreateOptions(value);
|
|
94
|
+
const result = contractValidator.validate(CreateOptionsSchema, message);
|
|
95
|
+
if (result.kind === "valid") {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (result.kind === "error") {
|
|
99
|
+
throw sandboxError({
|
|
100
|
+
kind: ErrorKind.InvalidArgument,
|
|
101
|
+
provider,
|
|
102
|
+
operation,
|
|
103
|
+
field: "",
|
|
104
|
+
message: "sandbox-kit: invalid configuration",
|
|
105
|
+
}, result.error);
|
|
106
|
+
}
|
|
107
|
+
const violation = result.violations[0];
|
|
108
|
+
const ruleID = violation.ruleId;
|
|
109
|
+
let field = pathToString(violation.field);
|
|
110
|
+
let rule = stableRuleIDs[ruleID] ?? ruleID;
|
|
111
|
+
if (!field) {
|
|
112
|
+
field = messageFieldHint(ruleID);
|
|
113
|
+
}
|
|
114
|
+
validationError(provider, operation, field, rule, violation.message || "invalid configuration");
|
|
115
|
+
}
|
|
116
|
+
function messageFieldHint(ruleID) {
|
|
117
|
+
switch (ruleID) {
|
|
118
|
+
case "cpu_limit":
|
|
119
|
+
case "cpu_requires_positive":
|
|
120
|
+
return "resources.cpu_limit_cores";
|
|
121
|
+
case "memory_limit":
|
|
122
|
+
case "memory_requires_positive":
|
|
123
|
+
return "resources.memory_limit_mib";
|
|
124
|
+
case "restricted_requires_allowlist":
|
|
125
|
+
return "network.outbound_cidrs,outbound_domains";
|
|
126
|
+
case "block_all_forbids_network":
|
|
127
|
+
return "network.outbound_cidrs,outbound_domains,private_network,linked_sandbox,inbound_cidrs,ports";
|
|
128
|
+
case "allow_all_forbids_allowlists":
|
|
129
|
+
return "network.outbound_cidrs,outbound_domains";
|
|
130
|
+
case "after_requires_duration":
|
|
131
|
+
case "after_forbids_duration":
|
|
132
|
+
return "after";
|
|
133
|
+
case "oneof.required":
|
|
134
|
+
return "tcp_port,command";
|
|
135
|
+
case "auth_credentials_required":
|
|
136
|
+
return "api_key,token_pair,bearer_token,oauth_refresh";
|
|
137
|
+
case "exactly_one":
|
|
138
|
+
return "client_secret,jwt_key";
|
|
139
|
+
default:
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export function validateConfig(value, provider = "", operation = "create") {
|
|
144
|
+
const message = materializeClientConfig(value);
|
|
145
|
+
const result = contractValidator.validate(ConfigSchema, message);
|
|
146
|
+
if (result.kind === "error") {
|
|
147
|
+
throw sandboxError({
|
|
148
|
+
kind: ErrorKind.InvalidArgument,
|
|
149
|
+
provider,
|
|
150
|
+
operation,
|
|
151
|
+
field: "",
|
|
152
|
+
message: "sandbox-kit: invalid configuration",
|
|
153
|
+
}, result.error);
|
|
154
|
+
}
|
|
155
|
+
if (result.kind === "invalid") {
|
|
156
|
+
const violation = result.violations[0];
|
|
157
|
+
const ruleID = violation.ruleId;
|
|
158
|
+
let field = pathToString(violation.field);
|
|
159
|
+
const rule = stableRuleIDs[ruleID] ?? ruleID;
|
|
160
|
+
if (!field) {
|
|
161
|
+
field = messageFieldHint(ruleID);
|
|
162
|
+
}
|
|
163
|
+
validationError(provider, operation, field, rule, violation.message || "invalid configuration");
|
|
164
|
+
}
|
|
165
|
+
if (message.endpoint !== undefined &&
|
|
166
|
+
!validSecureEndpoint(message.endpoint)) {
|
|
167
|
+
validationError(provider, operation, "endpoint", "secure_endpoint", "must use HTTPS except for loopback HTTP development endpoints");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function clone(value) {
|
|
171
|
+
return structuredClone(value);
|
|
172
|
+
}
|
|
173
|
+
function operationSignal(options, inheritedTimeoutMs) {
|
|
174
|
+
const timeout = options.timeout ?? inheritedTimeoutMs;
|
|
175
|
+
if (timeout !== undefined && (!Number.isFinite(timeout) || timeout < 0)) {
|
|
176
|
+
throw sandboxError({
|
|
177
|
+
kind: ErrorKind.InvalidArgument,
|
|
178
|
+
provider: "",
|
|
179
|
+
operation: "",
|
|
180
|
+
field: ConfigFieldTimeout,
|
|
181
|
+
message: "sandbox-kit: timeout must be a finite non-negative number",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if ((timeout === undefined || timeout === 0) && options.signal === undefined)
|
|
185
|
+
return { dispose() { } };
|
|
186
|
+
const controller = new AbortController();
|
|
187
|
+
const abort = () => controller.abort(options.signal?.reason);
|
|
188
|
+
if (options.signal?.aborted)
|
|
189
|
+
abort();
|
|
190
|
+
else
|
|
191
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
192
|
+
const timer = timeout !== undefined && timeout > 0
|
|
193
|
+
? setTimeout(() => controller.abort(new DOMException("The operation timed out", "TimeoutError")), timeout)
|
|
194
|
+
: undefined;
|
|
195
|
+
return {
|
|
196
|
+
signal: controller.signal,
|
|
197
|
+
dispose() {
|
|
198
|
+
if (timer !== undefined)
|
|
199
|
+
clearTimeout(timer);
|
|
200
|
+
options.signal?.removeEventListener("abort", abort);
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function operationOptions(options, signal) {
|
|
205
|
+
return {
|
|
206
|
+
...(options.timeout === undefined ? {} : { timeout: options.timeout }),
|
|
207
|
+
...(signal === undefined ? {} : { signal }),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function abortReason(signal) {
|
|
211
|
+
const reason = signal.reason;
|
|
212
|
+
return reason instanceof DOMException &&
|
|
213
|
+
(reason.name === "AbortError" || reason.name === "TimeoutError")
|
|
214
|
+
? reason
|
|
215
|
+
: new DOMException("The operation was aborted", "AbortError");
|
|
216
|
+
}
|
|
217
|
+
async function awaitWithSignal(operation, signal) {
|
|
218
|
+
if (signal?.aborted)
|
|
219
|
+
throw abortReason(signal);
|
|
220
|
+
const pending = operation();
|
|
221
|
+
if (signal === undefined)
|
|
222
|
+
return pending;
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
const aborted = () => reject(abortReason(signal));
|
|
225
|
+
const cleanup = () => signal.removeEventListener("abort", aborted);
|
|
226
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
227
|
+
pending.then((value) => {
|
|
228
|
+
cleanup();
|
|
229
|
+
resolve(value);
|
|
230
|
+
}, (error) => {
|
|
231
|
+
cleanup();
|
|
232
|
+
reject(error);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
const sandboxConstructorToken = Symbol("sandbox-kit.Sandbox");
|
|
237
|
+
export class Sandbox {
|
|
238
|
+
#backend;
|
|
239
|
+
#owner;
|
|
240
|
+
#timeout;
|
|
241
|
+
#info;
|
|
242
|
+
#closed = false;
|
|
243
|
+
#tail = Promise.resolve();
|
|
244
|
+
constructor(token, backend, owner, info, timeout) {
|
|
245
|
+
if (token !== sandboxConstructorToken)
|
|
246
|
+
throw new TypeError("Sandbox handles are created by Client");
|
|
247
|
+
this.#backend = backend;
|
|
248
|
+
this.#owner = owner;
|
|
249
|
+
this.#info = clone(info);
|
|
250
|
+
this.#timeout = timeout;
|
|
251
|
+
}
|
|
252
|
+
get id() {
|
|
253
|
+
return this.#info.id;
|
|
254
|
+
}
|
|
255
|
+
get providerName() {
|
|
256
|
+
return this.#info.provider;
|
|
257
|
+
}
|
|
258
|
+
info() {
|
|
259
|
+
return clone(this.#info);
|
|
260
|
+
}
|
|
261
|
+
capabilities() {
|
|
262
|
+
if (this.#closed || this.#owner.isClosed() || this.#info.terminal)
|
|
263
|
+
return [];
|
|
264
|
+
return this.#backend.capabilities?.(clone(this.#info)) ?? [];
|
|
265
|
+
}
|
|
266
|
+
supports(capability) {
|
|
267
|
+
return this.capabilities().includes(capability);
|
|
268
|
+
}
|
|
269
|
+
async refresh(options = {}) {
|
|
270
|
+
return this.#serialized("refresh", options, this.#timeout, async (signal) => {
|
|
271
|
+
const method = this.#backend.refresh;
|
|
272
|
+
if (!method)
|
|
273
|
+
unsupported(this.providerName, "refresh");
|
|
274
|
+
const info = await awaitWithSignal(() => method.call(this.#backend, this.id, operationOptions(options, signal)), signal);
|
|
275
|
+
this.#info = clone({ ...info, provider: this.providerName });
|
|
276
|
+
return this.info();
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
async start(options = {}) {
|
|
280
|
+
return this.#update("start", this.#backend.start, wireTimeoutOptions(options), options);
|
|
281
|
+
}
|
|
282
|
+
async stop(options = {}) {
|
|
283
|
+
const { force, ...call } = options;
|
|
284
|
+
return this.#update("stop", this.#backend.stop, {
|
|
285
|
+
...wireTimeoutOptions(call),
|
|
286
|
+
...(force === undefined ? {} : { force }),
|
|
287
|
+
}, call);
|
|
288
|
+
}
|
|
289
|
+
async pause(options = {}) {
|
|
290
|
+
return this.#update("pause", this.#backend.pause, wireTimeoutOptions(options), options);
|
|
291
|
+
}
|
|
292
|
+
async terminate(options = {}) {
|
|
293
|
+
const { wait, ...call } = options;
|
|
294
|
+
const wire = {
|
|
295
|
+
...wireTimeoutOptions(call),
|
|
296
|
+
...(wait === undefined ? {} : { wait }),
|
|
297
|
+
};
|
|
298
|
+
return this.#serialized("terminate", call, call.timeout ?? this.#timeout, async (signal) => {
|
|
299
|
+
const method = this.#backend.terminate;
|
|
300
|
+
if (!method)
|
|
301
|
+
unsupported(this.providerName, "terminate");
|
|
302
|
+
const result = await awaitWithSignal(() => method.call(this.#backend, this.id, clone(wire), operationOptions(call, signal)), signal);
|
|
303
|
+
this.#info = {
|
|
304
|
+
...this.#info,
|
|
305
|
+
state: SandboxState.Terminated,
|
|
306
|
+
terminal: true,
|
|
307
|
+
};
|
|
308
|
+
return result;
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
async archive(options = {}) {
|
|
312
|
+
return this.#update("archive", this.#backend.archive, wireTimeoutOptions(options), options);
|
|
313
|
+
}
|
|
314
|
+
async delete(options = {}) {
|
|
315
|
+
const { wait, ...call } = options;
|
|
316
|
+
return this.#update("delete", this.#backend.delete, { ...wireTimeoutOptions(call), ...(wait === undefined ? {} : { wait }) }, call);
|
|
317
|
+
}
|
|
318
|
+
async close(options = {}) {
|
|
319
|
+
if (this.#closed)
|
|
320
|
+
return;
|
|
321
|
+
await this.#serialized("close", options, this.#timeout, async (signal) => {
|
|
322
|
+
const close = this.#backend.closeSandbox;
|
|
323
|
+
if (close)
|
|
324
|
+
await awaitWithSignal(() => close.call(this.#backend, this.id, operationOptions(options, signal)), signal);
|
|
325
|
+
this.#closed = true;
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async [Symbol.asyncDispose]() {
|
|
329
|
+
await this.close();
|
|
330
|
+
}
|
|
331
|
+
async #update(operation, method, wire, options) {
|
|
332
|
+
return this.#serialized(operation, options, options.timeout ?? this.#timeout, async (signal) => {
|
|
333
|
+
if (!method)
|
|
334
|
+
unsupported(this.providerName, operation);
|
|
335
|
+
const info = await awaitWithSignal(() => method.call(this.#backend, this.id, clone(wire), operationOptions(options, signal)), signal);
|
|
336
|
+
this.#info = clone({ ...info, provider: this.providerName });
|
|
337
|
+
return this.info();
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
async #serialized(operation, options, timeout, body) {
|
|
341
|
+
this.#ensureOpen(operation);
|
|
342
|
+
const releaseClient = this.#owner.begin(operation);
|
|
343
|
+
const previous = this.#tail;
|
|
344
|
+
let unlock;
|
|
345
|
+
this.#tail = new Promise((resolve) => {
|
|
346
|
+
unlock = resolve;
|
|
347
|
+
});
|
|
348
|
+
let acquired = false;
|
|
349
|
+
let scoped;
|
|
350
|
+
try {
|
|
351
|
+
scoped = operationSignal(options, timeout);
|
|
352
|
+
await awaitWithSignal(() => previous, scoped.signal);
|
|
353
|
+
acquired = true;
|
|
354
|
+
this.#ensureOpen(operation);
|
|
355
|
+
return await body(scoped.signal);
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
throw withContext(error, this.providerName, operation);
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
scoped?.dispose();
|
|
362
|
+
if (acquired)
|
|
363
|
+
unlock();
|
|
364
|
+
else
|
|
365
|
+
void previous.then(unlock, unlock);
|
|
366
|
+
releaseClient();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
#ensureOpen(operation) {
|
|
370
|
+
if (this.#closed || this.#owner.isClosed())
|
|
371
|
+
throw sandboxError({
|
|
372
|
+
kind: ErrorKind.FailedPrecondition,
|
|
373
|
+
provider: this.providerName,
|
|
374
|
+
operation,
|
|
375
|
+
field: "sandbox",
|
|
376
|
+
message: "sandbox-kit: sandbox handle is closed",
|
|
377
|
+
});
|
|
378
|
+
if (operation !== "close" && this.#info.terminal)
|
|
379
|
+
throw sandboxError({
|
|
380
|
+
kind: ErrorKind.FailedPrecondition,
|
|
381
|
+
provider: this.providerName,
|
|
382
|
+
operation,
|
|
383
|
+
field: "sandbox",
|
|
384
|
+
message: "sandbox-kit: sandbox lifecycle is terminal",
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
export class Client {
|
|
389
|
+
#backend;
|
|
390
|
+
#providerName;
|
|
391
|
+
#timeout;
|
|
392
|
+
#lifecycle;
|
|
393
|
+
#active = 0;
|
|
394
|
+
#closing = false;
|
|
395
|
+
#closed = false;
|
|
396
|
+
#closePromise;
|
|
397
|
+
#drain;
|
|
398
|
+
#resolveDrain;
|
|
399
|
+
constructor(config) {
|
|
400
|
+
if (!config.provider?.name?.trim())
|
|
401
|
+
throw sandboxError({
|
|
402
|
+
kind: ErrorKind.InvalidArgument,
|
|
403
|
+
provider: "",
|
|
404
|
+
operation: "initialize",
|
|
405
|
+
field: ConfigFieldProvider,
|
|
406
|
+
message: "sandbox-kit: provider is required",
|
|
407
|
+
});
|
|
408
|
+
validateConfig(config, config.provider.name, "initialize");
|
|
409
|
+
this.#providerName = config.provider.name;
|
|
410
|
+
this.#timeout = config.timeout;
|
|
411
|
+
this.#lifecycle = {
|
|
412
|
+
begin: (operation) => this.#begin(operation),
|
|
413
|
+
isClosed: () => this.#closing || this.#closed,
|
|
414
|
+
};
|
|
415
|
+
try {
|
|
416
|
+
const { provider: _provider, ...shared } = config;
|
|
417
|
+
this.#backend = config.provider.initialize(shared);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
throw withContext(error, config.provider.name, "initialize");
|
|
421
|
+
}
|
|
422
|
+
if (!this.#backend)
|
|
423
|
+
throw sandboxError({
|
|
424
|
+
kind: ErrorKind.InvalidResponse,
|
|
425
|
+
provider: config.provider.name,
|
|
426
|
+
operation: "initialize",
|
|
427
|
+
field: ConfigFieldProvider,
|
|
428
|
+
message: "sandbox-kit: provider returned no backend",
|
|
429
|
+
});
|
|
430
|
+
if (this.#backend.name !== this.#providerName) {
|
|
431
|
+
void this.#backend.close({}).catch(() => { });
|
|
432
|
+
throw sandboxError({
|
|
433
|
+
kind: ErrorKind.InvalidResponse,
|
|
434
|
+
provider: this.#providerName,
|
|
435
|
+
operation: "initialize",
|
|
436
|
+
field: ConfigFieldProvider,
|
|
437
|
+
message: "sandbox-kit: initialized backend identity differs from selected provider",
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
get providerName() {
|
|
442
|
+
return this.#providerName;
|
|
443
|
+
}
|
|
444
|
+
async attach(id, options = {}) {
|
|
445
|
+
const release = this.#begin("attach");
|
|
446
|
+
let scoped;
|
|
447
|
+
try {
|
|
448
|
+
scoped = operationSignal(options, this.#timeout);
|
|
449
|
+
if (!id.trim())
|
|
450
|
+
throw sandboxError({
|
|
451
|
+
kind: ErrorKind.InvalidArgument,
|
|
452
|
+
provider: this.providerName,
|
|
453
|
+
operation: "attach",
|
|
454
|
+
field: InfoFieldId,
|
|
455
|
+
message: "sandbox-kit: sandbox ID must not be blank",
|
|
456
|
+
});
|
|
457
|
+
if (!this.#backend.attach)
|
|
458
|
+
unsupported(this.providerName, "attach");
|
|
459
|
+
const info = await awaitWithSignal(() => this.#backend.attach(id, operationOptions(options, scoped?.signal)), scoped.signal);
|
|
460
|
+
return new Sandbox(sandboxConstructorToken, this.#backend, this.#lifecycle, { ...info, id, provider: this.providerName }, this.#timeout);
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
throw withContext(error, this.providerName, "attach");
|
|
464
|
+
}
|
|
465
|
+
finally {
|
|
466
|
+
scoped?.dispose();
|
|
467
|
+
release();
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
async get(id, options = {}) {
|
|
471
|
+
const release = this.#begin("get");
|
|
472
|
+
let scoped;
|
|
473
|
+
try {
|
|
474
|
+
scoped = operationSignal(options, this.#timeout);
|
|
475
|
+
if (!id.trim())
|
|
476
|
+
throw sandboxError({
|
|
477
|
+
kind: ErrorKind.InvalidArgument,
|
|
478
|
+
provider: this.providerName,
|
|
479
|
+
operation: "get",
|
|
480
|
+
field: InfoFieldId,
|
|
481
|
+
message: "sandbox-kit: sandbox ID must not be blank",
|
|
482
|
+
});
|
|
483
|
+
if (!this.#backend.refresh)
|
|
484
|
+
unsupported(this.providerName, "get");
|
|
485
|
+
const info = await awaitWithSignal(() => this.#backend.refresh(id, operationOptions(options, scoped?.signal)), scoped.signal);
|
|
486
|
+
return new Sandbox(sandboxConstructorToken, this.#backend, this.#lifecycle, { ...info, id, provider: this.providerName }, this.#timeout);
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
throw withContext(error, this.providerName, "get");
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
scoped?.dispose();
|
|
493
|
+
release();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async create(request = {}, options = {}) {
|
|
497
|
+
const release = this.#begin("create");
|
|
498
|
+
const timeout = options.timeout ?? request.provisioning?.timeout ?? this.#timeout;
|
|
499
|
+
let scoped;
|
|
500
|
+
try {
|
|
501
|
+
// Validate the wire projection first; providers receive the public request.
|
|
502
|
+
validateCreateOptions(request, this.providerName, "create");
|
|
503
|
+
scoped = operationSignal(options, timeout);
|
|
504
|
+
const create = this.#backend.create;
|
|
505
|
+
if (!create)
|
|
506
|
+
unsupported(this.providerName, "create");
|
|
507
|
+
const providerRequest = {
|
|
508
|
+
...request,
|
|
509
|
+
...(timeout === undefined
|
|
510
|
+
? {}
|
|
511
|
+
: {
|
|
512
|
+
provisioning: {
|
|
513
|
+
...request.provisioning,
|
|
514
|
+
timeout,
|
|
515
|
+
},
|
|
516
|
+
}),
|
|
517
|
+
};
|
|
518
|
+
const result = await awaitWithSignal(() => create.call(this.#backend, providerRequest, operationOptions({ ...(timeout === undefined ? {} : { timeout: timeout }) }, scoped?.signal)), scoped.signal);
|
|
519
|
+
if (!result.sandbox?.id)
|
|
520
|
+
throw sandboxError({
|
|
521
|
+
kind: ErrorKind.InvalidResponse,
|
|
522
|
+
provider: this.providerName,
|
|
523
|
+
operation: "create",
|
|
524
|
+
field: InfoFieldId,
|
|
525
|
+
message: "sandbox-kit: provider returned no sandbox identity",
|
|
526
|
+
});
|
|
527
|
+
if (result.sandbox.provider &&
|
|
528
|
+
result.sandbox.provider !== this.providerName)
|
|
529
|
+
throw sandboxError({
|
|
530
|
+
kind: ErrorKind.InvalidResponse,
|
|
531
|
+
provider: this.providerName,
|
|
532
|
+
operation: "create",
|
|
533
|
+
field: ConfigFieldProvider,
|
|
534
|
+
message: "sandbox-kit: response provider differs from selected provider",
|
|
535
|
+
});
|
|
536
|
+
return new Sandbox(sandboxConstructorToken, this.#backend, this.#lifecycle, { ...result.sandbox, provider: this.providerName }, this.#timeout);
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
throw withContext(error, this.providerName, "create");
|
|
540
|
+
}
|
|
541
|
+
finally {
|
|
542
|
+
scoped?.dispose();
|
|
543
|
+
release();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
close(options = {}) {
|
|
547
|
+
if (this.#closePromise)
|
|
548
|
+
return this.#closePromise;
|
|
549
|
+
this.#closing = true;
|
|
550
|
+
this.#closePromise = this.#close(options);
|
|
551
|
+
return this.#closePromise;
|
|
552
|
+
}
|
|
553
|
+
async #close(options) {
|
|
554
|
+
let scoped;
|
|
555
|
+
try {
|
|
556
|
+
scoped = operationSignal(options, this.#timeout);
|
|
557
|
+
if (this.#active > 0) {
|
|
558
|
+
this.#drain ??= new Promise((resolve) => {
|
|
559
|
+
this.#resolveDrain = resolve;
|
|
560
|
+
});
|
|
561
|
+
await awaitWithSignal(() => this.#drain, scoped.signal);
|
|
562
|
+
}
|
|
563
|
+
await awaitWithSignal(() => this.#backend.close(operationOptions(options, scoped?.signal)), scoped.signal);
|
|
564
|
+
this.#closed = true;
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
this.#closePromise = undefined;
|
|
568
|
+
this.#closing = false;
|
|
569
|
+
throw withContext(error, this.providerName, "close");
|
|
570
|
+
}
|
|
571
|
+
finally {
|
|
572
|
+
scoped?.dispose();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
async [Symbol.asyncDispose]() {
|
|
576
|
+
await this.close();
|
|
577
|
+
}
|
|
578
|
+
#begin(operation) {
|
|
579
|
+
if (this.#closing || this.#closed)
|
|
580
|
+
throw sandboxError({
|
|
581
|
+
kind: ErrorKind.FailedPrecondition,
|
|
582
|
+
provider: this.providerName,
|
|
583
|
+
operation,
|
|
584
|
+
field: "client",
|
|
585
|
+
message: "sandbox-kit: client is closed",
|
|
586
|
+
});
|
|
587
|
+
this.#active++;
|
|
588
|
+
let released = false;
|
|
589
|
+
return () => {
|
|
590
|
+
if (released)
|
|
591
|
+
return;
|
|
592
|
+
released = true;
|
|
593
|
+
this.#active--;
|
|
594
|
+
if (this.#active === 0) {
|
|
595
|
+
this.#resolveDrain?.();
|
|
596
|
+
this.#resolveDrain = undefined;
|
|
597
|
+
this.#drain = undefined;
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/** Materializes a plain client config into the generated protobuf message. */
|
|
603
|
+
function materializeClientConfig(input) {
|
|
604
|
+
const { auth, timeout, ...rest } = input;
|
|
605
|
+
return create(ConfigSchema, {
|
|
606
|
+
...rest,
|
|
607
|
+
...(timeout === undefined ? {} : { timeout: durationFromMs(timeout) }),
|
|
608
|
+
...(auth === undefined
|
|
609
|
+
? {}
|
|
610
|
+
: { auth: { credentials: authCredentials(auth) } }),
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
function wireTimeoutOptions(options) {
|
|
614
|
+
return options.timeout === undefined
|
|
615
|
+
? {}
|
|
616
|
+
: { timeout: durationFromMs(options.timeout) };
|
|
617
|
+
}
|
|
618
|
+
/** Materializes plain create options into the generated protobuf message. */
|
|
619
|
+
function materializeCreateOptions(input) {
|
|
620
|
+
const { source, readiness, providerOptions, network, ...rest } = input;
|
|
621
|
+
const withDurations = materializeDurationFields(rest);
|
|
622
|
+
return create(CreateOptionsSchema, {
|
|
623
|
+
...withDurations,
|
|
624
|
+
...(network === undefined
|
|
625
|
+
? {}
|
|
626
|
+
: { network: materializeNetworkConfig(network) }),
|
|
627
|
+
...(source === undefined ? {} : { source: { kind: sourceKind(source) } }),
|
|
628
|
+
...(readiness === undefined
|
|
629
|
+
? {}
|
|
630
|
+
: { readiness: { check: readinessCheck(readiness) } }),
|
|
631
|
+
...(providerOptions === undefined
|
|
632
|
+
? {}
|
|
633
|
+
: { providerOptions: metadataObjectFromMap(providerOptions) }),
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
/** Wrap public string-array allowlists into wire StringAllowlist messages. */
|
|
637
|
+
function materializeNetworkConfig(network) {
|
|
638
|
+
const { outboundCidrs, outboundDomains, inboundCidrs, ...rest } = network;
|
|
639
|
+
return {
|
|
640
|
+
...rest,
|
|
641
|
+
...(outboundCidrs === undefined
|
|
642
|
+
? {}
|
|
643
|
+
: { outboundCidrs: { entries: outboundCidrs } }),
|
|
644
|
+
...(outboundDomains === undefined
|
|
645
|
+
? {}
|
|
646
|
+
: { outboundDomains: { entries: outboundDomains } }),
|
|
647
|
+
...(inboundCidrs === undefined
|
|
648
|
+
? {}
|
|
649
|
+
: { inboundCidrs: { entries: inboundCidrs } }),
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function authCredentials(auth) {
|
|
653
|
+
if (auth.apiKey !== undefined)
|
|
654
|
+
return { case: "apiKey", value: auth.apiKey };
|
|
655
|
+
if (auth.tokenPair !== undefined)
|
|
656
|
+
return {
|
|
657
|
+
case: "tokenPair",
|
|
658
|
+
value: auth.tokenPair,
|
|
659
|
+
};
|
|
660
|
+
if (auth.bearerToken !== undefined)
|
|
661
|
+
return {
|
|
662
|
+
case: "bearerToken",
|
|
663
|
+
value: auth.bearerToken,
|
|
664
|
+
};
|
|
665
|
+
if (auth.oauthRefresh !== undefined)
|
|
666
|
+
return {
|
|
667
|
+
case: "oauthRefresh",
|
|
668
|
+
value: auth.oauthRefresh,
|
|
669
|
+
};
|
|
670
|
+
return { case: undefined };
|
|
671
|
+
}
|
|
672
|
+
function sourceKind(source) {
|
|
673
|
+
if (source.providerDefault !== undefined)
|
|
674
|
+
return {
|
|
675
|
+
case: "providerDefault",
|
|
676
|
+
value: {},
|
|
677
|
+
};
|
|
678
|
+
if (source.image !== undefined)
|
|
679
|
+
return { case: "image", value: source.image };
|
|
680
|
+
if (source.snapshot !== undefined)
|
|
681
|
+
return { case: "snapshot", value: source.snapshot };
|
|
682
|
+
if (source.warmPool !== undefined)
|
|
683
|
+
return { case: "warmPool", value: source.warmPool };
|
|
684
|
+
return { case: undefined };
|
|
685
|
+
}
|
|
686
|
+
function readinessCheck(readiness) {
|
|
687
|
+
if (readiness.tcpPort !== undefined)
|
|
688
|
+
return { case: "tcpPort", value: readiness.tcpPort };
|
|
689
|
+
if (readiness.command !== undefined)
|
|
690
|
+
return { case: "command", value: readiness.command };
|
|
691
|
+
return { case: undefined };
|
|
692
|
+
}
|
|
693
|
+
function unsupported(provider, operation) {
|
|
694
|
+
throw sandboxError({
|
|
695
|
+
kind: ErrorKind.Unsupported,
|
|
696
|
+
provider,
|
|
697
|
+
operation,
|
|
698
|
+
field: "",
|
|
699
|
+
message: "sandbox-kit: provider does not support " + operation,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
function withContext(error, provider, operation) {
|
|
703
|
+
if (error instanceof SandboxError) {
|
|
704
|
+
if (error.provider === provider && error.operation === operation)
|
|
705
|
+
return error;
|
|
706
|
+
return sandboxError({
|
|
707
|
+
kind: error.kind,
|
|
708
|
+
provider: error.provider || provider,
|
|
709
|
+
operation: error.operation || operation,
|
|
710
|
+
field: error.field,
|
|
711
|
+
message: error.message,
|
|
712
|
+
...(error.rule === undefined ? {} : { rule: error.rule }),
|
|
713
|
+
...(error.statusCode === undefined
|
|
714
|
+
? {}
|
|
715
|
+
: { statusCode: error.statusCode }),
|
|
716
|
+
...(error.providerCode === undefined
|
|
717
|
+
? {}
|
|
718
|
+
: { providerCode: error.providerCode }),
|
|
719
|
+
...(error.providerSource === undefined
|
|
720
|
+
? {}
|
|
721
|
+
: { providerSource: error.providerSource }),
|
|
722
|
+
}, error.cause);
|
|
723
|
+
}
|
|
724
|
+
if (error instanceof DOMException && error.name === "AbortError")
|
|
725
|
+
return sandboxError({
|
|
726
|
+
kind: ErrorKind.Canceled,
|
|
727
|
+
provider,
|
|
728
|
+
operation,
|
|
729
|
+
field: "",
|
|
730
|
+
message: error.message,
|
|
731
|
+
}, error);
|
|
732
|
+
if (error instanceof DOMException && error.name === "TimeoutError")
|
|
733
|
+
return sandboxError({
|
|
734
|
+
kind: ErrorKind.Timeout,
|
|
735
|
+
provider,
|
|
736
|
+
operation,
|
|
737
|
+
field: "",
|
|
738
|
+
message: error.message,
|
|
739
|
+
}, error);
|
|
740
|
+
return sandboxError({
|
|
741
|
+
kind: ErrorKind.Unknown,
|
|
742
|
+
provider,
|
|
743
|
+
operation,
|
|
744
|
+
field: "",
|
|
745
|
+
message: error instanceof Error
|
|
746
|
+
? error.message
|
|
747
|
+
: "sandbox-kit: provider operation failed",
|
|
748
|
+
}, error);
|
|
749
|
+
}
|