@sandbox-kit/daytona 0.1.0 → 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/README.md +66 -31
- package/dist/checks/provider_checks.js +62 -75
- package/dist/config/provider_config.d.ts +1 -2
- package/dist/config/provider_config.js +9 -18
- package/dist/daytona.js +10 -10
- package/dist/errors.d.ts +10 -1
- package/dist/errors.js +35 -6
- package/dist/mappings/mappings.d.ts +1 -2
- package/dist/mappings/mappings.js +11 -13
- package/dist/mappings/provider_mappings.js +122 -97
- package/package.json +13 -2
package/README.md
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@sandbox-kit/daytona`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Daytona provider for [`@sandbox-kit/sandbox`](https://www.npmjs.com/package/@sandbox-kit/sandbox).
|
|
4
|
+
Use the shared client API; this package supplies the `daytona()` provider.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
5
7
|
|
|
6
8
|
```sh
|
|
7
9
|
npm install @sandbox-kit/sandbox @sandbox-kit/daytona
|
|
8
10
|
```
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
Requires Node.js 20, 22, or 24.15+, and a Daytona API key (or credentials your
|
|
13
|
+
Daytona SDK already resolves from the environment).
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
11
16
|
|
|
12
17
|
```ts
|
|
13
|
-
import { Client } from "@sandbox-kit/sandbox";
|
|
14
18
|
import { daytona } from "@sandbox-kit/daytona";
|
|
19
|
+
import { Client } from "@sandbox-kit/sandbox";
|
|
15
20
|
|
|
16
21
|
await using client = new Client({
|
|
17
22
|
provider: daytona(),
|
|
@@ -19,38 +24,68 @@ await using client = new Client({
|
|
|
19
24
|
timeout: 120_000,
|
|
20
25
|
});
|
|
21
26
|
|
|
27
|
+
// Default snapshot for the target:
|
|
28
|
+
const sandbox = await client.create();
|
|
29
|
+
console.log(sandbox.id);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Create from an image and size resources:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
22
35
|
const sandbox = await client.create({
|
|
23
36
|
source: { image: { reference: "daytonaio/playground:latest" } },
|
|
37
|
+
resources: {
|
|
38
|
+
cpuCores: 2,
|
|
39
|
+
memoryMib: 4096n,
|
|
40
|
+
diskMib: 8192n,
|
|
41
|
+
},
|
|
24
42
|
runtime: { language: "typescript" },
|
|
25
43
|
});
|
|
26
|
-
console.log(sandbox.id);
|
|
27
44
|
```
|
|
28
45
|
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
Closing the client releases local SDK resources only. Daytona keeps the sandbox
|
|
47
|
+
until you stop or delete it, or a lifetime policy fires.
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
| Setting | Notes |
|
|
52
|
+
| --------------- | ------------------------------------------------------------- |
|
|
53
|
+
| API key | `auth: { apiKey: { key } }` |
|
|
54
|
+
| Bearer / JWT | `auth: { bearerToken: { token } }` — requires organization ID |
|
|
55
|
+
| Omitted `auth` | Daytona SDK environment credentials |
|
|
56
|
+
| Organization | `scope: { organizationId }` |
|
|
57
|
+
| Endpoint | `endpoint` — defaults to `https://app.daytona.io/api` |
|
|
58
|
+
| Region / target | `region` |
|
|
59
|
+
|
|
60
|
+
Remote endpoints require HTTPS. HTTP is allowed for localhost or loopback only.
|
|
61
|
+
|
|
62
|
+
## What you can do
|
|
63
|
+
|
|
64
|
+
**Create** from the default snapshot, a named snapshot, or a registry image.
|
|
65
|
+
Optional runtime language/user, network allowlists, secrets, labels, TTL,
|
|
66
|
+
ephemeral behavior, and stop/pause/archive/delete policies.
|
|
67
|
+
|
|
68
|
+
**Lifecycle** on the handle: `get`, `refresh`, `start`, `stop`, `pause`,
|
|
69
|
+
`archive`, `delete`, and local `close`. Capability depends on the sandbox class
|
|
70
|
+
(container, VM, Windows, GPU). Check with `sandbox.supports(...)` or
|
|
71
|
+
`sandbox.capabilities()`.
|
|
72
|
+
|
|
73
|
+
Daytona does not support unverified `attach`. Use `client.get(id)` for a
|
|
74
|
+
provider-verified handle.
|
|
75
|
+
|
|
76
|
+
CPU requests are whole cores. Memory and disk are whole GiB expressed as MiB.
|
|
77
|
+
Resource overrides on create require an **image** source; snapshot creates
|
|
78
|
+
inherit snapshot resources.
|
|
31
79
|
|
|
32
80
|
## Examples
|
|
33
81
|
|
|
34
|
-
| Example | Project
|
|
35
|
-
| ---------- |
|
|
36
|
-
| Creation | [create-daytona-sandbox](
|
|
37
|
-
| Policies | [create-daytona-with-policies](
|
|
38
|
-
| Retrieval | [get-daytona-sandbox](
|
|
39
|
-
| Full index | [TypeScript examples](
|
|
40
|
-
|
|
41
|
-
##
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
`mappings/provider_mappings.ts`, and `config/provider_config.ts`, kept in sync with
|
|
45
|
-
`specs/providers/daytona.yaml`. `src/daytona.ts` owns typed Daytona SDK call
|
|
46
|
-
ordering and lifecycle orchestration; `src/mappings.ts` owns remaining
|
|
47
|
-
provider-specific composition.
|
|
48
|
-
|
|
49
|
-
## Dependency audit
|
|
50
|
-
|
|
51
|
-
`npm audit` reports four high-severity findings in the pinned
|
|
52
|
-
`@daytona/sdk@0.225.0` dependency chain (`fast-glob` → `micromatch` → `braces`).
|
|
53
|
-
The registry reports no fix (`braces` latest is still vulnerable; Daytona still
|
|
54
|
-
depends on `fast-glob`). The provider does not override those transitive
|
|
55
|
-
packages, because overrides could create an unsupported SDK combination.
|
|
56
|
-
Reassess the audit when Daytona drops or patches that chain.
|
|
82
|
+
| Example | Project |
|
|
83
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
84
|
+
| Creation | [create-daytona-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/create-daytona-sandbox) |
|
|
85
|
+
| Policies | [create-daytona-with-policies](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/create-daytona-with-policies) |
|
|
86
|
+
| Retrieval | [get-daytona-sandbox](https://github.com/sandbox-kit/kit/tree/main/examples/typescript/daytona/get-daytona-sandbox) |
|
|
87
|
+
| Full index | [TypeScript examples](https://github.com/sandbox-kit/kit/tree/main/examples/typescript) |
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ErrorKind, PolicyMode,
|
|
1
|
+
import { ErrorKind, PolicyMode, CreateFieldLifetime, CreateFieldNetworkOutboundCidrsEntries, CreateFieldNetworkOutboundDomainsEntries, CreateFieldNetworkOutboundProxyUrl, CreateFieldRuntimeLanguage, CreateFieldSecuritySecretsEnvironmentVariable, validCidr, validDomain, validEnvName, } from "@sandbox-kit/sandbox";
|
|
2
|
+
import { providerError } from "#errors.js";
|
|
2
3
|
// validateProviderCreate rejects creation settings this provider cannot honor.
|
|
3
4
|
// Rules are inventoried in specs/providers/daytona.yaml checks.
|
|
4
5
|
export function validateProviderCreate(request) {
|
|
@@ -7,48 +8,98 @@ export function validateProviderCreate(request) {
|
|
|
7
8
|
language !== "python" &&
|
|
8
9
|
language !== "javascript" &&
|
|
9
10
|
language !== "typescript") {
|
|
10
|
-
|
|
11
|
+
providerError({
|
|
12
|
+
kind: ErrorKind.Unsupported,
|
|
13
|
+
operation: "create",
|
|
14
|
+
field: CreateFieldRuntimeLanguage,
|
|
15
|
+
message: "runtime.language is outside supported values",
|
|
16
|
+
});
|
|
11
17
|
}
|
|
12
18
|
const outboundProxyUrl = request.network?.outboundProxyUrl;
|
|
13
19
|
if (outboundProxyUrl !== undefined && !validHttpUrl(outboundProxyUrl)) {
|
|
14
|
-
|
|
20
|
+
providerError({
|
|
21
|
+
kind: ErrorKind.InvalidArgument,
|
|
22
|
+
operation: "create",
|
|
23
|
+
field: CreateFieldNetworkOutboundProxyUrl,
|
|
24
|
+
message: "network.outbound_proxy_url is outside supported values",
|
|
25
|
+
});
|
|
15
26
|
}
|
|
16
27
|
for (const value of request.network?.outboundCidrs ?? []) {
|
|
17
28
|
if (!validCidr(value)) {
|
|
18
|
-
|
|
29
|
+
providerError({
|
|
30
|
+
kind: ErrorKind.InvalidArgument,
|
|
31
|
+
operation: "create",
|
|
32
|
+
field: CreateFieldNetworkOutboundCidrsEntries,
|
|
33
|
+
message: "network.outbound_cidrs.entries is outside supported values",
|
|
34
|
+
});
|
|
19
35
|
}
|
|
20
36
|
}
|
|
21
37
|
for (const value of request.network?.outboundDomains ?? []) {
|
|
22
38
|
if (!validDomain(value)) {
|
|
23
|
-
|
|
39
|
+
providerError({
|
|
40
|
+
kind: ErrorKind.InvalidArgument,
|
|
41
|
+
operation: "create",
|
|
42
|
+
field: CreateFieldNetworkOutboundDomainsEntries,
|
|
43
|
+
message: "network.outbound_domains.entries is outside supported values",
|
|
44
|
+
});
|
|
24
45
|
}
|
|
25
46
|
}
|
|
26
47
|
for (const item of request.security?.secrets ?? []) {
|
|
27
48
|
const environmentVariable = item.environmentVariable;
|
|
28
49
|
if (environmentVariable !== undefined &&
|
|
29
50
|
!validEnvName(environmentVariable)) {
|
|
30
|
-
|
|
51
|
+
providerError({
|
|
52
|
+
kind: ErrorKind.InvalidArgument,
|
|
53
|
+
operation: "create",
|
|
54
|
+
field: CreateFieldSecuritySecretsEnvironmentVariable,
|
|
55
|
+
message: "security.secrets.environment_variable is outside supported values",
|
|
56
|
+
});
|
|
31
57
|
}
|
|
32
58
|
}
|
|
33
59
|
if (delayed(request.lifetime?.idleStop) &&
|
|
34
60
|
delayed(request.lifetime?.idlePause)) {
|
|
35
|
-
|
|
61
|
+
providerError({
|
|
62
|
+
kind: ErrorKind.InvalidArgument,
|
|
63
|
+
operation: "create",
|
|
64
|
+
field: CreateFieldLifetime,
|
|
65
|
+
message: "incompatible lifetime policies",
|
|
66
|
+
});
|
|
36
67
|
}
|
|
37
68
|
if (delayed(request.lifetime?.idlePause) &&
|
|
38
69
|
request.lifetime?.ephemeral === true) {
|
|
39
|
-
|
|
70
|
+
providerError({
|
|
71
|
+
kind: ErrorKind.InvalidArgument,
|
|
72
|
+
operation: "create",
|
|
73
|
+
field: CreateFieldLifetime,
|
|
74
|
+
message: "incompatible lifetime policies",
|
|
75
|
+
});
|
|
40
76
|
}
|
|
41
77
|
if (delayed(request.lifetime?.idlePause) &&
|
|
42
78
|
immediate(request.lifetime?.stoppedDelete)) {
|
|
43
|
-
|
|
79
|
+
providerError({
|
|
80
|
+
kind: ErrorKind.InvalidArgument,
|
|
81
|
+
operation: "create",
|
|
82
|
+
field: CreateFieldLifetime,
|
|
83
|
+
message: "incompatible lifetime policies",
|
|
84
|
+
});
|
|
44
85
|
}
|
|
45
86
|
if (delayed(request.lifetime?.stoppedDelete) &&
|
|
46
87
|
request.lifetime?.ephemeral === true) {
|
|
47
|
-
|
|
88
|
+
providerError({
|
|
89
|
+
kind: ErrorKind.InvalidArgument,
|
|
90
|
+
operation: "create",
|
|
91
|
+
field: CreateFieldLifetime,
|
|
92
|
+
message: "incompatible lifetime policies",
|
|
93
|
+
});
|
|
48
94
|
}
|
|
49
95
|
if (delayed(request.lifetime?.stoppedArchive) &&
|
|
50
96
|
request.lifetime?.ephemeral === true) {
|
|
51
|
-
|
|
97
|
+
providerError({
|
|
98
|
+
kind: ErrorKind.InvalidArgument,
|
|
99
|
+
operation: "create",
|
|
100
|
+
field: CreateFieldLifetime,
|
|
101
|
+
message: "incompatible lifetime policies",
|
|
102
|
+
});
|
|
52
103
|
}
|
|
53
104
|
}
|
|
54
105
|
function delayed(policy) {
|
|
@@ -61,9 +112,6 @@ function immediate(policy) {
|
|
|
61
112
|
policy?.after !== undefined &&
|
|
62
113
|
policy.after === 0);
|
|
63
114
|
}
|
|
64
|
-
function validEnvName(value) {
|
|
65
|
-
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
66
|
-
}
|
|
67
115
|
function validHttpUrl(value) {
|
|
68
116
|
try {
|
|
69
117
|
const parsed = new URL(value);
|
|
@@ -74,64 +122,3 @@ function validHttpUrl(value) {
|
|
|
74
122
|
return false;
|
|
75
123
|
}
|
|
76
124
|
}
|
|
77
|
-
function validDomain(value) {
|
|
78
|
-
const domain = value.startsWith("*.") ? value.slice(2) : value;
|
|
79
|
-
return (domain.length <= 253 &&
|
|
80
|
-
/^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.?$/i.test(domain));
|
|
81
|
-
}
|
|
82
|
-
function validCidr(value) {
|
|
83
|
-
const separator = value.lastIndexOf("/");
|
|
84
|
-
if (separator <= 0)
|
|
85
|
-
return false;
|
|
86
|
-
const address = value.slice(0, separator);
|
|
87
|
-
const prefix = Number(value.slice(separator + 1));
|
|
88
|
-
if (!Number.isInteger(prefix))
|
|
89
|
-
return false;
|
|
90
|
-
return address.includes(":")
|
|
91
|
-
? prefix >= 0 && prefix <= 128 && validIpv6(address)
|
|
92
|
-
: prefix >= 0 && prefix <= 32 && validIpv4(address);
|
|
93
|
-
}
|
|
94
|
-
function validIpv4(value) {
|
|
95
|
-
const parts = value.split(".");
|
|
96
|
-
return (parts.length === 4 &&
|
|
97
|
-
parts.every((part) => /^(0|[1-9][0-9]{0,2})$/.test(part) && Number(part) <= 255));
|
|
98
|
-
}
|
|
99
|
-
function validIpv6(value) {
|
|
100
|
-
if ((value.match(/::/g)?.length ?? 0) > 1)
|
|
101
|
-
return false;
|
|
102
|
-
const halves = value.split("::");
|
|
103
|
-
if (halves.length > 2)
|
|
104
|
-
return false;
|
|
105
|
-
const count = (part) => {
|
|
106
|
-
if (part === "")
|
|
107
|
-
return 0;
|
|
108
|
-
const groups = part.split(":");
|
|
109
|
-
let total = 0;
|
|
110
|
-
for (const group of groups) {
|
|
111
|
-
if (/^[0-9a-f]{1,4}$/i.test(group))
|
|
112
|
-
total++;
|
|
113
|
-
else if (validIpv4(group))
|
|
114
|
-
total += 2;
|
|
115
|
-
else
|
|
116
|
-
return undefined;
|
|
117
|
-
}
|
|
118
|
-
return total;
|
|
119
|
-
};
|
|
120
|
-
const left = count(halves[0] ?? "");
|
|
121
|
-
const right = count(halves[1] ?? "");
|
|
122
|
-
return (left !== undefined &&
|
|
123
|
-
right !== undefined &&
|
|
124
|
-
(halves.length === 2 ? left + right < 8 : left === 8));
|
|
125
|
-
}
|
|
126
|
-
function invalid(field, message) {
|
|
127
|
-
fail(ErrorKind.InvalidArgument, field, message);
|
|
128
|
-
}
|
|
129
|
-
function fail(kind, field, message) {
|
|
130
|
-
throw sandboxError({
|
|
131
|
-
kind,
|
|
132
|
-
provider: "daytona",
|
|
133
|
-
operation: "create",
|
|
134
|
-
field,
|
|
135
|
-
message: "sandbox-kit daytona: " + message,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import type { DaytonaConfig as NativeClientConfig } from "@daytona/sdk";
|
|
2
2
|
import { type Config } from "@sandbox-kit/sandbox";
|
|
3
|
-
export
|
|
4
|
-
export declare function mapGeneratedClientConfig(config: Config, environment?: Environment): NativeClientConfig;
|
|
3
|
+
export declare function mapGeneratedClientConfig(config: Config): NativeClientConfig;
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
// Client configuration composition for the native SDK constructor.
|
|
2
2
|
// Kept in sync with specs/providers/daytona.yaml client: rules (not emitted).
|
|
3
|
-
import { ErrorKind,
|
|
3
|
+
import { ErrorKind, ConfigFieldAuth, ConfigFieldScopeAppName, ConfigFieldScopeEnvironment, ConfigFieldScopeProjectId, } from "@sandbox-kit/sandbox";
|
|
4
4
|
import { mapAPIKeyCredentials, mapBearerAuth, mapClientSettings, mapScope, } from "#mappings/provider_mappings.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
environment.DAYTONA_SERVER_URL ??
|
|
12
|
-
"https://app.daytona.io/api",
|
|
13
|
-
};
|
|
14
|
-
validateConfig(source, "daytona", "initialize");
|
|
5
|
+
import { providerError } from "#errors.js";
|
|
6
|
+
const defaultEndpoint = "https://app.daytona.io/api";
|
|
7
|
+
export function mapGeneratedClientConfig(config) {
|
|
8
|
+
const source = config.endpoint === undefined
|
|
9
|
+
? { ...config, endpoint: defaultEndpoint }
|
|
10
|
+
: config;
|
|
15
11
|
const target = mapClientSettings(source, "initialize");
|
|
16
12
|
if (source.scope !== undefined) {
|
|
17
13
|
const mapped = mapScope(source.scope, "initialize");
|
|
@@ -34,16 +30,11 @@ export function mapGeneratedClientConfig(config, environment = environmentVariab
|
|
|
34
30
|
}
|
|
35
31
|
return target;
|
|
36
32
|
}
|
|
37
|
-
function environmentVariables() {
|
|
38
|
-
return (globalThis
|
|
39
|
-
.process?.env ?? {});
|
|
40
|
-
}
|
|
41
33
|
function unsupported(field, message) {
|
|
42
|
-
|
|
34
|
+
providerError({
|
|
43
35
|
kind: ErrorKind.Unsupported,
|
|
44
|
-
provider: "daytona",
|
|
45
36
|
operation: "initialize",
|
|
46
37
|
field,
|
|
47
|
-
message
|
|
38
|
+
message,
|
|
48
39
|
});
|
|
49
40
|
}
|
package/dist/daytona.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Daytona, } from "@daytona/sdk";
|
|
2
|
-
import {
|
|
3
|
-
import { IsolationKind, LifecycleCapability, metadataObjectAsMap } from "@sandbox-kit/sandbox";
|
|
2
|
+
import { IsolationKind, LifecycleCapability, metadataObjectAsMap, } from "@sandbox-kit/sandbox";
|
|
4
3
|
import { mapClientConfig, mapCreateRequest, mapSandboxInfo, } from "#mappings/mappings.js";
|
|
5
|
-
import { mapDaytonaError } from "
|
|
4
|
+
import { mapDaytonaError } from "#errors.js";
|
|
6
5
|
import { BackendBase, ProviderBase } from "./provider.js";
|
|
7
6
|
/** @internal Exported from this module for boundary testing, but not from the package root. */
|
|
8
7
|
export class DaytonaProvider extends ProviderBase {
|
|
@@ -30,7 +29,7 @@ class DaytonaBackend extends BackendBase {
|
|
|
30
29
|
try {
|
|
31
30
|
options.signal?.throwIfAborted();
|
|
32
31
|
const params = mapCreateRequest(request);
|
|
33
|
-
const timeout = options.timeout
|
|
32
|
+
const timeout = secondsFromMs(options.timeout);
|
|
34
33
|
const sandbox = await this.native.create(params, timeout === undefined ? undefined : { timeout });
|
|
35
34
|
return { sandbox: mapSandboxInfo(sandbox, "create") };
|
|
36
35
|
}
|
|
@@ -65,19 +64,19 @@ class DaytonaBackend extends BackendBase {
|
|
|
65
64
|
});
|
|
66
65
|
}
|
|
67
66
|
async start(id, request) {
|
|
68
|
-
return this.withSandbox(id, "start", (value) => value.start(
|
|
67
|
+
return this.withSandbox(id, "start", (value) => value.start(secondsFromMs(request.timeout)));
|
|
69
68
|
}
|
|
70
69
|
async stop(id, request) {
|
|
71
|
-
return this.withSandbox(id, "stop", (value) => value.stop(
|
|
70
|
+
return this.withSandbox(id, "stop", (value) => value.stop(secondsFromMs(request.timeout), request.force));
|
|
72
71
|
}
|
|
73
72
|
async pause(id, request) {
|
|
74
|
-
return this.withSandbox(id, "pause", (value) => value.pause(
|
|
73
|
+
return this.withSandbox(id, "pause", (value) => value.pause(secondsFromMs(request.timeout)));
|
|
75
74
|
}
|
|
76
75
|
async archive(id, _request) {
|
|
77
76
|
return this.withSandbox(id, "archive", (value) => value.archive());
|
|
78
77
|
}
|
|
79
78
|
async delete(id, request) {
|
|
80
|
-
return this.withSandbox(id, "delete", (value) => value.delete(
|
|
79
|
+
return this.withSandbox(id, "delete", (value) => value.delete(secondsFromMs(request.timeout), request.wait));
|
|
81
80
|
}
|
|
82
81
|
async closeSandbox(_id) { }
|
|
83
82
|
async withSandbox(id, operation, action) {
|
|
@@ -99,8 +98,9 @@ class DaytonaBackend extends BackendBase {
|
|
|
99
98
|
}
|
|
100
99
|
}
|
|
101
100
|
}
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
/** Native Daytona lifecycle APIs take whole seconds; Kit timeouts are ms. */
|
|
102
|
+
function secondsFromMs(value) {
|
|
103
|
+
return value === undefined ? undefined : value / 1_000;
|
|
104
104
|
}
|
|
105
105
|
/** Selects Daytona as the provider for the shared Sandbox Kit client. */
|
|
106
106
|
export function daytona() {
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
|
-
import { SandboxError } from "@sandbox-kit/sandbox";
|
|
1
|
+
import { SandboxError, type ErrorInfo } from "@sandbox-kit/sandbox";
|
|
2
|
+
/** Inputs for throwing a Daytona-scoped validation or mapping error. */
|
|
3
|
+
export interface ProviderErrorInput {
|
|
4
|
+
kind: ErrorInfo["kind"];
|
|
5
|
+
operation: string;
|
|
6
|
+
field: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
2
9
|
export declare function mapDaytonaError(error: unknown, operation: string): SandboxError;
|
|
10
|
+
/** Local throw helper for mappings, checks, and config. */
|
|
11
|
+
export declare function providerError(input: ProviderErrorInput): never;
|
package/dist/errors.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
// Classification order: existing SandboxError, abort/timeout DOMException,
|
|
3
3
|
// then DaytonaError code/status maps, then unknown. Cause stays on SandboxError.cause.
|
|
4
4
|
import { DaytonaError } from "@daytona/sdk";
|
|
5
|
-
import { ErrorKind, SandboxError, sandboxError } from "@sandbox-kit/sandbox";
|
|
5
|
+
import { ErrorKind, SandboxError, sandboxError, } from "@sandbox-kit/sandbox";
|
|
6
6
|
export function mapDaytonaError(error, operation) {
|
|
7
7
|
if (error instanceof SandboxError)
|
|
8
8
|
return error;
|
|
9
9
|
if (error instanceof DOMException && error.name === "AbortError")
|
|
10
|
-
return create(ErrorKind.Canceled, error, operation);
|
|
10
|
+
return create({ kind: ErrorKind.Canceled, error, operation });
|
|
11
11
|
if (error instanceof DOMException && error.name === "TimeoutError")
|
|
12
|
-
return create(ErrorKind.Timeout, error, operation);
|
|
12
|
+
return create({ kind: ErrorKind.Timeout, error, operation });
|
|
13
13
|
let kind = ErrorKind.Unknown;
|
|
14
14
|
let statusCode;
|
|
15
15
|
let providerCode;
|
|
@@ -72,8 +72,37 @@ export function mapDaytonaError(error, operation) {
|
|
|
72
72
|
if (kind === ErrorKind.Unknown && error.statusCode === 0 && !error.code)
|
|
73
73
|
kind = ErrorKind.Unavailable;
|
|
74
74
|
}
|
|
75
|
-
return create(
|
|
75
|
+
return create({
|
|
76
|
+
kind,
|
|
77
|
+
error,
|
|
78
|
+
operation,
|
|
79
|
+
...(statusCode === undefined ? {} : { statusCode }),
|
|
80
|
+
...(providerCode === undefined ? {} : { providerCode }),
|
|
81
|
+
...(providerSource === undefined ? {} : { providerSource }),
|
|
82
|
+
});
|
|
76
83
|
}
|
|
77
|
-
function create(
|
|
78
|
-
|
|
84
|
+
function create(input) {
|
|
85
|
+
const { kind, error, operation, statusCode, providerCode, providerSource } = input;
|
|
86
|
+
return sandboxError({
|
|
87
|
+
kind,
|
|
88
|
+
provider: "daytona",
|
|
89
|
+
operation,
|
|
90
|
+
field: "",
|
|
91
|
+
message: error instanceof Error
|
|
92
|
+
? error.message
|
|
93
|
+
: "sandbox-kit daytona: provider operation failed",
|
|
94
|
+
...(statusCode === undefined ? {} : { statusCode }),
|
|
95
|
+
...(providerCode === undefined ? {} : { providerCode }),
|
|
96
|
+
...(providerSource === undefined ? {} : { providerSource }),
|
|
97
|
+
}, error);
|
|
98
|
+
}
|
|
99
|
+
/** Local throw helper for mappings, checks, and config. */
|
|
100
|
+
export function providerError(input) {
|
|
101
|
+
throw sandboxError({
|
|
102
|
+
kind: input.kind,
|
|
103
|
+
provider: "daytona",
|
|
104
|
+
operation: input.operation,
|
|
105
|
+
field: input.field,
|
|
106
|
+
message: "sandbox-kit daytona: " + input.message,
|
|
107
|
+
});
|
|
79
108
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type Config, type CreateOptions, type SandboxInfo } from "@sandbox-kit/sandbox";
|
|
2
2
|
import { type CreateSandboxFromImageParams, type CreateSandboxFromSnapshotParams, type DaytonaConfig, type Sandbox as DaytonaSandbox } from "@daytona/sdk";
|
|
3
|
-
import { type Environment } from "#config/provider_config.js";
|
|
4
3
|
export type DaytonaCreateParams = CreateSandboxFromImageParams | CreateSandboxFromSnapshotParams;
|
|
5
|
-
export declare function mapClientConfig(config: Config
|
|
4
|
+
export declare function mapClientConfig(config: Config): DaytonaConfig;
|
|
6
5
|
export declare function mapCreateRequest(request: CreateOptions): DaytonaCreateParams;
|
|
7
6
|
export declare function mapSandboxInfo(value: DaytonaSandbox, operation?: string): SandboxInfo;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// Provider-specific orchestration around generated portable field mappings.
|
|
2
|
-
import { EgressMode, ErrorKind, IsolationKind, SandboxState, SecretInjection, SnapshotKind, ValueOrigin, WaitCondition,
|
|
2
|
+
import { EgressMode, ErrorKind, IsolationKind, SandboxState, SecretInjection, SnapshotKind, ValueOrigin, WaitCondition, CreateFieldIsolation, CreateFieldNetwork, CreateFieldNetworkEgress, CreateFieldNetworkLinkedSandbox, CreateFieldObservabilityVerbose, CreateFieldPlacement, CreateFieldProvisioningWaitFor, CreateFieldReadiness, CreateFieldResources, CreateFieldResourcesCpuCores, CreateFieldRuntime, CreateFieldSecuritySecretsEnvironmentVariable, CreateFieldSecuritySecretsInjection, CreateFieldSourceSnapshotKind, CreateFieldRuntimeUser, CreateFieldSecuritySecretsReference, CreateFieldSecurityWorkloadIdentity, CreateFieldSourceImageReference, CreateFieldSourceSnapshotProviderKind, CreateFieldSourceSnapshotReference, CreateFieldSourceWarmPool, CreateFieldNetworkOutboundCidrs, CreateFieldNetworkOutboundDomains, CreateFieldLifetimeIdleTerminate, CreateFieldObservabilityTelemetryEndpoint, } from "@sandbox-kit/sandbox";
|
|
3
3
|
import { metadataObjectFromMap } from "@sandbox-kit/sandbox";
|
|
4
4
|
import {} from "@daytona/sdk";
|
|
5
5
|
import { mapAllocatedResources as generatedMapAllocatedResources, mapCommonCreateFields as generatedMapCommonCreateFields, mapLifetimePolicies as generatedMapLifetimePolicies, mapResources as generatedMapResources, mapSandboxInfo as generatedMapSandboxInfo, } from "./provider_mappings.js";
|
|
6
6
|
import { validateProviderCreate } from "#checks/provider_checks.js";
|
|
7
|
-
import { mapGeneratedClientConfig
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import { mapGeneratedClientConfig } from "#config/provider_config.js";
|
|
8
|
+
import { providerError } from "#errors.js";
|
|
9
|
+
export function mapClientConfig(config) {
|
|
10
|
+
const result = mapGeneratedClientConfig(config);
|
|
10
11
|
if (config.auth?.bearerToken !== undefined &&
|
|
11
12
|
config.scope?.organizationId === undefined)
|
|
12
13
|
configInvalid("scope.organizationId", "organization ID is required with bearer authentication");
|
|
@@ -313,29 +314,26 @@ function nonblank(value, field) {
|
|
|
313
314
|
return value;
|
|
314
315
|
}
|
|
315
316
|
function invalid(field, message) {
|
|
316
|
-
|
|
317
|
+
providerError({
|
|
317
318
|
kind: ErrorKind.InvalidArgument,
|
|
318
|
-
provider: "daytona",
|
|
319
319
|
operation: "create",
|
|
320
320
|
field,
|
|
321
|
-
message
|
|
321
|
+
message,
|
|
322
322
|
});
|
|
323
323
|
}
|
|
324
324
|
function unsupported(field, message) {
|
|
325
|
-
|
|
325
|
+
providerError({
|
|
326
326
|
kind: ErrorKind.Unsupported,
|
|
327
|
-
provider: "daytona",
|
|
328
327
|
operation: "create",
|
|
329
328
|
field,
|
|
330
|
-
message
|
|
329
|
+
message,
|
|
331
330
|
});
|
|
332
331
|
}
|
|
333
332
|
function configInvalid(field, message) {
|
|
334
|
-
|
|
333
|
+
providerError({
|
|
335
334
|
kind: ErrorKind.InvalidArgument,
|
|
336
|
-
provider: "daytona",
|
|
337
335
|
operation: "initialize",
|
|
338
336
|
field,
|
|
339
|
-
message
|
|
337
|
+
message,
|
|
340
338
|
});
|
|
341
339
|
}
|
|
@@ -1,95 +1,101 @@
|
|
|
1
1
|
// Provider field mappings between shared CreateOptions/Config and the native SDK.
|
|
2
2
|
// Inventoried in specs/providers/daytona.yaml groups; kept in sync by hand (not emitted).
|
|
3
|
-
import { ErrorKind, PolicyMode, ValueOrigin,
|
|
3
|
+
import { ErrorKind, PolicyMode, ValueOrigin, CreateFieldLifetimeIdlePause, CreateFieldLifetimeIdleStop, CreateFieldLifetimeStoppedArchive, CreateFieldLifetimeStoppedDelete, CreateFieldResourcesCpuCores, CreateFieldResourcesDiskMib, CreateFieldResourcesMemoryMib, InfoFieldId, InfoFieldLabels, InfoFieldName, InfoFieldProviderState, InfoFieldRegion, } from "@sandbox-kit/sandbox";
|
|
4
|
+
import { providerError } from "#errors.js";
|
|
4
5
|
export function mapLifetimePolicies(source, operation = "create") {
|
|
5
6
|
const target = {};
|
|
6
7
|
if (source === undefined)
|
|
7
8
|
return target;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
else if (mode === PolicyMode.After) {
|
|
57
|
-
if (policy.after === undefined)
|
|
58
|
-
mappingError(ErrorKind.InvalidArgument, CreateFieldLifetimeStoppedArchive, "duration is required", operation);
|
|
59
|
-
const afterMs = policy.after;
|
|
60
|
-
if (afterMs === 0)
|
|
61
|
-
mappingError(ErrorKind.Unsupported, CreateFieldLifetimeStoppedArchive, "immediate action is not representable", operation);
|
|
62
|
-
if (!Number.isFinite(afterMs) || afterMs < 0 || afterMs % 60_000 !== 0)
|
|
63
|
-
mappingError(ErrorKind.InvalidArgument, CreateFieldLifetimeStoppedArchive, "delay cannot be represented in whole minutes", operation);
|
|
64
|
-
const minutes = afterMs / 60_000;
|
|
65
|
-
if (minutes > 43200)
|
|
66
|
-
mappingError(ErrorKind.InvalidArgument, CreateFieldLifetimeStoppedArchive, "delay exceeds documented maximum", operation);
|
|
67
|
-
target.autoArchiveInterval = minutes;
|
|
68
|
-
}
|
|
69
|
-
else if (mode !== PolicyMode.Default) {
|
|
70
|
-
mappingError(ErrorKind.InvalidArgument, CreateFieldLifetimeStoppedArchive, "unknown policy mode", operation);
|
|
71
|
-
}
|
|
9
|
+
const idleStop = mapMinutePolicy(source.idleStop, {
|
|
10
|
+
field: CreateFieldLifetimeIdleStop,
|
|
11
|
+
operation,
|
|
12
|
+
allowDisable: true,
|
|
13
|
+
forbidImmediate: true,
|
|
14
|
+
});
|
|
15
|
+
if (idleStop !== undefined)
|
|
16
|
+
target.autoStopInterval = idleStop;
|
|
17
|
+
const idlePause = mapMinutePolicy(source.idlePause, {
|
|
18
|
+
field: CreateFieldLifetimeIdlePause,
|
|
19
|
+
operation,
|
|
20
|
+
allowDisable: true,
|
|
21
|
+
forbidImmediate: true,
|
|
22
|
+
});
|
|
23
|
+
if (idlePause !== undefined)
|
|
24
|
+
target.autoPauseInterval = idlePause;
|
|
25
|
+
const stoppedArchive = mapMinutePolicy(source.stoppedArchive, {
|
|
26
|
+
field: CreateFieldLifetimeStoppedArchive,
|
|
27
|
+
operation,
|
|
28
|
+
allowDisable: false,
|
|
29
|
+
forbidImmediate: true,
|
|
30
|
+
maxMinutes: 43200,
|
|
31
|
+
});
|
|
32
|
+
if (stoppedArchive !== undefined)
|
|
33
|
+
target.autoArchiveInterval = stoppedArchive;
|
|
34
|
+
const stoppedDelete = mapMinutePolicy(source.stoppedDelete, {
|
|
35
|
+
field: CreateFieldLifetimeStoppedDelete,
|
|
36
|
+
operation,
|
|
37
|
+
allowDisable: false,
|
|
38
|
+
forbidImmediate: false,
|
|
39
|
+
});
|
|
40
|
+
if (stoppedDelete !== undefined)
|
|
41
|
+
target.autoDeleteInterval = stoppedDelete;
|
|
42
|
+
return target;
|
|
43
|
+
}
|
|
44
|
+
function mapMinutePolicy(policy, spec) {
|
|
45
|
+
if (policy === undefined)
|
|
46
|
+
return undefined;
|
|
47
|
+
const mode = policy.mode ?? PolicyMode.Default;
|
|
48
|
+
if (mode === PolicyMode.Disabled) {
|
|
49
|
+
if (!spec.allowDisable)
|
|
50
|
+
mappingError({
|
|
51
|
+
kind: ErrorKind.Unsupported,
|
|
52
|
+
field: spec.field,
|
|
53
|
+
message: "explicit disabling is not representable",
|
|
54
|
+
operation: spec.operation,
|
|
55
|
+
});
|
|
56
|
+
return 0;
|
|
72
57
|
}
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
58
|
+
if (mode === PolicyMode.After) {
|
|
59
|
+
if (policy.after === undefined)
|
|
60
|
+
mappingError({
|
|
61
|
+
kind: ErrorKind.InvalidArgument,
|
|
62
|
+
field: spec.field,
|
|
63
|
+
message: "duration is required",
|
|
64
|
+
operation: spec.operation,
|
|
65
|
+
});
|
|
66
|
+
const afterMs = policy.after;
|
|
67
|
+
if (spec.forbidImmediate && afterMs === 0)
|
|
68
|
+
mappingError({
|
|
69
|
+
kind: ErrorKind.Unsupported,
|
|
70
|
+
field: spec.field,
|
|
71
|
+
message: "immediate action is not representable",
|
|
72
|
+
operation: spec.operation,
|
|
73
|
+
});
|
|
74
|
+
if (!Number.isFinite(afterMs) || afterMs < 0 || afterMs % 60_000 !== 0)
|
|
75
|
+
mappingError({
|
|
76
|
+
kind: ErrorKind.InvalidArgument,
|
|
77
|
+
field: spec.field,
|
|
78
|
+
message: "delay cannot be represented in whole minutes",
|
|
79
|
+
operation: spec.operation,
|
|
80
|
+
});
|
|
81
|
+
const minutes = afterMs / 60_000;
|
|
82
|
+
if (spec.maxMinutes !== undefined && minutes > spec.maxMinutes)
|
|
83
|
+
mappingError({
|
|
84
|
+
kind: ErrorKind.InvalidArgument,
|
|
85
|
+
field: spec.field,
|
|
86
|
+
message: "delay exceeds documented maximum",
|
|
87
|
+
operation: spec.operation,
|
|
88
|
+
});
|
|
89
|
+
return minutes;
|
|
91
90
|
}
|
|
92
|
-
|
|
91
|
+
if (mode !== PolicyMode.Default)
|
|
92
|
+
mappingError({
|
|
93
|
+
kind: ErrorKind.InvalidArgument,
|
|
94
|
+
field: spec.field,
|
|
95
|
+
message: "unknown policy mode",
|
|
96
|
+
operation: spec.operation,
|
|
97
|
+
});
|
|
98
|
+
return undefined;
|
|
93
99
|
}
|
|
94
100
|
export function mapCommonCreateFields(source, operation = "create") {
|
|
95
101
|
const target = {};
|
|
@@ -123,7 +129,12 @@ export function mapResources(source, operation = "create") {
|
|
|
123
129
|
if (source.cpuCores !== undefined) {
|
|
124
130
|
const value = source.cpuCores;
|
|
125
131
|
if (!Number.isInteger(value) || value > 2147483647)
|
|
126
|
-
mappingError(
|
|
132
|
+
mappingError({
|
|
133
|
+
kind: ErrorKind.InvalidArgument,
|
|
134
|
+
field: CreateFieldResourcesCpuCores,
|
|
135
|
+
message: "cpu_cores cannot be represented",
|
|
136
|
+
operation,
|
|
137
|
+
});
|
|
127
138
|
target.cpu = Number(value);
|
|
128
139
|
}
|
|
129
140
|
}
|
|
@@ -131,7 +142,12 @@ export function mapResources(source, operation = "create") {
|
|
|
131
142
|
if (source.memoryMib !== undefined) {
|
|
132
143
|
const value = source.memoryMib;
|
|
133
144
|
if (value % 1024n !== 0n || value / 1024n > 2147483647n)
|
|
134
|
-
mappingError(
|
|
145
|
+
mappingError({
|
|
146
|
+
kind: ErrorKind.InvalidArgument,
|
|
147
|
+
field: CreateFieldResourcesMemoryMib,
|
|
148
|
+
message: "memory_mib cannot be represented",
|
|
149
|
+
operation,
|
|
150
|
+
});
|
|
135
151
|
target.memory = Number(value / 1024n);
|
|
136
152
|
}
|
|
137
153
|
}
|
|
@@ -139,7 +155,12 @@ export function mapResources(source, operation = "create") {
|
|
|
139
155
|
if (source.diskMib !== undefined) {
|
|
140
156
|
const value = source.diskMib;
|
|
141
157
|
if (value % 1024n !== 0n || value / 1024n > 2147483647n)
|
|
142
|
-
mappingError(
|
|
158
|
+
mappingError({
|
|
159
|
+
kind: ErrorKind.InvalidArgument,
|
|
160
|
+
field: CreateFieldResourcesDiskMib,
|
|
161
|
+
message: "disk_mib cannot be represented",
|
|
162
|
+
operation,
|
|
163
|
+
});
|
|
143
164
|
target.disk = Number(value / 1024n);
|
|
144
165
|
}
|
|
145
166
|
}
|
|
@@ -207,7 +228,12 @@ export function mapAllocatedResources(source, operation = "create") {
|
|
|
207
228
|
if (source.memory !== undefined) {
|
|
208
229
|
const value = source.memory;
|
|
209
230
|
if (!Number.isSafeInteger(value) || value < 0)
|
|
210
|
-
mappingError(
|
|
231
|
+
mappingError({
|
|
232
|
+
kind: ErrorKind.InvalidResponse,
|
|
233
|
+
field: "memory",
|
|
234
|
+
message: "memory cannot be represented",
|
|
235
|
+
operation,
|
|
236
|
+
});
|
|
211
237
|
target.memoryMib = BigInt(value) * 1024n;
|
|
212
238
|
}
|
|
213
239
|
}
|
|
@@ -215,7 +241,12 @@ export function mapAllocatedResources(source, operation = "create") {
|
|
|
215
241
|
if (source.disk !== undefined) {
|
|
216
242
|
const value = source.disk;
|
|
217
243
|
if (!Number.isSafeInteger(value) || value < 0)
|
|
218
|
-
mappingError(
|
|
244
|
+
mappingError({
|
|
245
|
+
kind: ErrorKind.InvalidResponse,
|
|
246
|
+
field: "disk",
|
|
247
|
+
message: "disk cannot be represented",
|
|
248
|
+
operation,
|
|
249
|
+
});
|
|
219
250
|
target.diskMib = BigInt(value) * 1024n;
|
|
220
251
|
}
|
|
221
252
|
}
|
|
@@ -271,12 +302,6 @@ export function mapScope(source, operation = "initialize") {
|
|
|
271
302
|
}
|
|
272
303
|
return target;
|
|
273
304
|
}
|
|
274
|
-
function mappingError(
|
|
275
|
-
|
|
276
|
-
kind,
|
|
277
|
-
provider: "daytona",
|
|
278
|
-
operation,
|
|
279
|
-
field,
|
|
280
|
-
message: "sandbox-kit daytona: " + message,
|
|
281
|
-
});
|
|
305
|
+
function mappingError(input) {
|
|
306
|
+
providerError(input);
|
|
282
307
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandbox-kit/daytona",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "daytona provider for sandbox kit",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sandbox-kit",
|
|
7
|
+
"sandbox",
|
|
8
|
+
"daytona",
|
|
9
|
+
"sdk",
|
|
10
|
+
"provider"
|
|
11
|
+
],
|
|
5
12
|
"license": "MIT",
|
|
6
13
|
"type": "module",
|
|
7
14
|
"packageManager": "npm@11.19.0",
|
|
@@ -45,6 +52,10 @@
|
|
|
45
52
|
"#config/*.js": {
|
|
46
53
|
"@sandbox-kit/source": "./src/config/*.ts",
|
|
47
54
|
"default": "./dist/config/*.js"
|
|
55
|
+
},
|
|
56
|
+
"#errors.js": {
|
|
57
|
+
"@sandbox-kit/source": "./src/errors.ts",
|
|
58
|
+
"default": "./dist/errors.js"
|
|
48
59
|
}
|
|
49
60
|
},
|
|
50
61
|
"scripts": {
|
|
@@ -55,7 +66,7 @@
|
|
|
55
66
|
"dependencies": {
|
|
56
67
|
"@bufbuild/protobuf": "2.16.0",
|
|
57
68
|
"@daytona/sdk": "0.225.0",
|
|
58
|
-
"@sandbox-kit/sandbox": "0.1.
|
|
69
|
+
"@sandbox-kit/sandbox": "0.1.1"
|
|
59
70
|
},
|
|
60
71
|
"devDependencies": {
|
|
61
72
|
"@types/node": "24.10.1",
|