the universal sandbox runtime for agents and humans. pocketenv.io
sandbox openclaw agent claude-code vercel-sandbox deno-sandbox cloudflare-sandbox atproto sprites daytona
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor sandbox app into modular routes and libs

Extract auth middleware, sandbox routes, and a sandbox-db helper.
Add unit tests for sandbox types, providers, helpers, and pull/push.
Add a "test" task to deno.json to run Deno tests

+890 -599
+2 -1
apps/sandbox/deno.json
··· 1 1 { 2 2 "tasks": { 3 - "dev": "deno run --watch src/index.ts" 3 + "dev": "deno run --watch src/index.ts", 4 + "test": "deno test --allow-env --allow-read src/**/*.test.ts" 4 5 }, 5 6 "imports": { 6 7 "@daytonaio/sdk": "npm:@daytonaio/sdk@^0.141.0",
+6 -598
apps/sandbox/src/index.ts
··· 2 2 import { Context } from "./context.ts"; 3 3 import { logger } from "hono/logger"; 4 4 import { consola } from "consola"; 5 - import { 6 - files, 7 - sandboxes, 8 - sandboxFiles, 9 - sandboxSecrets, 10 - sandboxVariables, 11 - sandboxVolumes, 12 - secrets, 13 - sshKeys, 14 - tailscaleAuthKeys, 15 - users, 16 - variables, 17 - spriteAuth, 18 - denoAuth, 19 - vercelAuth, 20 - sandboxCp, 21 - } from "./schema/mod.ts"; 22 - import { 23 - adjectives, 24 - nouns, 25 - generateUniqueAsync, 26 - } from "unique-username-generator"; 27 - import { eq, ExtractTablesWithRelations, or } from "drizzle-orm"; 28 - import { getConnection } from "./drizzle.ts"; 29 - import { 30 - SandboxConfig, 31 - SandboxConfigSchema, 32 - StartSandboxInput, 33 - StartSandboxInputSchema, 34 - } from "./types/sandbox.ts"; 35 - import { 36 - BaseSandbox, 37 - createSandbox, 38 - getSandboxById, 39 - Provider, 40 - } from "./providers/mod.ts"; 41 - import { SelectSandbox } from "./schema/sandboxes.ts"; 42 - import { PgTransaction } from "drizzle-orm/pg-core"; 43 - import { NodePgQueryResultHKT } from "drizzle-orm/node-postgres"; 44 5 import chalk from "chalk"; 45 - import process from "node:process"; 46 - import jwt from "@tsndr/cloudflare-worker-jwt"; 47 - import decrypt from "./lib/decrypt.ts"; 48 - import { InsertSpriteAuth } from "./schema/sprite-auth.ts"; 49 - import daytonaAuth, { InsertDaytonaAuth } from "./schema/daytona-auth.ts"; 50 - import { InsertDenoAuth } from "./schema/deno-auth.ts"; 51 - import crypto from "node:crypto"; 52 - import { PullDirectoryParams, pullSchema } from "./types/pull.ts"; 53 - import { PushDirectoryParams, pushSchema } from "./types/push.ts"; 54 - import { 55 - getAuthParams, 56 - buildCredentials, 57 - resolveSandboxInstance, 58 - } from "./lib/sandbox-helpers.ts"; 6 + import { authMiddleware } from "./middleware/auth.ts"; 7 + import { sandboxRouter } from "./routes/sandboxes.ts"; 59 8 60 - const SUPPORTED_PROVIDERS = ["daytona", "vercel", "deno", "sprites"]; 9 + export { getSandbox, saveSecrets, saveVariables } from "./lib/sandbox-db.ts"; 61 10 62 11 const app = new Hono<{ Variables: Context }>(); 63 12 64 - app.use("*", async (c, next) => { 65 - c.set("db", getConnection()); 66 - const token = c.req.header("Authorization")?.split(" ")[1]?.trim(); 67 - if (token) { 68 - try { 69 - const decoded = await jwt.verify(token, process.env.JWT_SECRET!); 70 - c.set("did", decoded?.payload.sub); 71 - } catch (err) { 72 - consola.error("JWT verification failed:", err); 73 - return c.json({ error: "Unauthorized" }, 401); 74 - } 75 - } else { 76 - if (c.req.path === "/") { 77 - await next(); 78 - return; 79 - } 80 - return c.json({ error: "Unauthorized" }, 401); 81 - } 82 - await next(); 83 - }); 84 - 85 - interface CmdOutput { 86 - success: boolean; 87 - stdout: string; 88 - stderr: string; 89 - } 90 - const getOutput = (res: CmdOutput) => (res.success ? res.stdout : res.stderr); 91 - 13 + app.use("*", authMiddleware); 92 14 app.use(logger()); 93 15 94 - app.get("/", async (c) => { 16 + app.get("/", (c) => { 95 17 return c.text(` 96 18 _____ ____ 97 19 / ___/____ _____ ____/ / /_ ____ _ __ ··· 102 24 `); 103 25 }); 104 26 105 - app.post("/v1/sandboxes", async (c) => { 106 - const body = await c.req.json<SandboxConfig>(); 107 - 108 - let suffix = Math.random().toString(36).substring(2, 6); 109 - let name = await generateUniqueAsync( 110 - { dictionaries: [adjectives, nouns], separator: "-" }, 111 - () => false, 112 - ); 113 - let spriteName = await generateUniqueAsync( 114 - { dictionaries: [adjectives, nouns], separator: "-" }, 115 - () => false, 116 - ); 117 - spriteName = `${spriteName}-${suffix}`; 118 - 119 - try { 120 - const params = SandboxConfigSchema.parse(body); 121 - name = params.name || `${name}-${suffix}`; 122 - do { 123 - const existing = await c.var.db 124 - .select() 125 - .from(sandboxes) 126 - .where(eq(sandboxes.name, name)) 127 - .execute(); 128 - if (existing.length === 0) { 129 - break; 130 - } 131 - 132 - name = await generateUniqueAsync( 133 - { dictionaries: [adjectives, nouns], separator: "-" }, 134 - () => false, 135 - ); 136 - suffix = Math.random().toString(36).substring(2, 6); 137 - name = `${name}-${suffix}`; 138 - } while (true); 139 - 140 - const record = await c.var.db.transaction(async (tx) => { 141 - const user = await tx 142 - .select() 143 - .from(users) 144 - .where(eq(users.did, c.var.did || "")) 145 - .execute() 146 - .then((res) => res[0]); 147 - let [record] = await tx 148 - .insert(sandboxes) 149 - .values({ 150 - base: params.base, 151 - name, 152 - provider: params.provider, 153 - publicKey: process.env.PUBLIC_KEY!, 154 - userId: user?.id, 155 - instanceType: "standard-1", 156 - keepAlive: params.keepAlive, 157 - sleepAfter: params.sleepAfter, 158 - status: "INITIALIZING", 159 - }) 160 - .returning() 161 - .execute(); 162 - 163 - if (params.secrets.length > 0) { 164 - await saveSecrets(tx, record, { secrets: params.secrets }); 165 - } 166 - 167 - if (params.variables.length > 0) { 168 - await saveVariables(tx, record, { variables: params.variables }); 169 - } 170 - 171 - if (params.spriteToken && user?.id) { 172 - await tx 173 - .insert(spriteAuth) 174 - .values({ 175 - sandboxId: record.id, 176 - spriteToken: params.spriteToken, 177 - redactedSpriteToken: params.redactedSpriteToken ?? "", 178 - userId: user.id, 179 - } satisfies InsertSpriteAuth) 180 - .execute(); 181 - } 182 - 183 - if (params.daytonaApiKey && user?.id) { 184 - await tx 185 - .insert(daytonaAuth) 186 - .values({ 187 - sandboxId: record.id, 188 - apiKey: params.daytonaApiKey, 189 - redactedApiKey: params.redactedDaytonaApiKey ?? "", 190 - userId: user.id, 191 - organizationId: params.daytonaOrganizationId!, 192 - } satisfies InsertDaytonaAuth) 193 - .execute(); 194 - } 195 - 196 - if (params.denoDeployToken && user?.id) { 197 - await tx 198 - .insert(denoAuth) 199 - .values({ 200 - sandboxId: record.id, 201 - deployToken: params.denoDeployToken, 202 - redactedDenoToken: params.redactedDenoDeployToken ?? "", 203 - userId: user.id, 204 - } satisfies InsertDenoAuth) 205 - .execute(); 206 - } 207 - 208 - if (params.vercelApiToken && user?.id) { 209 - await tx 210 - .insert(vercelAuth) 211 - .values({ 212 - sandboxId: record.id, 213 - vercelToken: params.vercelApiToken, 214 - redactedVercelToken: params.redactedVercelApiToken ?? "", 215 - userId: user.id, 216 - projectId: params.vercelProjectId!, 217 - teamId: params.vercelTeamId!, 218 - }) 219 - .execute(); 220 - } 221 - 222 - const sandbox = await createSandbox(params.provider, { 223 - id: record.id, 224 - keepAlive: params.keepAlive, 225 - sleepAfter: params.sleepAfter, 226 - snapshotRoot: process.env.DENO_SNAPSHOT_ROOT, 227 - spriteToken: decrypt(params.spriteToken), 228 - spriteName, 229 - daytonaApiKey: decrypt(params.daytonaApiKey), 230 - organizationId: params.daytonaOrganizationId, 231 - denoDeployToken: decrypt(params.denoDeployToken), 232 - vercelApiToken: decrypt(params.vercelApiToken), 233 - vercelProjectId: params.vercelProjectId, 234 - vercelTeamId: params.vercelTeamId, 235 - }); 236 - const sandboxId = await sandbox.id(); 237 - 238 - [record] = await tx 239 - .update(sandboxes) 240 - .set({ 241 - status: "RUNNING", 242 - sandboxId: sandboxId, 243 - startedAt: new Date(), 244 - vcpus: params.vcpus, 245 - memory: params.memory, 246 - disk: params.disk, 247 - }) 248 - .where(eq(sandboxes.id, record.id)) 249 - .returning() 250 - .execute(); 251 - 252 - return record; 253 - }); 254 - 255 - return c.json(record); 256 - } catch (err) { 257 - console.log(err); 258 - return c.json( 259 - { error: err instanceof Error ? err.message : "Unknown error" }, 260 - 400, 261 - ); 262 - } 263 - }); 264 - 265 - app.get("/v1/sandboxes/:sandboxId", async (c) => { 266 - const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 267 - return c.json(record); 268 - }); 269 - 270 - app.put("/v1/sandboxes/:sandboxId", async (c) => { 271 - return c.json({}); 272 - }); 273 - 274 - app.get("/v1/sandboxes", async (c) => { 275 - const records = await c.var.db.select().from(sandboxes).execute(); 276 - return c.json(records); 277 - }); 278 - 279 - app.post("/v1/sandboxes/:sandboxId/start", async (c) => { 280 - const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 281 - if (!record) return c.json({ error: "Sandbox not found" }, 404); 282 - if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 283 - return c.json({ error: "Sandbox provider not supported" }, 400); 284 - } 285 - 286 - const { repo } = StartSandboxInputSchema.parse( 287 - await c.req.json<StartSandboxInput>(), 288 - ); 289 - 290 - const auth = await getAuthParams(c.var.db, record.id); 291 - const credentials = buildCredentials(auth); 292 - const sandbox = await resolveSandboxInstance(c.var.db, record, credentials); 293 - 294 - await sandbox.start(); 295 - 296 - c.var.db 297 - .update(sandboxes) 298 - .set({ 299 - sandboxId: 300 - record.provider === "deno" ? await sandbox.id() : record.sandboxId, 301 - }) 302 - .where(eq(sandboxes.id, record.id)) 303 - .execute() 304 - .catch((e) => 305 - consola.error( 306 - `Failed to update SSH info for sandbox ${c.req.param("sandboxId")}: ${e}`, 307 - ), 308 - ); 309 - 310 - const [sandboxFileRecords, sshKeyRecords, tailscaleRecords, volumeRecords] = 311 - await Promise.all([ 312 - c.var.db 313 - .select() 314 - .from(sandboxFiles) 315 - .leftJoin(files, eq(files.id, sandboxFiles.fileId)) 316 - .where(eq(sandboxFiles.sandboxId, c.req.param("sandboxId"))) 317 - .execute(), 318 - c.var.db 319 - .select() 320 - .from(sshKeys) 321 - .where(eq(sshKeys.sandboxId, c.req.param("sandboxId"))) 322 - .execute(), 323 - c.var.db 324 - .select() 325 - .from(tailscaleAuthKeys) 326 - .where(eq(tailscaleAuthKeys.sandboxId, c.req.param("sandboxId"))) 327 - .execute(), 328 - c.var.db 329 - .select() 330 - .from(sandboxVolumes) 331 - .leftJoin(sandboxes, eq(sandboxes.id, sandboxVolumes.sandboxId)) 332 - .leftJoin(users, eq(users.id, sandboxes.userId)) 333 - .where(eq(sandboxVolumes.sandboxId, c.req.param("sandboxId"))) 334 - .execute(), 335 - ]); 336 - 337 - await sandbox.setupDefaultSshKeys(); 338 - 339 - Promise.all([ 340 - ...sandboxFileRecords 341 - .filter((x) => x.files !== null) 342 - .map((r) => 343 - sandbox.writeFile(r.sandbox_files.path, decrypt(r.files!.content)!), 344 - ), 345 - ...sshKeyRecords.map((r) => 346 - sandbox.setupSshKeys(decrypt(r.privateKey)!, r.publicKey), 347 - ), 348 - tailscaleRecords.length > 0 && 349 - sandbox.setupTailscale(decrypt(tailscaleRecords[0].authKey)!), 350 - ...volumeRecords.map((v) => 351 - sandbox.mount( 352 - v.sandbox_volumes.path, 353 - `/${v.users?.did || ""}${v.users?.did ? "/" : ""}${v.sandbox_volumes.id}/`, 354 - ), 355 - ), 356 - ]) 357 - .then(() => consola.success(`Sandbox ${c.req.param("sandboxId")} is ready`)) 358 - .catch((e) => 359 - consola.error( 360 - `Failed to set up sandbox ${c.req.param("sandboxId")}: ${e}`, 361 - ), 362 - ); 363 - 364 - if (record.repo) { 365 - sandbox 366 - .clone(record.repo) 367 - .then(() => 368 - consola.success(`Git Repository successfully cloned: ${record.repo}`), 369 - ) 370 - .catch((e) => consola.error(`Failed to Clone Repository: ${e}`)); 371 - } 372 - 373 - if (repo) { 374 - sandbox 375 - .clone(repo) 376 - .then(() => 377 - consola.success(`Git Repository successfully cloned: ${repo}`), 378 - ) 379 - .catch((e) => consola.error(`Failed to Clone Repository: ${e}`)); 380 - } 381 - 382 - await c.var.db 383 - .update(sandboxes) 384 - .set({ 385 - status: "RUNNING", 386 - startedAt: new Date(), 387 - sandboxId: 388 - record.provider === "deno" ? await sandbox.id() : record.sandboxId, 389 - }) 390 - .where(eq(sandboxes.id, c.req.param("sandboxId"))) 391 - .execute(); 392 - return c.json({}); 393 - }); 394 - 395 - app.post("/v1/sandboxes/:sandboxId/stop", async (c) => { 396 - const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 397 - if (!record) return c.json({ error: "Sandbox not found" }, 404); 398 - if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 399 - return c.json({ error: "Sandbox provider not supported" }, 400); 400 - } 401 - 402 - const auth = await getAuthParams(c.var.db, record.id); 403 - const sandbox = await resolveSandboxInstance( 404 - c.var.db, 405 - record, 406 - buildCredentials(auth), 407 - ); 408 - 409 - await sandbox.stop(); 410 - await c.var.db 411 - .update(sandboxes) 412 - .set({ 413 - status: "STOPPED", 414 - sandboxId: ["deno", "vercel"].includes(record.provider) 415 - ? null 416 - : record.sandboxId, 417 - }) 418 - .where(eq(sandboxes.id, c.req.param("sandboxId"))) 419 - .execute(); 420 - return c.json({}); 421 - }); 422 - 423 - app.post("/v1/sandboxes/:sandboxId/runs", async (c) => { 424 - const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 425 - if (!record) return c.json({ error: "Sandbox not found" }, 404); 426 - if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 427 - return c.json({ error: "Sandbox provider not supported" }, 400); 428 - } 429 - 430 - const auth = await getAuthParams(c.var.db, record.id); 431 - const sandbox = await resolveSandboxInstance( 432 - c.var.db, 433 - record, 434 - buildCredentials(auth), 435 - ); 436 - 437 - const { command } = await c.req.json(); 438 - const res = await sandbox.sh`${command}`; 439 - return c.json(res); 440 - }); 441 - 442 - app.delete("/v1/sandboxes/:sandboxId", async (c) => { 443 - const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 444 - if (!record) return c.json({ error: "Sandbox not found" }, 404); 445 - if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 446 - return c.json({ error: "Sandbox provider not supported" }, 400); 447 - } 448 - 449 - const auth = await getAuthParams(c.var.db, record.id); 450 - const sandbox = await resolveSandboxInstance( 451 - c.var.db, 452 - record, 453 - buildCredentials(auth), 454 - ); 455 - 456 - await sandbox.delete(); 457 - await c.var.db 458 - .delete(sandboxes) 459 - .where(eq(sandboxes.id, c.req.param("sandboxId"))) 460 - .execute(); 461 - return c.json({ success: true }, 200); 462 - }); 463 - 464 - app.get("/v1/sandboxes/:sandboxId/ssh", async (c) => { 465 - const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 466 - if (!record) return c.json({ error: "Sandbox not found" }, 404); 467 - if (!["daytona", "deno"].includes(record.provider)) { 468 - return c.json({ error: "Sandbox provider not supported" }, 400); 469 - } 470 - 471 - const auth = await getAuthParams(c.var.db, record.id); 472 - const sandbox = await resolveSandboxInstance( 473 - c.var.db, 474 - record, 475 - buildCredentials(auth), 476 - ); 477 - 478 - c.var.db 479 - .update(sandboxes) 480 - .set({ 481 - sandboxId: 482 - record.provider === "deno" ? await sandbox.id() : record.sandboxId, 483 - }) 484 - .where(eq(sandboxes.id, record.id)) 485 - .execute() 486 - .catch((e) => 487 - consola.error( 488 - `Failed to update SSH info for sandbox ${c.req.param("sandboxId")}: ${e}`, 489 - ), 490 - ); 491 - 492 - return c.json(await sandbox.ssh()); 493 - }); 494 - 495 - app.post("/v1/sandboxes/:sandboxId/ports", async (c) => { 496 - // TODO: Implement expose port 497 - return c.json({}); 498 - }); 499 - 500 - app.delete("/v1/sandboxes/:sandboxId/ports", async (c) => { 501 - // TODO: Implement unexpose port 502 - return c.json({}); 503 - }); 504 - 505 - app.post("/v1/sandboxes/:sandboxId/pull-directory", async (c) => { 506 - const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 507 - if (!record) return c.json({ error: "Sandbox not found" }, 404); 508 - if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 509 - return c.json({ error: "Sandbox provider not supported" }, 400); 510 - } 511 - 512 - const auth = await getAuthParams(c.var.db, record.id); 513 - const sandbox = await resolveSandboxInstance( 514 - c.var.db, 515 - record, 516 - buildCredentials(auth), 517 - ); 518 - 519 - const token = c.req.header("Authorization"); 520 - const params = await c.req.json<PullDirectoryParams>(); 521 - await pullSchema.parseAsync(params); 522 - 523 - const outdir = crypto.randomUUID(); 524 - await sandbox.sh`mkdir -p /tmp/${outdir} && cd /tmp/${outdir} && curl https://sandbox.pocketenv.io/cp/${params.uuid} -H "Authorization: ${token}" --output - | tar xzvf -`; 525 - await sandbox.sh`mkdir -p ${params.directoryPath} || sudo mkdir -p ${params.directoryPath}`; 526 - await sandbox.sh`(shopt -s dotglob && cp -r /tmp/${outdir}/* ${params.directoryPath}) || (shopt -s dotglob && sudo cp -r /tmp/${outdir}/* ${params.directoryPath})`; 527 - 528 - await c.var.db 529 - .delete(sandboxCp) 530 - .where(eq(sandboxCp.copyUuid, params.uuid)) 531 - .execute(); 532 - 533 - return c.json({ success: true }); 534 - }); 535 - 536 - app.post("/v1/sandboxes/:sandboxId/push-directory", async (c) => { 537 - const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 538 - if (!record) return c.json({ error: "Sandbox not found" }, 404); 539 - if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 540 - return c.json({ error: "Sandbox provider not supported" }, 400); 541 - } 542 - 543 - const auth = await getAuthParams(c.var.db, record.id); 544 - const sandbox = await resolveSandboxInstance( 545 - c.var.db, 546 - record, 547 - buildCredentials(auth), 548 - ); 549 - 550 - const token = c.req.header("Authorization"); 551 - const params = await c.req.json<PushDirectoryParams>(); 552 - await pushSchema.parseAsync(params); 553 - const uuid = crypto.randomUUID(); 554 - await sandbox.sh`cd /tmp && tar czvf ${uuid}.tar.gz -C $(dirname ${params.directoryPath}) $(basename ${params.directoryPath}) && curl -X POST "https://sandbox.pocketenv.io/cp?uuid=${uuid}" -H "Authorization: ${token}" -F "file=@${uuid}.tar.gz" && rm ${uuid}.tar.gz`; 555 - return c.json({ success: true, uuid: uuid.toString() }); 556 - }); 557 - 558 - export const getSandbox = async (db: Context["db"], sandboxId: string) => { 559 - const [record] = await db 560 - .select() 561 - .from(sandboxes) 562 - .where(or(eq(sandboxes.id, sandboxId), eq(sandboxes.sandboxId, sandboxId))) 563 - .execute(); 564 - 565 - return record; 566 - }; 567 - 568 - export const saveSecrets = async ( 569 - tx: PgTransaction< 570 - NodePgQueryResultHKT, 571 - Record<string, never>, 572 - ExtractTablesWithRelations<Record<string, never>> 573 - >, 574 - sandbox: SelectSandbox, 575 - values: { secrets: { name: string; value: string }[] }, 576 - ) => { 577 - const insertedSecrets = await tx 578 - .insert(secrets) 579 - .values(values.secrets) 580 - .returning() 581 - .execute(); 582 - 583 - await tx 584 - .insert(sandboxSecrets) 585 - .values( 586 - insertedSecrets.map((secret) => ({ 587 - sandboxId: sandbox.id, 588 - secretId: secret.id, 589 - })), 590 - ) 591 - .execute(); 592 - }; 593 - 594 - export const saveVariables = async ( 595 - tx: PgTransaction< 596 - NodePgQueryResultHKT, 597 - Record<string, never>, 598 - ExtractTablesWithRelations<Record<string, never>> 599 - >, 600 - sandbox: SelectSandbox, 601 - values: { variables: { name: string; value: string }[] }, 602 - ) => { 603 - const insertedVariables = await tx 604 - .insert(variables) 605 - .values(values.variables) 606 - .returning() 607 - .execute(); 608 - 609 - await tx 610 - .insert(sandboxVariables) 611 - .values( 612 - insertedVariables.map((variable) => ({ 613 - sandboxId: sandbox.id, 614 - variableId: variable.id, 615 - name: variable.name, 616 - })), 617 - ) 618 - .execute(); 619 - }; 27 + app.route("/v1/sandboxes", sandboxRouter); 620 28 621 29 const PORT = 8788; 622 30
+73
apps/sandbox/src/lib/sandbox-db.ts
··· 1 + import { Context } from "../context.ts"; 2 + import { eq, or } from "drizzle-orm"; 3 + import { 4 + sandboxes, 5 + sandboxSecrets, 6 + sandboxVariables, 7 + secrets, 8 + variables, 9 + } from "../schema/mod.ts"; 10 + import { SelectSandbox } from "../schema/sandboxes.ts"; 11 + import { PgTransaction } from "drizzle-orm/pg-core"; 12 + import { NodePgQueryResultHKT } from "drizzle-orm/node-postgres"; 13 + import { ExtractTablesWithRelations } from "drizzle-orm"; 14 + 15 + type Tx = PgTransaction< 16 + NodePgQueryResultHKT, 17 + Record<string, never>, 18 + ExtractTablesWithRelations<Record<string, never>> 19 + >; 20 + 21 + export const getSandbox = async (db: Context["db"], sandboxId: string) => { 22 + const [record] = await db 23 + .select() 24 + .from(sandboxes) 25 + .where(or(eq(sandboxes.id, sandboxId), eq(sandboxes.sandboxId, sandboxId))) 26 + .execute(); 27 + return record; 28 + }; 29 + 30 + export const saveSecrets = async ( 31 + tx: Tx, 32 + sandbox: SelectSandbox, 33 + values: { secrets: { name: string; value: string }[] }, 34 + ) => { 35 + const insertedSecrets = await tx 36 + .insert(secrets) 37 + .values(values.secrets) 38 + .returning() 39 + .execute(); 40 + 41 + await tx 42 + .insert(sandboxSecrets) 43 + .values( 44 + insertedSecrets.map((secret) => ({ 45 + sandboxId: sandbox.id, 46 + secretId: secret.id, 47 + })), 48 + ) 49 + .execute(); 50 + }; 51 + 52 + export const saveVariables = async ( 53 + tx: Tx, 54 + sandbox: SelectSandbox, 55 + values: { variables: { name: string; value: string }[] }, 56 + ) => { 57 + const insertedVariables = await tx 58 + .insert(variables) 59 + .values(values.variables) 60 + .returning() 61 + .execute(); 62 + 63 + await tx 64 + .insert(sandboxVariables) 65 + .values( 66 + insertedVariables.map((variable) => ({ 67 + sandboxId: sandbox.id, 68 + variableId: variable.id, 69 + name: variable.name, 70 + })), 71 + ) 72 + .execute(); 73 + };
+39
apps/sandbox/src/lib/sandbox-helpers.test.ts
··· 1 + import { assertEquals } from "@std/assert"; 2 + import { buildCredentials, AuthParams } from "./sandbox-helpers.ts"; 3 + 4 + Deno.test("buildCredentials - all undefined auth params", () => { 5 + const auth: AuthParams = {}; 6 + const creds = buildCredentials(auth); 7 + assertEquals(creds.daytonaApiKey, undefined); 8 + assertEquals(creds.organizationId, undefined); 9 + assertEquals(creds.spriteToken, undefined); 10 + assertEquals(creds.denoDeployToken, undefined); 11 + assertEquals(creds.vercelApiToken, undefined); 12 + assertEquals(creds.vercelProjectId, undefined); 13 + assertEquals(creds.vercelTeamId, undefined); 14 + }); 15 + 16 + Deno.test("buildCredentials - null auth params fields", () => { 17 + const auth: AuthParams = { 18 + spriteAuthParams: null, 19 + daytonaAuthParams: null, 20 + denoAuthParams: null, 21 + vercelAuthParams: null, 22 + }; 23 + const creds = buildCredentials(auth); 24 + assertEquals(creds.daytonaApiKey, undefined); 25 + assertEquals(creds.spriteToken, undefined); 26 + assertEquals(creds.denoDeployToken, undefined); 27 + assertEquals(creds.vercelApiToken, undefined); 28 + }); 29 + 30 + Deno.test("buildCredentials - passes through non-encrypted fields directly", () => { 31 + const auth: AuthParams = { 32 + daytonaAuthParams: { organizationId: "org-abc" }, 33 + vercelAuthParams: { projectId: "prj_xyz", teamId: "team_1" }, 34 + }; 35 + const creds = buildCredentials(auth); 36 + assertEquals(creds.organizationId, "org-abc"); 37 + assertEquals(creds.vercelProjectId, "prj_xyz"); 38 + assertEquals(creds.vercelTeamId, "team_1"); 39 + });
+30
apps/sandbox/src/middleware/auth.ts
··· 1 + import { Context } from "../context.ts"; 2 + import { consola } from "consola"; 3 + import { MiddlewareHandler } from "hono"; 4 + import { getConnection } from "../drizzle.ts"; 5 + import jwt from "@tsndr/cloudflare-worker-jwt"; 6 + import process from "node:process"; 7 + 8 + export const authMiddleware: MiddlewareHandler<{ Variables: Context }> = async ( 9 + c, 10 + next, 11 + ) => { 12 + c.set("db", getConnection()); 13 + const token = c.req.header("Authorization")?.split(" ")[1]?.trim(); 14 + if (token) { 15 + try { 16 + const decoded = await jwt.verify(token, process.env.JWT_SECRET!); 17 + c.set("did", decoded?.payload.sub); 18 + } catch (err) { 19 + consola.error("JWT verification failed:", err); 20 + return c.json({ error: "Unauthorized" }, 401); 21 + } 22 + } else { 23 + if (c.req.path === "/") { 24 + await next(); 25 + return; 26 + } 27 + return c.json({ error: "Unauthorized" }, 401); 28 + } 29 + await next(); 30 + };
+18
apps/sandbox/src/providers/mod.test.ts
··· 1 + import { assertRejects } from "@std/assert"; 2 + import { createSandbox, getSandboxById } from "./mod.ts"; 3 + 4 + Deno.test("createSandbox - throws on unsupported provider", async () => { 5 + await assertRejects( 6 + () => createSandbox("unknown" as any), 7 + Error, 8 + "Unsupported provider: unknown", 9 + ); 10 + }); 11 + 12 + Deno.test("getSandboxById - throws on unsupported provider", async () => { 13 + await assertRejects( 14 + () => getSandboxById("unknown" as any, "id-123"), 15 + Error, 16 + "Unsupported provider: unknown", 17 + ); 18 + });
+507
apps/sandbox/src/routes/sandboxes.ts
··· 1 + import { Hono } from "hono"; 2 + import { Context } from "../context.ts"; 3 + import { consola } from "consola"; 4 + import { 5 + adjectives, 6 + nouns, 7 + generateUniqueAsync, 8 + } from "unique-username-generator"; 9 + import { eq } from "drizzle-orm"; 10 + import { 11 + files, 12 + sandboxes, 13 + sandboxCp, 14 + sandboxFiles, 15 + sandboxVolumes, 16 + sshKeys, 17 + tailscaleAuthKeys, 18 + users, 19 + spriteAuth, 20 + denoAuth, 21 + vercelAuth, 22 + } from "../schema/mod.ts"; 23 + import { 24 + SandboxConfig, 25 + SandboxConfigSchema, 26 + StartSandboxInput, 27 + StartSandboxInputSchema, 28 + } from "../types/sandbox.ts"; 29 + import { createSandbox } from "../providers/mod.ts"; 30 + import { getSandbox, saveSecrets, saveVariables } from "../lib/sandbox-db.ts"; 31 + import { 32 + getAuthParams, 33 + buildCredentials, 34 + resolveSandboxInstance, 35 + } from "../lib/sandbox-helpers.ts"; 36 + import decrypt from "../lib/decrypt.ts"; 37 + import { InsertSpriteAuth } from "../schema/sprite-auth.ts"; 38 + import daytonaAuth, { InsertDaytonaAuth } from "../schema/daytona-auth.ts"; 39 + import { InsertDenoAuth } from "../schema/deno-auth.ts"; 40 + import { PullDirectoryParams, pullSchema } from "../types/pull.ts"; 41 + import { PushDirectoryParams, pushSchema } from "../types/push.ts"; 42 + import crypto from "node:crypto"; 43 + import process from "node:process"; 44 + 45 + const SUPPORTED_PROVIDERS = ["daytona", "vercel", "deno", "sprites"]; 46 + 47 + const sandboxRouter = new Hono<{ Variables: Context }>(); 48 + 49 + sandboxRouter.post("/", async (c) => { 50 + const body = await c.req.json<SandboxConfig>(); 51 + 52 + let suffix = Math.random().toString(36).substring(2, 6); 53 + let name = await generateUniqueAsync( 54 + { dictionaries: [adjectives, nouns], separator: "-" }, 55 + () => false, 56 + ); 57 + let spriteName = await generateUniqueAsync( 58 + { dictionaries: [adjectives, nouns], separator: "-" }, 59 + () => false, 60 + ); 61 + spriteName = `${spriteName}-${suffix}`; 62 + 63 + try { 64 + const params = SandboxConfigSchema.parse(body); 65 + name = params.name || `${name}-${suffix}`; 66 + 67 + do { 68 + const existing = await c.var.db 69 + .select() 70 + .from(sandboxes) 71 + .where(eq(sandboxes.name, name)) 72 + .execute(); 73 + if (existing.length === 0) break; 74 + 75 + name = await generateUniqueAsync( 76 + { dictionaries: [adjectives, nouns], separator: "-" }, 77 + () => false, 78 + ); 79 + suffix = Math.random().toString(36).substring(2, 6); 80 + name = `${name}-${suffix}`; 81 + } while (true); 82 + 83 + const record = await c.var.db.transaction(async (tx) => { 84 + const user = await tx 85 + .select() 86 + .from(users) 87 + .where(eq(users.did, c.var.did || "")) 88 + .execute() 89 + .then((res) => res[0]); 90 + 91 + let [record] = await tx 92 + .insert(sandboxes) 93 + .values({ 94 + base: params.base, 95 + name, 96 + provider: params.provider, 97 + publicKey: process.env.PUBLIC_KEY!, 98 + userId: user?.id, 99 + instanceType: "standard-1", 100 + keepAlive: params.keepAlive, 101 + sleepAfter: params.sleepAfter, 102 + status: "INITIALIZING", 103 + }) 104 + .returning() 105 + .execute(); 106 + 107 + if (params.secrets.length > 0) { 108 + await saveSecrets(tx, record, { secrets: params.secrets }); 109 + } 110 + 111 + if (params.variables.length > 0) { 112 + await saveVariables(tx, record, { variables: params.variables }); 113 + } 114 + 115 + if (params.spriteToken && user?.id) { 116 + await tx 117 + .insert(spriteAuth) 118 + .values({ 119 + sandboxId: record.id, 120 + spriteToken: params.spriteToken, 121 + redactedSpriteToken: params.redactedSpriteToken ?? "", 122 + userId: user.id, 123 + } satisfies InsertSpriteAuth) 124 + .execute(); 125 + } 126 + 127 + if (params.daytonaApiKey && user?.id) { 128 + await tx 129 + .insert(daytonaAuth) 130 + .values({ 131 + sandboxId: record.id, 132 + apiKey: params.daytonaApiKey, 133 + redactedApiKey: params.redactedDaytonaApiKey ?? "", 134 + userId: user.id, 135 + organizationId: params.daytonaOrganizationId!, 136 + } satisfies InsertDaytonaAuth) 137 + .execute(); 138 + } 139 + 140 + if (params.denoDeployToken && user?.id) { 141 + await tx 142 + .insert(denoAuth) 143 + .values({ 144 + sandboxId: record.id, 145 + deployToken: params.denoDeployToken, 146 + redactedDenoToken: params.redactedDenoDeployToken ?? "", 147 + userId: user.id, 148 + } satisfies InsertDenoAuth) 149 + .execute(); 150 + } 151 + 152 + if (params.vercelApiToken && user?.id) { 153 + await tx 154 + .insert(vercelAuth) 155 + .values({ 156 + sandboxId: record.id, 157 + vercelToken: params.vercelApiToken, 158 + redactedVercelToken: params.redactedVercelApiToken ?? "", 159 + userId: user.id, 160 + projectId: params.vercelProjectId!, 161 + teamId: params.vercelTeamId!, 162 + }) 163 + .execute(); 164 + } 165 + 166 + const sandbox = await createSandbox(params.provider, { 167 + id: record.id, 168 + keepAlive: params.keepAlive, 169 + sleepAfter: params.sleepAfter, 170 + snapshotRoot: process.env.DENO_SNAPSHOT_ROOT, 171 + spriteToken: decrypt(params.spriteToken), 172 + spriteName, 173 + daytonaApiKey: decrypt(params.daytonaApiKey), 174 + organizationId: params.daytonaOrganizationId, 175 + denoDeployToken: decrypt(params.denoDeployToken), 176 + vercelApiToken: decrypt(params.vercelApiToken), 177 + vercelProjectId: params.vercelProjectId, 178 + vercelTeamId: params.vercelTeamId, 179 + }); 180 + const sandboxId = await sandbox.id(); 181 + 182 + [record] = await tx 183 + .update(sandboxes) 184 + .set({ 185 + status: "RUNNING", 186 + sandboxId: sandboxId, 187 + startedAt: new Date(), 188 + vcpus: params.vcpus, 189 + memory: params.memory, 190 + disk: params.disk, 191 + }) 192 + .where(eq(sandboxes.id, record.id)) 193 + .returning() 194 + .execute(); 195 + 196 + return record; 197 + }); 198 + 199 + return c.json(record); 200 + } catch (err) { 201 + console.log(err); 202 + return c.json( 203 + { error: err instanceof Error ? err.message : "Unknown error" }, 204 + 400, 205 + ); 206 + } 207 + }); 208 + 209 + sandboxRouter.get("/", async (c) => { 210 + const records = await c.var.db.select().from(sandboxes).execute(); 211 + return c.json(records); 212 + }); 213 + 214 + sandboxRouter.get("/:sandboxId", async (c) => { 215 + const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 216 + return c.json(record); 217 + }); 218 + 219 + sandboxRouter.put("/:sandboxId", async (c) => { 220 + return c.json({}); 221 + }); 222 + 223 + sandboxRouter.post("/:sandboxId/start", async (c) => { 224 + const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 225 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 226 + if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 227 + return c.json({ error: "Sandbox provider not supported" }, 400); 228 + } 229 + 230 + const { repo } = StartSandboxInputSchema.parse( 231 + await c.req.json<StartSandboxInput>(), 232 + ); 233 + 234 + const auth = await getAuthParams(c.var.db, record.id); 235 + const credentials = buildCredentials(auth); 236 + const sandbox = await resolveSandboxInstance(c.var.db, record, credentials); 237 + 238 + await sandbox.start(); 239 + 240 + c.var.db 241 + .update(sandboxes) 242 + .set({ 243 + sandboxId: 244 + record.provider === "deno" ? await sandbox.id() : record.sandboxId, 245 + }) 246 + .where(eq(sandboxes.id, record.id)) 247 + .execute() 248 + .catch((e) => 249 + consola.error( 250 + `Failed to update SSH info for sandbox ${c.req.param("sandboxId")}: ${e}`, 251 + ), 252 + ); 253 + 254 + const [sandboxFileRecords, sshKeyRecords, tailscaleRecords, volumeRecords] = 255 + await Promise.all([ 256 + c.var.db 257 + .select() 258 + .from(sandboxFiles) 259 + .leftJoin(files, eq(files.id, sandboxFiles.fileId)) 260 + .where(eq(sandboxFiles.sandboxId, c.req.param("sandboxId"))) 261 + .execute(), 262 + c.var.db 263 + .select() 264 + .from(sshKeys) 265 + .where(eq(sshKeys.sandboxId, c.req.param("sandboxId"))) 266 + .execute(), 267 + c.var.db 268 + .select() 269 + .from(tailscaleAuthKeys) 270 + .where(eq(tailscaleAuthKeys.sandboxId, c.req.param("sandboxId"))) 271 + .execute(), 272 + c.var.db 273 + .select() 274 + .from(sandboxVolumes) 275 + .leftJoin(sandboxes, eq(sandboxes.id, sandboxVolumes.sandboxId)) 276 + .leftJoin(users, eq(users.id, sandboxes.userId)) 277 + .where(eq(sandboxVolumes.sandboxId, c.req.param("sandboxId"))) 278 + .execute(), 279 + ]); 280 + 281 + await sandbox.setupDefaultSshKeys(); 282 + 283 + Promise.all([ 284 + ...sandboxFileRecords 285 + .filter((x) => x.files !== null) 286 + .map((r) => 287 + sandbox.writeFile(r.sandbox_files.path, decrypt(r.files!.content)!), 288 + ), 289 + ...sshKeyRecords.map((r) => 290 + sandbox.setupSshKeys(decrypt(r.privateKey)!, r.publicKey), 291 + ), 292 + tailscaleRecords.length > 0 && 293 + sandbox.setupTailscale(decrypt(tailscaleRecords[0].authKey)!), 294 + ...volumeRecords.map((v) => 295 + sandbox.mount( 296 + v.sandbox_volumes.path, 297 + `/${v.users?.did || ""}${v.users?.did ? "/" : ""}${v.sandbox_volumes.id}/`, 298 + ), 299 + ), 300 + ]) 301 + .then(() => consola.success(`Sandbox ${c.req.param("sandboxId")} is ready`)) 302 + .catch((e) => 303 + consola.error( 304 + `Failed to set up sandbox ${c.req.param("sandboxId")}: ${e}`, 305 + ), 306 + ); 307 + 308 + if (record.repo) { 309 + sandbox 310 + .clone(record.repo) 311 + .then(() => 312 + consola.success(`Git Repository successfully cloned: ${record.repo}`), 313 + ) 314 + .catch((e) => consola.error(`Failed to Clone Repository: ${e}`)); 315 + } 316 + 317 + if (repo) { 318 + sandbox 319 + .clone(repo) 320 + .then(() => 321 + consola.success(`Git Repository successfully cloned: ${repo}`), 322 + ) 323 + .catch((e) => consola.error(`Failed to Clone Repository: ${e}`)); 324 + } 325 + 326 + await c.var.db 327 + .update(sandboxes) 328 + .set({ 329 + status: "RUNNING", 330 + startedAt: new Date(), 331 + sandboxId: 332 + record.provider === "deno" ? await sandbox.id() : record.sandboxId, 333 + }) 334 + .where(eq(sandboxes.id, c.req.param("sandboxId"))) 335 + .execute(); 336 + 337 + return c.json({}); 338 + }); 339 + 340 + sandboxRouter.post("/:sandboxId/stop", async (c) => { 341 + const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 342 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 343 + if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 344 + return c.json({ error: "Sandbox provider not supported" }, 400); 345 + } 346 + 347 + const auth = await getAuthParams(c.var.db, record.id); 348 + const sandbox = await resolveSandboxInstance( 349 + c.var.db, 350 + record, 351 + buildCredentials(auth), 352 + ); 353 + 354 + await sandbox.stop(); 355 + await c.var.db 356 + .update(sandboxes) 357 + .set({ 358 + status: "STOPPED", 359 + sandboxId: ["deno", "vercel"].includes(record.provider) 360 + ? null 361 + : record.sandboxId, 362 + }) 363 + .where(eq(sandboxes.id, c.req.param("sandboxId"))) 364 + .execute(); 365 + 366 + return c.json({}); 367 + }); 368 + 369 + sandboxRouter.post("/:sandboxId/runs", async (c) => { 370 + const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 371 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 372 + if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 373 + return c.json({ error: "Sandbox provider not supported" }, 400); 374 + } 375 + 376 + const auth = await getAuthParams(c.var.db, record.id); 377 + const sandbox = await resolveSandboxInstance( 378 + c.var.db, 379 + record, 380 + buildCredentials(auth), 381 + ); 382 + 383 + const { command } = await c.req.json(); 384 + const res = await sandbox.sh`${command}`; 385 + return c.json(res); 386 + }); 387 + 388 + sandboxRouter.delete("/:sandboxId", async (c) => { 389 + const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 390 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 391 + if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 392 + return c.json({ error: "Sandbox provider not supported" }, 400); 393 + } 394 + 395 + const auth = await getAuthParams(c.var.db, record.id); 396 + const sandbox = await resolveSandboxInstance( 397 + c.var.db, 398 + record, 399 + buildCredentials(auth), 400 + ); 401 + 402 + await sandbox.delete(); 403 + await c.var.db 404 + .delete(sandboxes) 405 + .where(eq(sandboxes.id, c.req.param("sandboxId"))) 406 + .execute(); 407 + 408 + return c.json({ success: true }, 200); 409 + }); 410 + 411 + sandboxRouter.get("/:sandboxId/ssh", async (c) => { 412 + const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 413 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 414 + if (!["daytona", "deno"].includes(record.provider)) { 415 + return c.json({ error: "Sandbox provider not supported" }, 400); 416 + } 417 + 418 + const auth = await getAuthParams(c.var.db, record.id); 419 + const sandbox = await resolveSandboxInstance( 420 + c.var.db, 421 + record, 422 + buildCredentials(auth), 423 + ); 424 + 425 + c.var.db 426 + .update(sandboxes) 427 + .set({ 428 + sandboxId: 429 + record.provider === "deno" ? await sandbox.id() : record.sandboxId, 430 + }) 431 + .where(eq(sandboxes.id, record.id)) 432 + .execute() 433 + .catch((e) => 434 + consola.error( 435 + `Failed to update SSH info for sandbox ${c.req.param("sandboxId")}: ${e}`, 436 + ), 437 + ); 438 + 439 + return c.json(await sandbox.ssh()); 440 + }); 441 + 442 + sandboxRouter.post("/:sandboxId/ports", async (c) => { 443 + // TODO: Implement expose port 444 + return c.json({}); 445 + }); 446 + 447 + sandboxRouter.delete("/:sandboxId/ports", async (c) => { 448 + // TODO: Implement unexpose port 449 + return c.json({}); 450 + }); 451 + 452 + sandboxRouter.post("/:sandboxId/pull-directory", async (c) => { 453 + const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 454 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 455 + if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 456 + return c.json({ error: "Sandbox provider not supported" }, 400); 457 + } 458 + 459 + const auth = await getAuthParams(c.var.db, record.id); 460 + const sandbox = await resolveSandboxInstance( 461 + c.var.db, 462 + record, 463 + buildCredentials(auth), 464 + ); 465 + 466 + const token = c.req.header("Authorization"); 467 + const params = await c.req.json<PullDirectoryParams>(); 468 + await pullSchema.parseAsync(params); 469 + 470 + const outdir = crypto.randomUUID(); 471 + await sandbox.sh`mkdir -p /tmp/${outdir} && cd /tmp/${outdir} && curl https://sandbox.pocketenv.io/cp/${params.uuid} -H "Authorization: ${token}" --output - | tar xzvf -`; 472 + await sandbox.sh`mkdir -p ${params.directoryPath} || sudo mkdir -p ${params.directoryPath}`; 473 + await sandbox.sh`(shopt -s dotglob && cp -r /tmp/${outdir}/* ${params.directoryPath}) || (shopt -s dotglob && sudo cp -r /tmp/${outdir}/* ${params.directoryPath})`; 474 + 475 + await c.var.db 476 + .delete(sandboxCp) 477 + .where(eq(sandboxCp.copyUuid, params.uuid)) 478 + .execute(); 479 + 480 + return c.json({ success: true }); 481 + }); 482 + 483 + sandboxRouter.post("/:sandboxId/push-directory", async (c) => { 484 + const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 485 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 486 + if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 487 + return c.json({ error: "Sandbox provider not supported" }, 400); 488 + } 489 + 490 + const auth = await getAuthParams(c.var.db, record.id); 491 + const sandbox = await resolveSandboxInstance( 492 + c.var.db, 493 + record, 494 + buildCredentials(auth), 495 + ); 496 + 497 + const token = c.req.header("Authorization"); 498 + const params = await c.req.json<PushDirectoryParams>(); 499 + await pushSchema.parseAsync(params); 500 + 501 + const uuid = crypto.randomUUID(); 502 + await sandbox.sh`cd /tmp && tar czvf ${uuid}.tar.gz -C $(dirname ${params.directoryPath}) $(basename ${params.directoryPath}) && curl -X POST "https://sandbox.pocketenv.io/cp?uuid=${uuid}" -H "Authorization: ${token}" -F "file=@${uuid}.tar.gz" && rm ${uuid}.tar.gz`; 503 + 504 + return c.json({ success: true, uuid: uuid.toString() }); 505 + }); 506 + 507 + export { sandboxRouter };
+26
apps/sandbox/src/types/pull.test.ts
··· 1 + import { assertEquals } from "@std/assert"; 2 + import { pullSchema } from "./pull.ts"; 3 + 4 + Deno.test("pullSchema - valid input", () => { 5 + const result = pullSchema.parse({ 6 + uuid: "abc-123", 7 + directoryPath: "/workspace/project", 8 + }); 9 + assertEquals(result.uuid, "abc-123"); 10 + assertEquals(result.directoryPath, "/workspace/project"); 11 + }); 12 + 13 + Deno.test("pullSchema - missing uuid fails", () => { 14 + const result = pullSchema.safeParse({ directoryPath: "/workspace" }); 15 + assertEquals(result.success, false); 16 + }); 17 + 18 + Deno.test("pullSchema - missing directoryPath fails", () => { 19 + const result = pullSchema.safeParse({ uuid: "abc-123" }); 20 + assertEquals(result.success, false); 21 + }); 22 + 23 + Deno.test("pullSchema - empty object fails", () => { 24 + const result = pullSchema.safeParse({}); 25 + assertEquals(result.success, false); 26 + });
+17
apps/sandbox/src/types/push.test.ts
··· 1 + import { assertEquals } from "@std/assert"; 2 + import { pushSchema } from "./push.ts"; 3 + 4 + Deno.test("pushSchema - valid input", () => { 5 + const result = pushSchema.parse({ directoryPath: "/workspace/project" }); 6 + assertEquals(result.directoryPath, "/workspace/project"); 7 + }); 8 + 9 + Deno.test("pushSchema - missing directoryPath fails", () => { 10 + const result = pushSchema.safeParse({}); 11 + assertEquals(result.success, false); 12 + }); 13 + 14 + Deno.test("pushSchema - empty string directoryPath is allowed", () => { 15 + const result = pushSchema.safeParse({ directoryPath: "" }); 16 + assertEquals(result.success, true); 17 + });
+172
apps/sandbox/src/types/sandbox.test.ts
··· 1 + import { assertEquals, assertThrows } from "@std/assert"; 2 + import { SandboxConfigSchema, StartSandboxInputSchema } from "./sandbox.ts"; 3 + 4 + Deno.test("SandboxConfigSchema - defaults", () => { 5 + const result = SandboxConfigSchema.parse({ 6 + provider: "deno", 7 + denoDeployToken: "tok", 8 + redactedDenoDeployToken: "redacted", 9 + }); 10 + assertEquals(result.provider, "deno"); 11 + assertEquals(result.base, "openclaw"); 12 + assertEquals(result.keepAlive, false); 13 + assertEquals(result.vcpus, 2); 14 + assertEquals(result.memory, 4); 15 + assertEquals(result.disk, 3); 16 + assertEquals(result.secrets, []); 17 + assertEquals(result.variables, []); 18 + }); 19 + 20 + Deno.test("SandboxConfigSchema - deno provider requires tokens", () => { 21 + const result = SandboxConfigSchema.safeParse({ provider: "deno" }); 22 + assertEquals(result.success, false); 23 + const issues = (result as any).error.issues.map((i: any) => i.path[0]); 24 + assertEquals(issues.includes("denoDeployToken"), true); 25 + assertEquals(issues.includes("redactedDenoDeployToken"), true); 26 + }); 27 + 28 + Deno.test("SandboxConfigSchema - sprites provider requires spriteToken", () => { 29 + const result = SandboxConfigSchema.safeParse({ provider: "sprites" }); 30 + assertEquals(result.success, false); 31 + const issues = (result as any).error.issues.map((i: any) => i.path[0]); 32 + assertEquals(issues.includes("spriteToken"), true); 33 + assertEquals(issues.includes("redactedSpriteToken"), true); 34 + }); 35 + 36 + Deno.test("SandboxConfigSchema - daytona provider requires apiKey and orgId", () => { 37 + const result = SandboxConfigSchema.safeParse({ provider: "daytona" }); 38 + assertEquals(result.success, false); 39 + const issues = (result as any).error.issues.map((i: any) => i.path[0]); 40 + assertEquals(issues.includes("daytonaApiKey"), true); 41 + assertEquals(issues.includes("redactedDaytonaApiKey"), true); 42 + assertEquals(issues.includes("daytonaOrganizationId"), true); 43 + }); 44 + 45 + Deno.test("SandboxConfigSchema - vercel provider requires token and projectId", () => { 46 + const result = SandboxConfigSchema.safeParse({ provider: "vercel" }); 47 + assertEquals(result.success, false); 48 + const issues = (result as any).error.issues.map((i: any) => i.path[0]); 49 + assertEquals(issues.includes("vercelApiKey"), true); 50 + assertEquals(issues.includes("vercelProjectId"), true); 51 + }); 52 + 53 + Deno.test("SandboxConfigSchema - valid daytona config", () => { 54 + const result = SandboxConfigSchema.parse({ 55 + provider: "daytona", 56 + daytonaApiKey: "key", 57 + redactedDaytonaApiKey: "redacted", 58 + daytonaOrganizationId: "org-123", 59 + }); 60 + assertEquals(result.provider, "daytona"); 61 + assertEquals(result.daytonaApiKey, "key"); 62 + assertEquals(result.daytonaOrganizationId, "org-123"); 63 + }); 64 + 65 + Deno.test("SandboxConfigSchema - valid sprites config", () => { 66 + const result = SandboxConfigSchema.parse({ 67 + provider: "sprites", 68 + spriteToken: "tok", 69 + redactedSpriteToken: "redacted", 70 + }); 71 + assertEquals(result.provider, "sprites"); 72 + assertEquals(result.spriteToken, "tok"); 73 + }); 74 + 75 + Deno.test("SandboxConfigSchema - valid vercel config", () => { 76 + const result = SandboxConfigSchema.parse({ 77 + provider: "vercel", 78 + vercelApiToken: "vt_abc", 79 + redactedVercelApiToken: "redacted", 80 + vercelProjectId: "prj_123", 81 + }); 82 + assertEquals(result.provider, "vercel"); 83 + assertEquals(result.vercelApiToken, "vt_abc"); 84 + assertEquals(result.vercelProjectId, "prj_123"); 85 + }); 86 + 87 + Deno.test("SandboxConfigSchema - sleepAfter valid formats", () => { 88 + for (const valid of ["1h", "30m", "15s", "100s"]) { 89 + const result = SandboxConfigSchema.safeParse({ 90 + provider: "deno", 91 + denoDeployToken: "tok", 92 + redactedDenoDeployToken: "r", 93 + sleepAfter: valid, 94 + }); 95 + assertEquals(result.success, true, `Expected ${valid} to be valid`); 96 + } 97 + }); 98 + 99 + Deno.test("SandboxConfigSchema - sleepAfter invalid format", () => { 100 + const result = SandboxConfigSchema.safeParse({ 101 + provider: "deno", 102 + denoDeployToken: "tok", 103 + redactedDenoDeployToken: "r", 104 + sleepAfter: "2hours", 105 + }); 106 + assertEquals(result.success, false); 107 + }); 108 + 109 + Deno.test("SandboxConfigSchema - rejects duplicate secret names", () => { 110 + assertThrows( 111 + () => 112 + SandboxConfigSchema.safeParse({ 113 + provider: "deno", 114 + denoDeployToken: "tok", 115 + redactedDenoDeployToken: "r", 116 + secrets: [ 117 + { name: "API_KEY", value: "a" }, 118 + { name: "API_KEY", value: "b" }, 119 + ], 120 + }), 121 + Error, 122 + "Duplicate names found", 123 + ); 124 + }); 125 + 126 + Deno.test("SandboxConfigSchema - rejects duplicate variable names", () => { 127 + assertThrows( 128 + () => 129 + SandboxConfigSchema.safeParse({ 130 + provider: "deno", 131 + denoDeployToken: "tok", 132 + redactedDenoDeployToken: "r", 133 + variables: [ 134 + { name: "PORT", value: "3000" }, 135 + { name: "PORT", value: "4000" }, 136 + ], 137 + }), 138 + Error, 139 + "Duplicate names found", 140 + ); 141 + }); 142 + 143 + Deno.test("SandboxConfigSchema - accepts unique secrets and variables", () => { 144 + const result = SandboxConfigSchema.parse({ 145 + provider: "deno", 146 + denoDeployToken: "tok", 147 + redactedDenoDeployToken: "r", 148 + secrets: [ 149 + { name: "DB_URL", value: "postgres://..." }, 150 + { name: "API_KEY", value: "secret" }, 151 + ], 152 + variables: [ 153 + { name: "PORT", value: "3000" }, 154 + { name: "HOST", value: "localhost" }, 155 + ], 156 + }); 157 + assertEquals(result.secrets.length, 2); 158 + assertEquals(result.variables.length, 2); 159 + }); 160 + 161 + Deno.test("SandboxConfigSchema - rejects unsupported provider", () => { 162 + const result = SandboxConfigSchema.safeParse({ provider: "aws" }); 163 + assertEquals(result.success, false); 164 + }); 165 + 166 + Deno.test("StartSandboxInputSchema - optional repo", () => { 167 + assertEquals(StartSandboxInputSchema.parse({}).repo, undefined); 168 + assertEquals( 169 + StartSandboxInputSchema.parse({ repo: "https://github.com/x/y" }).repo, 170 + "https://github.com/x/y", 171 + ); 172 + });