@sandboxrouter/sdk 0.3.2 → 0.3.4

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/dist/server.js CHANGED
@@ -31,11 +31,11 @@ import { ProviderError, RouterError } from "./types.js";
31
31
  */
32
32
  export function createApp(router = new Router(), token = process.env.SANDBOXROUTER_TOKEN,
33
33
  /** Browser origins allowed to call this server (e.g. the website's Playground). Off by default. */
34
- corsOrigins = (process.env.SANDBOXROUTER_CORS ?? "").split(",").map((s) => s.trim()).filter(Boolean)) {
34
+ corsOrigins = (process.env.SANDBOXROUTER_CORS ?? "").split(",").map((s) => s.trim()).filter(Boolean), hooks = {}) {
35
35
  // With CORS on, any page open in a browser on this machine can call the server (Chrome even
36
36
  // lets a public page reach localhost, which the Private-Network header below allows), so it
37
37
  // would start sandboxes on these provider keys. A token is required for that.
38
- if (corsOrigins.length && !token) {
38
+ if (corsOrigins.length && !token && !hooks.authorize) {
39
39
  throw new Error("CORS is on but SANDBOXROUTER_TOKEN is not set: browser pages could start sandboxes on your keys. Set a token, or turn CORS off.");
40
40
  }
41
41
  const expected = token ? Buffer.from(`Bearer ${token}`) : undefined;
@@ -71,11 +71,23 @@ corsOrigins = (process.env.SANDBOXROUTER_CORS ?? "").split(",").map((s) => s.tri
71
71
  }
72
72
  if (url.pathname === "/health")
73
73
  return json(res, 200, { ok: true });
74
- if (!authorized(req))
74
+ if (hooks.extra && (await hooks.extra(req, res, url)))
75
+ return;
76
+ let caller = null;
77
+ if (hooks.authorize) {
78
+ caller = await hooks.authorize(req);
79
+ if (caller === null)
80
+ return json(res, 401, { error: "unauthorized" });
81
+ }
82
+ else if (!authorized(req))
75
83
  return json(res, 401, { error: "unauthorized" });
76
84
  if (parts[0] !== "v1")
77
85
  return json(res, 404, { error: "not found" });
78
- const box = async (id) => live.get(id) ?? router.get(id);
86
+ const box = async (id) => {
87
+ if (hooks.owns && !(await hooks.owns(caller, id)))
88
+ throw new HttpError(404, "no such sandbox");
89
+ return live.get(id) ?? router.get(id);
90
+ };
79
91
  // GET /v1/providers
80
92
  if (req.method === "GET" && parts[1] === "providers" && parts.length === 2) {
81
93
  return json(res, 200, {
@@ -91,14 +103,25 @@ corsOrigins = (process.env.SANDBOXROUTER_CORS ?? "").split(",").map((s) => s.tri
91
103
  const { spec, route } = splitBody(await readJson(req));
92
104
  return json(res, 200, { candidates: router.plan(spec, route) });
93
105
  }
94
- if (parts[1] === "computers")
106
+ if (parts[1] === "computers") {
107
+ if (hooks.computers === false)
108
+ return json(res, 404, { error: "computers aren't available on this server" });
95
109
  return computers(req, res, url, parts);
110
+ }
96
111
  if (parts[1] !== "sandboxes")
97
112
  return json(res, 404, { error: "not found" });
98
113
  // POST /v1/sandboxes
99
114
  if (req.method === "POST" && parts.length === 2) {
100
115
  const { spec, route } = splitBody(await readJson(req));
116
+ await hooks.beforeCreate?.(caller, spec, route);
101
117
  const sbx = await router.create(spec, route);
118
+ try {
119
+ await hooks.afterCreate?.(caller, sbx, spec);
120
+ }
121
+ catch (err) {
122
+ await sbx.kill().catch(() => { });
123
+ throw err;
124
+ }
102
125
  live.set(sbx.id, sbx);
103
126
  return json(res, 201, {
104
127
  id: sbx.id,
@@ -132,6 +155,8 @@ corsOrigins = (process.env.SANDBOXROUTER_CORS ?? "").split(",").map((s) => s.tri
132
155
  }
133
156
  // POST /v1/sandboxes/:id/pause
134
157
  if (req.method === "POST" && parts[3] === "pause" && parts.length === 4) {
158
+ if (hooks.pause === false)
159
+ return json(res, 400, { error: "pause isn't available on this server" });
135
160
  await (await box(id)).pause();
136
161
  // Drop the cached handle so the next call reattaches through router.get(), which resumes.
137
162
  live.delete(id);
@@ -156,6 +181,7 @@ corsOrigins = (process.env.SANDBOXROUTER_CORS ?? "").split(",").map((s) => s.tri
156
181
  if (req.method === "DELETE" && parts.length === 3) {
157
182
  await (await box(id)).kill();
158
183
  live.delete(id);
184
+ await hooks.afterKill?.(caller, id);
159
185
  return json(res, 200, { id, killed: true });
160
186
  }
161
187
  return json(res, 404, { error: "not found" });
@@ -227,7 +253,7 @@ corsOrigins = (process.env.SANDBOXROUTER_CORS ?? "").split(",").map((s) => s.tri
227
253
  });
228
254
  });
229
255
  }
230
- class HttpError extends Error {
256
+ export class HttpError extends Error {
231
257
  status;
232
258
  constructor(status, message) {
233
259
  super(message);
package/dist/types.d.ts CHANGED
@@ -22,20 +22,24 @@ export interface SandboxSpec {
22
22
  metadata?: Record<string, string>;
23
23
  }
24
24
  /**
25
- * "cheapest" (and "default", the same thing): lowest list price for the spec, ties alphabetical.
25
+ * "verified" (and "default", the same thing): providers that passed the real-account check first
26
+ * (as of this release; `VERIFIED` in src/verified.ts), then the rest; cheapest first within each,
27
+ * ties alphabetical.
28
+ * "cheapest": lowest list price for the spec, tested or not, ties alphabetical.
26
29
  * "fastest": lowest median cold start you measured with `bench`, then cheapest.
27
30
  */
28
- export type Strategy = "default" | "cheapest" | "fastest";
31
+ export type Strategy = "default" | "verified" | "cheapest" | "fastest";
29
32
  export interface RouteOptions {
30
33
  /** Pin to one provider, or give an ordered list to try. Omit to let the router choose. */
31
34
  provider?: ProviderId | ProviderId[];
32
- /** How to order candidates when no explicit list is given. Default: cheapest. */
35
+ /** How to order candidates when no explicit list is given. Default: "verified" (tested providers first, then cheapest). */
33
36
  strategy?: Strategy;
34
37
  /**
35
38
  * What to do when a create fails. `true` (default): try the next candidate when the provider
36
- * looks unavailable (timeout, network error, 429, 5xx, or an error without a status), but stop on
37
- * a setup error (400, 401, 402, 403, 404, 422: a bad key, no billing, a bad image or request), so
38
- * a mistake isn't hidden by quietly moving the work to another, maybe pricier, provider.
39
+ * looks unavailable (timeout, network error, 429, 5xx, or an error without a status) or can't bill
40
+ * (402: no balance, plan needed), but stop on a setup error (400, 401, 403, 404, 422: a bad key, a
41
+ * bad image or request), so a mistake isn't hidden by quietly moving the work to another, maybe
42
+ * pricier, provider. Every failed attempt is on `sandbox.attempts`.
39
43
  * `"any"`: try the next candidate on every error. `false`: only the first candidate.
40
44
  */
41
45
  fallback?: boolean | "any";
@@ -0,0 +1,2 @@
1
+ import type { ProviderId } from "./types.js";
2
+ export declare const VERIFIED: Partial<Record<ProviderId, string>>;
@@ -0,0 +1,7 @@
1
+ export const VERIFIED = {
2
+ blaxel: "2026-11-10",
3
+ daytona: "2026-11-10",
4
+ e2b: "2026-11-10",
5
+ maritime: "2026-11-10",
6
+ sail: "2026-11-10",
7
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sandboxrouter/sdk",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "One API for every sandbox provider: route AI agents' code by price, speed or capability, with automatic fallback. Free and open source.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",