@schemavaults/sandbox-management-sdk 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 +295 -2
- package/dist/commands.d.ts +16 -0
- package/dist/commands.js +99 -0
- package/dist/dbh/index.d.ts +4 -0
- package/dist/dbh/index.js +2 -0
- package/dist/dbh/migration.d.ts +18 -0
- package/dist/dbh/migration.js +41 -0
- package/dist/dbh/store.d.ts +42 -0
- package/dist/dbh/store.js +105 -0
- package/dist/docker/egress-proxy/config.d.ts +20 -0
- package/dist/docker/egress-proxy/config.js +29 -0
- package/dist/docker/egress-proxy/main.d.ts +1 -0
- package/dist/docker/egress-proxy/main.js +19 -0
- package/dist/docker/egress-proxy/policy.d.ts +49 -0
- package/dist/docker/egress-proxy/policy.js +89 -0
- package/dist/docker/egress-proxy/server.d.ts +25 -0
- package/dist/docker/egress-proxy/server.js +225 -0
- package/dist/docker/egress.d.ts +41 -0
- package/dist/docker/egress.js +67 -0
- package/dist/docker/index.d.ts +3 -0
- package/dist/docker/index.js +2 -0
- package/dist/docker/provider.d.ts +101 -0
- package/dist/docker/provider.js +373 -0
- package/dist/docker/tar.d.ts +20 -0
- package/dist/docker/tar.js +124 -0
- package/dist/errors.d.ts +40 -0
- package/dist/errors.js +67 -0
- package/dist/in-memory-store.d.ts +11 -0
- package/dist/in-memory-store.js +40 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +8 -0
- package/dist/internal/base64.d.ts +7 -0
- package/dist/internal/base64.js +20 -0
- package/dist/internal/output.d.ts +6 -0
- package/dist/internal/output.js +25 -0
- package/dist/internal/posix-path.d.ts +8 -0
- package/dist/internal/posix-path.js +38 -0
- package/dist/manager.d.ts +126 -0
- package/dist/manager.js +477 -0
- package/dist/network.d.ts +27 -0
- package/dist/network.js +128 -0
- package/dist/provider.d.ts +77 -0
- package/dist/provider.js +1 -0
- package/dist/shell-main-process.d.ts +27 -0
- package/dist/shell-main-process.js +94 -0
- package/dist/store.d.ts +36 -0
- package/dist/store.js +19 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/mock-provider.d.ts +99 -0
- package/dist/testing/mock-provider.js +181 -0
- package/dist/types.d.ts +158 -0
- package/dist/types.js +21 -0
- package/dist/vercel/index.d.ts +2 -0
- package/dist/vercel/index.js +1 -0
- package/dist/vercel/provider.d.ts +125 -0
- package/dist/vercel/provider.js +190 -0
- package/dist/web-storage/index.d.ts +2 -0
- package/dist/web-storage/index.js +1 -0
- package/dist/web-storage/store.d.ts +53 -0
- package/dist/web-storage/store.js +118 -0
- package/package.json +117 -4
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A command to run inside a sandbox.
|
|
3
|
+
*
|
|
4
|
+
* - A `string` is run through `sh -c`, so pipes, `&&` and variable expansion work.
|
|
5
|
+
* - An object is run as an exact argv with no implicit shell.
|
|
6
|
+
*/
|
|
7
|
+
export type CommandInput = string | ArgvCommand;
|
|
8
|
+
export interface ArgvCommand {
|
|
9
|
+
cmd: string;
|
|
10
|
+
args?: string[];
|
|
11
|
+
/** Working directory. Relative paths resolve against the sandbox workdir. */
|
|
12
|
+
cwd?: string;
|
|
13
|
+
/** Extra environment variables, merged over the sandbox's `spec.env`. */
|
|
14
|
+
env?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
/** A command after normalization: always argv, `cwd` always absolute. */
|
|
17
|
+
export interface NormalizedCommand {
|
|
18
|
+
cmd: string;
|
|
19
|
+
args: string[];
|
|
20
|
+
cwd: string;
|
|
21
|
+
env: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
export interface GitSource {
|
|
24
|
+
/** Clone URL, e.g. `https://github.com/org/repo.git`. */
|
|
25
|
+
url: string;
|
|
26
|
+
/** Branch, tag or commit SHA to check out. Defaults to the remote HEAD. */
|
|
27
|
+
revision?: string;
|
|
28
|
+
/** Shallow clone depth. Omit for full history. */
|
|
29
|
+
depth?: number;
|
|
30
|
+
/** HTTP basic credentials, e.g. `{ username: "x-access-token", password: token }`. */
|
|
31
|
+
auth?: {
|
|
32
|
+
username: string;
|
|
33
|
+
password: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export interface SandboxResources {
|
|
37
|
+
vcpus?: number;
|
|
38
|
+
memoryMb?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Outbound network access for a sandbox, enforced by its provider.
|
|
42
|
+
*
|
|
43
|
+
* - `"allow-all"` (the default): no restriction.
|
|
44
|
+
* - `"deny-all"`: no outbound access at all.
|
|
45
|
+
* - An allowlist: only the listed destinations are reachable.
|
|
46
|
+
*
|
|
47
|
+
* The manager refuses to create a sandbox on a provider that cannot enforce
|
|
48
|
+
* the policy, so a policy is never silently ignored.
|
|
49
|
+
*/
|
|
50
|
+
export type SandboxNetworkPolicy = "allow-all" | "deny-all" | SandboxNetworkAllowlist;
|
|
51
|
+
/** Everything not listed is denied, so an empty allowlist means no access. */
|
|
52
|
+
export interface SandboxNetworkAllowlist {
|
|
53
|
+
/**
|
|
54
|
+
* Host names the sandbox may reach, e.g. `"github.com"`. A leading `*.`
|
|
55
|
+
* matches subdomains (`"*.npmjs.org"`); `"*"` matches every domain.
|
|
56
|
+
*/
|
|
57
|
+
allowDomains?: string[];
|
|
58
|
+
/** IP addresses or CIDR ranges the sandbox may reach, e.g. `"203.0.113.10"` or `"10.0.0.0/8"`. */
|
|
59
|
+
allowCidrs?: string[];
|
|
60
|
+
/** IP addresses or CIDR ranges that are always blocked, even for allowed domains. */
|
|
61
|
+
denyCidrs?: string[];
|
|
62
|
+
}
|
|
63
|
+
/** The kind of a `SandboxNetworkPolicy`. Providers declare which kinds they enforce. */
|
|
64
|
+
export type SandboxNetworkPolicyKind = "allow-all" | "deny-all" | "allowlist";
|
|
65
|
+
/**
|
|
66
|
+
* Provider-specific spec options, keyed by provider name. Each adapter
|
|
67
|
+
* augments this interface with its own typed key (`docker`, `vercel`, ...).
|
|
68
|
+
*/
|
|
69
|
+
export interface SandboxProviderOptions {
|
|
70
|
+
[provider: string]: unknown;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Everything needed to (re)create and resume a sandbox. It is persisted in the
|
|
74
|
+
* store, so any process can resume a sandbox by ID alone.
|
|
75
|
+
*
|
|
76
|
+
* Note: `env` and `git.auth` are stored as-is; treat the store as sensitive.
|
|
77
|
+
*/
|
|
78
|
+
export interface SandboxSpec {
|
|
79
|
+
/** Directory commands run in. Defaults to the provider's workdir. */
|
|
80
|
+
workdir?: string;
|
|
81
|
+
/** Environment variables set for every command in the sandbox. */
|
|
82
|
+
env?: Record<string, string>;
|
|
83
|
+
/** Repository cloned into `workdir` before `setup` runs. */
|
|
84
|
+
git?: GitSource;
|
|
85
|
+
/** One-shot command(s) run once after creation; stops at the first non-zero exit. */
|
|
86
|
+
setup?: CommandInput | CommandInput[];
|
|
87
|
+
/** The long-running main process, launched detached after `setup`. */
|
|
88
|
+
start?: CommandInput;
|
|
89
|
+
/**
|
|
90
|
+
* Launched detached (as the new main process) whenever a stopped sandbox is
|
|
91
|
+
* woken back up. Defaults to `start`.
|
|
92
|
+
*/
|
|
93
|
+
resume?: CommandInput;
|
|
94
|
+
/** Ports to expose; see `SandboxManager.getUrl`. */
|
|
95
|
+
ports?: number[];
|
|
96
|
+
/** Maximum time a sandbox may run before it is stopped. */
|
|
97
|
+
timeoutMs?: number;
|
|
98
|
+
resources?: SandboxResources;
|
|
99
|
+
/**
|
|
100
|
+
* Outbound network access. Applies from creation, so `git` and `setup`
|
|
101
|
+
* run under it too. Default `"allow-all"`.
|
|
102
|
+
*/
|
|
103
|
+
network?: SandboxNetworkPolicy;
|
|
104
|
+
providerOptions?: SandboxProviderOptions;
|
|
105
|
+
}
|
|
106
|
+
export type SandboxStatus = "provisioning" | "setting_up" | "starting" | "running" | "stopping" | "stopped" | "resuming" | "failed" | "destroying" | "destroyed";
|
|
107
|
+
export declare const SANDBOX_STATUSES: readonly SandboxStatus[];
|
|
108
|
+
/** Statuses that are mid-operation; other callers wait for them to settle. */
|
|
109
|
+
export declare const TRANSITIONAL_STATUSES: readonly SandboxStatus[];
|
|
110
|
+
export interface SandboxRecord {
|
|
111
|
+
/** SDK-generated ID, e.g. `sbx-3f9c...`. */
|
|
112
|
+
id: string;
|
|
113
|
+
/** Name of the provider the sandbox lives on. */
|
|
114
|
+
provider: string;
|
|
115
|
+
/** The provider's own ID (Docker container ID, Vercel sandbox name). */
|
|
116
|
+
providerRef: string | null;
|
|
117
|
+
status: SandboxStatus;
|
|
118
|
+
spec: SandboxSpec;
|
|
119
|
+
/** Your user / tenant / project ID. Indexed for listing. */
|
|
120
|
+
ownerId: string | null;
|
|
121
|
+
/** String labels; `list({ labels })` matches records containing all given pairs. */
|
|
122
|
+
labels: Record<string, string>;
|
|
123
|
+
/** Free-form JSON metadata. Not queryable. */
|
|
124
|
+
metadata: Record<string, unknown>;
|
|
125
|
+
/** Last failure message, if any. */
|
|
126
|
+
error: string | null;
|
|
127
|
+
/** Incremented on every update; used for compare-and-set. */
|
|
128
|
+
version: number;
|
|
129
|
+
createdAt: Date;
|
|
130
|
+
updatedAt: Date;
|
|
131
|
+
/** When the sandbox should be stopped (from `spec.timeoutMs`). */
|
|
132
|
+
expiresAt: Date | null;
|
|
133
|
+
}
|
|
134
|
+
export interface ExecResult {
|
|
135
|
+
exitCode: number;
|
|
136
|
+
stdout: string;
|
|
137
|
+
stderr: string;
|
|
138
|
+
}
|
|
139
|
+
export interface ExecOptions {
|
|
140
|
+
onStdout?: (chunk: string) => void;
|
|
141
|
+
onStderr?: (chunk: string) => void;
|
|
142
|
+
signal?: AbortSignal;
|
|
143
|
+
timeoutMs?: number;
|
|
144
|
+
}
|
|
145
|
+
export interface SandboxFile {
|
|
146
|
+
/** Relative paths resolve against the sandbox workdir. */
|
|
147
|
+
path: string;
|
|
148
|
+
content: string | Uint8Array;
|
|
149
|
+
/** Unix permission bits, e.g. `0o755`. Defaults to `0o644`. */
|
|
150
|
+
mode?: number;
|
|
151
|
+
}
|
|
152
|
+
/** State of the sandbox's detached main process (the `start` / `resume` command). */
|
|
153
|
+
export interface MainProcessInfo {
|
|
154
|
+
pid: number | null;
|
|
155
|
+
running: boolean;
|
|
156
|
+
/** Exit code once the process has exited, otherwise `null`. */
|
|
157
|
+
exitCode: number | null;
|
|
158
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const SANDBOX_STATUSES = [
|
|
2
|
+
"provisioning",
|
|
3
|
+
"setting_up",
|
|
4
|
+
"starting",
|
|
5
|
+
"running",
|
|
6
|
+
"stopping",
|
|
7
|
+
"stopped",
|
|
8
|
+
"resuming",
|
|
9
|
+
"failed",
|
|
10
|
+
"destroying",
|
|
11
|
+
"destroyed",
|
|
12
|
+
];
|
|
13
|
+
/** Statuses that are mid-operation; other callers wait for them to settle. */
|
|
14
|
+
export const TRANSITIONAL_STATUSES = [
|
|
15
|
+
"provisioning",
|
|
16
|
+
"setting_up",
|
|
17
|
+
"starting",
|
|
18
|
+
"stopping",
|
|
19
|
+
"resuming",
|
|
20
|
+
"destroying",
|
|
21
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { VERCEL_DEFAULT_WORKDIR, VercelSandboxProvider, toVercelNetworkPolicy } from "./provider.js";
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Writable } from "node:stream";
|
|
2
|
+
import type { MainLogChunk, ProviderCreateInput, ProviderCreateResult, ProviderSandbox, ProviderSandboxStatus, ResolvedSandboxFile, SandboxProvider } from "../provider.js";
|
|
3
|
+
import type { ExecOptions, ExecResult, MainProcessInfo, NormalizedCommand, SandboxNetworkPolicy } from "../types.js";
|
|
4
|
+
/** Per-sandbox options, set as `spec.providerOptions.vercel`. */
|
|
5
|
+
export interface VercelSandboxOptions {
|
|
6
|
+
/** Vercel Container Registry image, e.g. `"vercel/sandbox/universal"`. */
|
|
7
|
+
image?: string;
|
|
8
|
+
/** Legacy managed runtime, e.g. `"node24"`. Ignored when `image` is set. */
|
|
9
|
+
runtime?: string;
|
|
10
|
+
/** Region, e.g. `"iad1"`. */
|
|
11
|
+
region?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Passed to `@vercel/sandbox` as-is, for what `spec.network` cannot express
|
|
14
|
+
* (per-domain request rules, header injection). Cannot be combined with `spec.network`.
|
|
15
|
+
*/
|
|
16
|
+
networkPolicy?: unknown;
|
|
17
|
+
/** Up to 5 key/value tags. */
|
|
18
|
+
tags?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
declare module "../types.js" {
|
|
21
|
+
interface SandboxProviderOptions {
|
|
22
|
+
vercel?: VercelSandboxOptions;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export interface VercelCredentials {
|
|
26
|
+
token: string;
|
|
27
|
+
teamId: string;
|
|
28
|
+
projectId: string;
|
|
29
|
+
}
|
|
30
|
+
export interface VercelRunCommandParams {
|
|
31
|
+
cmd: string;
|
|
32
|
+
args?: string[];
|
|
33
|
+
cwd?: string;
|
|
34
|
+
env?: Record<string, string>;
|
|
35
|
+
detached?: boolean;
|
|
36
|
+
stdout?: Writable;
|
|
37
|
+
stderr?: Writable;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
}
|
|
41
|
+
/** The subset of a `@vercel/sandbox` `Sandbox` instance this provider uses. */
|
|
42
|
+
export interface VercelSandboxInstance {
|
|
43
|
+
readonly name: string;
|
|
44
|
+
/** Status of the current session: `pending`, `running`, `stopping`, `stopped`, ... */
|
|
45
|
+
readonly status: string;
|
|
46
|
+
runCommand(params: VercelRunCommandParams): Promise<{
|
|
47
|
+
exitCode: number | null;
|
|
48
|
+
}>;
|
|
49
|
+
writeFiles(files: {
|
|
50
|
+
path: string;
|
|
51
|
+
content: Uint8Array;
|
|
52
|
+
mode?: number;
|
|
53
|
+
}[]): Promise<void>;
|
|
54
|
+
readFileToBuffer(file: {
|
|
55
|
+
path: string;
|
|
56
|
+
}): Promise<Uint8Array | null>;
|
|
57
|
+
domain(port: number): string;
|
|
58
|
+
stop(): Promise<unknown>;
|
|
59
|
+
delete(): Promise<unknown>;
|
|
60
|
+
extendTimeout(durationMs: number): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
/** The subset of the `@vercel/sandbox` `Sandbox` class this provider uses. */
|
|
63
|
+
export interface VercelSandboxApi {
|
|
64
|
+
create(params: Record<string, unknown>): Promise<VercelSandboxInstance>;
|
|
65
|
+
get(params: {
|
|
66
|
+
name: string;
|
|
67
|
+
resume?: boolean;
|
|
68
|
+
} & Record<string, unknown>): Promise<VercelSandboxInstance>;
|
|
69
|
+
}
|
|
70
|
+
export interface VercelSandboxProviderOptions {
|
|
71
|
+
/** Provider name used in sandbox records. Default `"vercel"`. */
|
|
72
|
+
name?: string;
|
|
73
|
+
/** Access-token credentials. Omit to use Vercel OIDC (`VERCEL_OIDC_TOKEN`). */
|
|
74
|
+
credentials?: VercelCredentials;
|
|
75
|
+
/** Sandbox API; defaults to `Sandbox` from `@vercel/sandbox`. Inject a fake in tests. */
|
|
76
|
+
api?: VercelSandboxApi;
|
|
77
|
+
/** Image used when the spec sets neither `image` nor `runtime`. */
|
|
78
|
+
defaultImage?: string;
|
|
79
|
+
defaultRegion?: string;
|
|
80
|
+
}
|
|
81
|
+
export declare const VERCEL_DEFAULT_WORKDIR = "/vercel/sandbox";
|
|
82
|
+
/**
|
|
83
|
+
* Runs each sandbox as a persistent Vercel Sandbox: `stop` snapshots the
|
|
84
|
+
* filesystem and `wake` restores it. `spec.timeoutMs` maps to the native
|
|
85
|
+
* sandbox timeout (Vercel stops the sandbox itself when it expires).
|
|
86
|
+
*/
|
|
87
|
+
export declare class VercelSandboxProvider implements SandboxProvider {
|
|
88
|
+
readonly name: string;
|
|
89
|
+
readonly defaultWorkdir = "/vercel/sandbox";
|
|
90
|
+
readonly networkPolicies: readonly ["deny-all", "allowlist"];
|
|
91
|
+
private readonly credentials;
|
|
92
|
+
private readonly defaultImage;
|
|
93
|
+
private readonly defaultRegion;
|
|
94
|
+
private apiPromise;
|
|
95
|
+
constructor(options?: VercelSandboxProviderOptions);
|
|
96
|
+
create(input: ProviderCreateInput): Promise<ProviderCreateResult>;
|
|
97
|
+
wake(sandbox: ProviderSandbox): Promise<void>;
|
|
98
|
+
stop(sandbox: ProviderSandbox): Promise<void>;
|
|
99
|
+
destroy(sandbox: ProviderSandbox): Promise<void>;
|
|
100
|
+
status(sandbox: ProviderSandbox): Promise<ProviderSandboxStatus>;
|
|
101
|
+
exec(sandbox: ProviderSandbox, command: NormalizedCommand, options?: ExecOptions): Promise<ExecResult>;
|
|
102
|
+
startMainProcess(sandbox: ProviderSandbox, command: NormalizedCommand): Promise<void>;
|
|
103
|
+
getMainProcess(sandbox: ProviderSandbox): Promise<MainProcessInfo | null>;
|
|
104
|
+
readMainLog(sandbox: ProviderSandbox, offset: number): Promise<MainLogChunk>;
|
|
105
|
+
writeFiles(sandbox: ProviderSandbox, files: ResolvedSandboxFile[]): Promise<void>;
|
|
106
|
+
readFile(sandbox: ProviderSandbox, path: string): Promise<Uint8Array | null>;
|
|
107
|
+
getUrl(sandbox: ProviderSandbox, port: number): Promise<string>;
|
|
108
|
+
extendTimeout(sandbox: ProviderSandbox, durationMs: number): Promise<void>;
|
|
109
|
+
private run;
|
|
110
|
+
private connect;
|
|
111
|
+
/** Connects without resuming, so a sandbox Vercel stopped (e.g. on timeout) is reported, not silently resumed. */
|
|
112
|
+
private connectRunning;
|
|
113
|
+
private api;
|
|
114
|
+
}
|
|
115
|
+
/** The `@vercel/sandbox` `NetworkPolicy` shapes `spec.network` maps onto. */
|
|
116
|
+
type VercelNetworkPolicy = "allow-all" | "deny-all" | {
|
|
117
|
+
allow: string[];
|
|
118
|
+
subnets?: {
|
|
119
|
+
allow?: string[];
|
|
120
|
+
deny?: string[];
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
/** Maps `spec.network` onto a `@vercel/sandbox` network policy. */
|
|
124
|
+
export declare function toVercelNetworkPolicy(policy: SandboxNetworkPolicy): VercelNetworkPolicy;
|
|
125
|
+
export {};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { ProviderSandboxMissingError, ProviderSandboxStoppedError, SandboxError } from "../errors.js";
|
|
3
|
+
import { outputCollector } from "../internal/output.js";
|
|
4
|
+
import { normalizeCidr } from "../network.js";
|
|
5
|
+
import { shellGetMainProcess, shellReadMainLog, shellStartMainProcess } from "../shell-main-process.js";
|
|
6
|
+
export const VERCEL_DEFAULT_WORKDIR = "/vercel/sandbox";
|
|
7
|
+
const RUNNING_STATUSES = new Set(["pending", "running"]);
|
|
8
|
+
/**
|
|
9
|
+
* Runs each sandbox as a persistent Vercel Sandbox: `stop` snapshots the
|
|
10
|
+
* filesystem and `wake` restores it. `spec.timeoutMs` maps to the native
|
|
11
|
+
* sandbox timeout (Vercel stops the sandbox itself when it expires).
|
|
12
|
+
*/
|
|
13
|
+
export class VercelSandboxProvider {
|
|
14
|
+
name;
|
|
15
|
+
defaultWorkdir = VERCEL_DEFAULT_WORKDIR;
|
|
16
|
+
networkPolicies = ["deny-all", "allowlist"];
|
|
17
|
+
credentials;
|
|
18
|
+
defaultImage;
|
|
19
|
+
defaultRegion;
|
|
20
|
+
apiPromise;
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.name = options.name ?? "vercel";
|
|
23
|
+
this.credentials = options.credentials ?? {};
|
|
24
|
+
this.defaultImage = options.defaultImage;
|
|
25
|
+
this.defaultRegion = options.defaultRegion;
|
|
26
|
+
if (options.api)
|
|
27
|
+
this.apiPromise = Promise.resolve(options.api);
|
|
28
|
+
}
|
|
29
|
+
async create(input) {
|
|
30
|
+
const options = input.spec.providerOptions?.vercel ?? {};
|
|
31
|
+
if (input.spec.network !== undefined && options.networkPolicy !== undefined) {
|
|
32
|
+
throw new SandboxError("Set spec.network or spec.providerOptions.vercel.networkPolicy, not both");
|
|
33
|
+
}
|
|
34
|
+
const image = options.image ?? (options.runtime ? undefined : this.defaultImage);
|
|
35
|
+
const api = await this.api();
|
|
36
|
+
const sandbox = await api.create({
|
|
37
|
+
...this.credentials,
|
|
38
|
+
name: input.id,
|
|
39
|
+
persistent: true,
|
|
40
|
+
...(image ? { image } : options.runtime ? { runtime: options.runtime } : {}),
|
|
41
|
+
ports: input.spec.ports,
|
|
42
|
+
timeout: input.spec.timeoutMs,
|
|
43
|
+
resources: input.spec.resources?.vcpus ? { vcpus: input.spec.resources.vcpus } : undefined,
|
|
44
|
+
env: input.spec.env,
|
|
45
|
+
tags: options.tags,
|
|
46
|
+
region: options.region ?? this.defaultRegion,
|
|
47
|
+
networkPolicy: input.spec.network === undefined ? options.networkPolicy : toVercelNetworkPolicy(input.spec.network),
|
|
48
|
+
});
|
|
49
|
+
if (input.workdir !== VERCEL_DEFAULT_WORKDIR) {
|
|
50
|
+
const result = await sandbox.runCommand({ cmd: "mkdir", args: ["-p", input.workdir] });
|
|
51
|
+
if (result.exitCode !== 0) {
|
|
52
|
+
await sandbox.delete().catch(() => undefined);
|
|
53
|
+
throw new SandboxError(`Failed to create workdir ${input.workdir}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { providerRef: sandbox.name };
|
|
57
|
+
}
|
|
58
|
+
async wake(sandbox) {
|
|
59
|
+
await this.connect(sandbox, { resume: true });
|
|
60
|
+
}
|
|
61
|
+
async stop(sandbox) {
|
|
62
|
+
const instance = await this.connect(sandbox);
|
|
63
|
+
if (RUNNING_STATUSES.has(instance.status))
|
|
64
|
+
await instance.stop();
|
|
65
|
+
}
|
|
66
|
+
async destroy(sandbox) {
|
|
67
|
+
try {
|
|
68
|
+
await (await this.connect(sandbox)).delete();
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (!(error instanceof ProviderSandboxMissingError) && !isNotFound(error))
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async status(sandbox) {
|
|
76
|
+
try {
|
|
77
|
+
const instance = await this.connect(sandbox);
|
|
78
|
+
return RUNNING_STATUSES.has(instance.status) ? "running" : "stopped";
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof ProviderSandboxMissingError)
|
|
82
|
+
return "missing";
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async exec(sandbox, command, options = {}) {
|
|
87
|
+
const instance = await this.connectRunning(sandbox);
|
|
88
|
+
return this.run(instance, command, options);
|
|
89
|
+
}
|
|
90
|
+
async startMainProcess(sandbox, command) {
|
|
91
|
+
const instance = await this.connectRunning(sandbox);
|
|
92
|
+
await shellStartMainProcess((cmd) => this.run(instance, cmd), async (launcher) => {
|
|
93
|
+
await instance.runCommand({ cmd: launcher.cmd, args: launcher.args, cwd: launcher.cwd, detached: true });
|
|
94
|
+
}, command);
|
|
95
|
+
}
|
|
96
|
+
async getMainProcess(sandbox) {
|
|
97
|
+
const instance = await this.connectRunning(sandbox);
|
|
98
|
+
return shellGetMainProcess((cmd) => this.run(instance, cmd));
|
|
99
|
+
}
|
|
100
|
+
async readMainLog(sandbox, offset) {
|
|
101
|
+
const instance = await this.connectRunning(sandbox);
|
|
102
|
+
return shellReadMainLog((cmd) => this.run(instance, cmd), offset);
|
|
103
|
+
}
|
|
104
|
+
async writeFiles(sandbox, files) {
|
|
105
|
+
const instance = await this.connectRunning(sandbox);
|
|
106
|
+
const dirs = [...new Set(files.map((file) => posix.dirname(file.path)))];
|
|
107
|
+
const mkdir = await instance.runCommand({ cmd: "mkdir", args: ["-p", ...dirs] });
|
|
108
|
+
if (mkdir.exitCode !== 0)
|
|
109
|
+
throw new SandboxError(`Failed to create directories ${dirs.join(", ")}`);
|
|
110
|
+
await instance.writeFiles(files.map((file) => ({ path: file.path, content: file.content, mode: file.mode })));
|
|
111
|
+
}
|
|
112
|
+
async readFile(sandbox, path) {
|
|
113
|
+
const instance = await this.connectRunning(sandbox);
|
|
114
|
+
const buffer = await instance.readFileToBuffer({ path });
|
|
115
|
+
return buffer === null ? null : new Uint8Array(buffer);
|
|
116
|
+
}
|
|
117
|
+
async getUrl(sandbox, port) {
|
|
118
|
+
const instance = await this.connectRunning(sandbox);
|
|
119
|
+
return instance.domain(port);
|
|
120
|
+
}
|
|
121
|
+
async extendTimeout(sandbox, durationMs) {
|
|
122
|
+
const instance = await this.connectRunning(sandbox);
|
|
123
|
+
await instance.extendTimeout(durationMs);
|
|
124
|
+
}
|
|
125
|
+
async run(instance, command, options = {}) {
|
|
126
|
+
const stdout = outputCollector(options.onStdout);
|
|
127
|
+
const stderr = outputCollector(options.onStderr);
|
|
128
|
+
const result = await instance.runCommand({
|
|
129
|
+
cmd: command.cmd,
|
|
130
|
+
args: command.args,
|
|
131
|
+
cwd: command.cwd,
|
|
132
|
+
env: command.env,
|
|
133
|
+
stdout: stdout.writable,
|
|
134
|
+
stderr: stderr.writable,
|
|
135
|
+
signal: options.signal,
|
|
136
|
+
timeoutMs: options.timeoutMs,
|
|
137
|
+
});
|
|
138
|
+
return { exitCode: result.exitCode ?? -1, stdout: stdout.finish(), stderr: stderr.finish() };
|
|
139
|
+
}
|
|
140
|
+
async connect(sandbox, { resume = false } = {}) {
|
|
141
|
+
const api = await this.api();
|
|
142
|
+
try {
|
|
143
|
+
return await api.get({ ...this.credentials, name: sandbox.providerRef, resume });
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (isNotFound(error))
|
|
147
|
+
throw new ProviderSandboxMissingError(sandbox.providerRef);
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Connects without resuming, so a sandbox Vercel stopped (e.g. on timeout) is reported, not silently resumed. */
|
|
152
|
+
async connectRunning(sandbox) {
|
|
153
|
+
const instance = await this.connect(sandbox);
|
|
154
|
+
if (!RUNNING_STATUSES.has(instance.status))
|
|
155
|
+
throw new ProviderSandboxStoppedError(sandbox.providerRef);
|
|
156
|
+
return instance;
|
|
157
|
+
}
|
|
158
|
+
api() {
|
|
159
|
+
this.apiPromise ??= import("@vercel/sandbox").then(({ Sandbox }) => ({
|
|
160
|
+
create: (params) => Sandbox.create(params),
|
|
161
|
+
get: (params) => Sandbox.get(params),
|
|
162
|
+
}));
|
|
163
|
+
return this.apiPromise;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Maps `spec.network` onto a `@vercel/sandbox` network policy. */
|
|
167
|
+
export function toVercelNetworkPolicy(policy) {
|
|
168
|
+
if (typeof policy === "string")
|
|
169
|
+
return policy;
|
|
170
|
+
const allow = policy.allowDomains ?? [];
|
|
171
|
+
const allowCidrs = (policy.allowCidrs ?? []).map(normalizeCidr);
|
|
172
|
+
const denyCidrs = (policy.denyCidrs ?? []).map(normalizeCidr);
|
|
173
|
+
if (allow.length === 0 && allowCidrs.length === 0)
|
|
174
|
+
return "deny-all";
|
|
175
|
+
if (allowCidrs.length === 0 && denyCidrs.length === 0)
|
|
176
|
+
return { allow };
|
|
177
|
+
return {
|
|
178
|
+
allow,
|
|
179
|
+
subnets: {
|
|
180
|
+
...(allowCidrs.length > 0 && { allow: allowCidrs }),
|
|
181
|
+
...(denyCidrs.length > 0 && { deny: denyCidrs }),
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function isNotFound(error) {
|
|
186
|
+
if (typeof error !== "object" || error === null)
|
|
187
|
+
return false;
|
|
188
|
+
const response = error.response;
|
|
189
|
+
return response?.status === 404;
|
|
190
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DEFAULT_WEB_STORAGE_PREFIX, WebStorageSandboxStore } from "./store.js";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { SandboxListFilter, SandboxRecordPatch, SandboxStore } from "../store.js";
|
|
2
|
+
import type { SandboxRecord } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The part of the Web Storage API the store uses. `localStorage` and
|
|
5
|
+
* `sessionStorage` implement it, and so does any object with these members.
|
|
6
|
+
*/
|
|
7
|
+
export interface WebStorageLike {
|
|
8
|
+
readonly length: number;
|
|
9
|
+
key(index: number): string | null;
|
|
10
|
+
getItem(key: string): string | null;
|
|
11
|
+
setItem(key: string, value: string): void;
|
|
12
|
+
removeItem(key: string): void;
|
|
13
|
+
}
|
|
14
|
+
export interface WebStorageSandboxStoreOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Where records are kept. Default: `globalThis.localStorage`. Pass
|
|
17
|
+
* `sessionStorage` to keep them for the life of the browser tab.
|
|
18
|
+
*/
|
|
19
|
+
storage?: WebStorageLike;
|
|
20
|
+
/**
|
|
21
|
+
* Prefix of every key the store writes; each record is one entry under
|
|
22
|
+
* `prefix + id`. Give stores that share a storage different prefixes.
|
|
23
|
+
* Default: `"sandbox-sdk:sandboxes:"`.
|
|
24
|
+
*/
|
|
25
|
+
prefix?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare const DEFAULT_WEB_STORAGE_PREFIX = "sandbox-sdk:sandboxes:";
|
|
28
|
+
/**
|
|
29
|
+
* `SandboxStore` backed by the Web Storage API (`localStorage` or
|
|
30
|
+
* `sessionStorage`), for running the SDK in a browser with no database.
|
|
31
|
+
*
|
|
32
|
+
* Every method reads and writes storage synchronously, so `update` is an
|
|
33
|
+
* atomic compare-and-set within one JavaScript context. Browsers do not make
|
|
34
|
+
* that guarantee across tabs that share a `localStorage`; give each tab its
|
|
35
|
+
* own `prefix` or `ownerId`, or use `sessionStorage`, if they write the same
|
|
36
|
+
* records. Storage quotas are small (around 5 MB per origin), and a write
|
|
37
|
+
* past the quota throws the browser's `QuotaExceededError`.
|
|
38
|
+
*/
|
|
39
|
+
export declare class WebStorageSandboxStore implements SandboxStore {
|
|
40
|
+
private readonly storage;
|
|
41
|
+
private readonly prefix;
|
|
42
|
+
constructor(options?: WebStorageSandboxStoreOptions);
|
|
43
|
+
insert(record: SandboxRecord): Promise<void>;
|
|
44
|
+
get(id: string): Promise<SandboxRecord | null>;
|
|
45
|
+
list(filter?: SandboxListFilter): Promise<SandboxRecord[]>;
|
|
46
|
+
update(id: string, expectedVersion: number, patch: SandboxRecordPatch): Promise<SandboxRecord | null>;
|
|
47
|
+
delete(id: string): Promise<boolean>;
|
|
48
|
+
/** Every key under the prefix, collected first so writes cannot shift the indexes. */
|
|
49
|
+
private keys;
|
|
50
|
+
private write;
|
|
51
|
+
/** `null` if the entry is missing or is not a sandbox record. */
|
|
52
|
+
private read;
|
|
53
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { SandboxError } from "../errors.js";
|
|
2
|
+
import { matchesFilter } from "../store.js";
|
|
3
|
+
export const DEFAULT_WEB_STORAGE_PREFIX = "sandbox-sdk:sandboxes:";
|
|
4
|
+
/**
|
|
5
|
+
* `SandboxStore` backed by the Web Storage API (`localStorage` or
|
|
6
|
+
* `sessionStorage`), for running the SDK in a browser with no database.
|
|
7
|
+
*
|
|
8
|
+
* Every method reads and writes storage synchronously, so `update` is an
|
|
9
|
+
* atomic compare-and-set within one JavaScript context. Browsers do not make
|
|
10
|
+
* that guarantee across tabs that share a `localStorage`; give each tab its
|
|
11
|
+
* own `prefix` or `ownerId`, or use `sessionStorage`, if they write the same
|
|
12
|
+
* records. Storage quotas are small (around 5 MB per origin), and a write
|
|
13
|
+
* past the quota throws the browser's `QuotaExceededError`.
|
|
14
|
+
*/
|
|
15
|
+
export class WebStorageSandboxStore {
|
|
16
|
+
storage;
|
|
17
|
+
prefix;
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
const storage = options.storage ?? globalThis.localStorage;
|
|
20
|
+
if (!storage) {
|
|
21
|
+
throw new SandboxError("No Web Storage is available here; pass `storage` (e.g. sessionStorage)");
|
|
22
|
+
}
|
|
23
|
+
this.storage = storage;
|
|
24
|
+
this.prefix = options.prefix ?? DEFAULT_WEB_STORAGE_PREFIX;
|
|
25
|
+
}
|
|
26
|
+
async insert(record) {
|
|
27
|
+
const key = this.prefix + record.id;
|
|
28
|
+
if (this.storage.getItem(key) !== null) {
|
|
29
|
+
throw new SandboxError(`Sandbox record "${record.id}" already exists`);
|
|
30
|
+
}
|
|
31
|
+
this.write(record);
|
|
32
|
+
}
|
|
33
|
+
async get(id) {
|
|
34
|
+
return this.read(this.prefix + id);
|
|
35
|
+
}
|
|
36
|
+
async list(filter = {}) {
|
|
37
|
+
const records = [];
|
|
38
|
+
for (const key of this.keys()) {
|
|
39
|
+
const record = this.read(key);
|
|
40
|
+
if (record && matchesFilter(record, filter))
|
|
41
|
+
records.push(record);
|
|
42
|
+
}
|
|
43
|
+
records.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime() || (a.id < b.id ? 1 : a.id > b.id ? -1 : 0));
|
|
44
|
+
const offset = filter.offset ?? 0;
|
|
45
|
+
const end = filter.limit === undefined ? undefined : offset + filter.limit;
|
|
46
|
+
return records.slice(offset, end);
|
|
47
|
+
}
|
|
48
|
+
async update(id, expectedVersion, patch) {
|
|
49
|
+
// No await between the read and the write: nothing else in this
|
|
50
|
+
// JavaScript context can write the record in between.
|
|
51
|
+
const current = this.read(this.prefix + id);
|
|
52
|
+
if (!current || current.version !== expectedVersion)
|
|
53
|
+
return null;
|
|
54
|
+
const defined = Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== undefined));
|
|
55
|
+
const next = {
|
|
56
|
+
...current,
|
|
57
|
+
...structuredClone(defined),
|
|
58
|
+
version: current.version + 1,
|
|
59
|
+
updatedAt: new Date(),
|
|
60
|
+
};
|
|
61
|
+
this.write(next);
|
|
62
|
+
return next;
|
|
63
|
+
}
|
|
64
|
+
async delete(id) {
|
|
65
|
+
const key = this.prefix + id;
|
|
66
|
+
if (this.storage.getItem(key) === null)
|
|
67
|
+
return false;
|
|
68
|
+
this.storage.removeItem(key);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
/** Every key under the prefix, collected first so writes cannot shift the indexes. */
|
|
72
|
+
keys() {
|
|
73
|
+
const keys = [];
|
|
74
|
+
for (let i = 0; i < this.storage.length; i += 1) {
|
|
75
|
+
const key = this.storage.key(i);
|
|
76
|
+
if (key !== null && key.startsWith(this.prefix))
|
|
77
|
+
keys.push(key);
|
|
78
|
+
}
|
|
79
|
+
return keys;
|
|
80
|
+
}
|
|
81
|
+
write(record) {
|
|
82
|
+
this.storage.setItem(this.prefix + record.id, JSON.stringify(record));
|
|
83
|
+
}
|
|
84
|
+
/** `null` if the entry is missing or is not a sandbox record. */
|
|
85
|
+
read(key) {
|
|
86
|
+
const raw = this.storage.getItem(key);
|
|
87
|
+
if (raw === null)
|
|
88
|
+
return null;
|
|
89
|
+
let stored;
|
|
90
|
+
try {
|
|
91
|
+
stored = JSON.parse(raw);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (!isStoredRecord(stored))
|
|
97
|
+
return null;
|
|
98
|
+
return {
|
|
99
|
+
...stored,
|
|
100
|
+
createdAt: new Date(stored.createdAt),
|
|
101
|
+
updatedAt: new Date(stored.updatedAt),
|
|
102
|
+
expiresAt: stored.expiresAt === null ? null : new Date(stored.expiresAt),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function isStoredRecord(value) {
|
|
107
|
+
if (typeof value !== "object" || value === null)
|
|
108
|
+
return false;
|
|
109
|
+
const record = value;
|
|
110
|
+
return (typeof record.id === "string" &&
|
|
111
|
+
typeof record.provider === "string" &&
|
|
112
|
+
typeof record.status === "string" &&
|
|
113
|
+
typeof record.version === "number" &&
|
|
114
|
+
typeof record.createdAt === "string" &&
|
|
115
|
+
typeof record.updatedAt === "string" &&
|
|
116
|
+
typeof record.spec === "object" &&
|
|
117
|
+
record.spec !== null);
|
|
118
|
+
}
|