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.

Modularize sandbox routes and helpers

+1220 -1647
+15 -1100
apps/cf-sandbox/src/index.ts
··· 1 1 import { Hono } from "hono"; 2 2 import { cors } from "hono/cors"; 3 3 import { Context } from "./context"; 4 - import { getSandbox, proxyToSandbox, Sandbox } from "@cloudflare/sandbox"; 5 - import { 6 - files, 7 - sandboxCp, 8 - sandboxes, 9 - sandboxFiles, 10 - sandboxPorts, 11 - sandboxSecrets, 12 - sandboxVariables, 13 - sandboxVolumes, 14 - secrets, 15 - sshKeys, 16 - tailscaleAuthKeys, 17 - users, 18 - variables, 19 - } from "./schema"; 20 - import { 21 - adjectives, 22 - nouns, 23 - generateUniqueAsync, 24 - } from "unique-username-generator"; 25 - import { and, eq, ExtractTablesWithRelations, isNull, or } from "drizzle-orm"; 26 - import { getConnection } from "./drizzle"; 27 - import { env } from "cloudflare:workers"; 28 - import { 29 - SandboxConfig, 30 - SandboxConfigSchema, 31 - StartSandboxConfig, 32 - StartSandboxConfigSchema, 33 - } from "./types/sandbox"; 34 - import { BaseSandbox, createSandbox } from "./providers"; 35 - import { SelectSandbox } from "./schema/sandboxes"; 36 - import { PgTransaction } from "drizzle-orm/pg-core"; 37 - import { NodePgQueryResultHKT } from "drizzle-orm/node-postgres"; 38 - import jwt from "@tsndr/cloudflare-worker-jwt"; 39 - import { consola } from "consola"; 40 - import decrypt from "./lib/decrypt"; 41 - import crypto from "node:crypto"; 42 - import services from "./schema/services"; 43 - import { PushDirectoryParams, pushSchema } from "./types/push"; 44 - import { PullDirectoryParams, pullSchema } from "./types/pull"; 4 + import { proxyToSandbox, Sandbox } from "@cloudflare/sandbox"; 5 + import { authMiddleware } from "./middleware/auth"; 6 + import { cpRoutes } from "./routes/cp"; 7 + import { sandboxRoutes } from "./routes/sandboxes"; 8 + import { terminalRoutes } from "./routes/terminal"; 9 + 10 + export { saveSecrets, saveVariables, getSandboxRecord as getSandboxById } from "./lib/sandbox-helpers"; 45 11 46 12 type Bindings = { 47 13 Sandbox: DurableObjectNamespace<Sandbox<Env>>; ··· 54 20 app.use("*", async (c, next) => { 55 21 const proxyResponse = await proxyToSandbox(c.req.raw, c.env); 56 22 if (proxyResponse) return proxyResponse; 57 - 58 - c.set("db", getConnection()); 59 - const token = c.req.header("Authorization")?.split(" ")[1]?.trim(); 60 - if (token) { 61 - try { 62 - const decoded = await jwt.verify(token, process.env.JWT_SECRET!); 63 - c.set( 64 - "did", 65 - decoded?.payload?.sub || (decoded?.payload as { did: string })?.did, 66 - ); 67 - } catch (err) { 68 - consola.error("JWT verification failed:", err); 69 - return c.json({ error: "Unauthorized" }, 401); 70 - } 71 - } else { 72 - if (!c.req.path.endsWith("/ws/terminal") && c.req.path !== "/") { 73 - consola.warn("No Authorization header found"); 74 - return c.json({ error: "Unauthorized" }, 401); 75 - } 76 - } 77 23 await next(); 78 24 }); 79 25 80 - interface CmdOutput { 81 - success: boolean; 82 - stdout: string; 83 - stderr: string; 84 - } 85 - const getOutput = (res: CmdOutput) => (res.success ? res.stdout : res.stderr); 26 + app.use("*", authMiddleware); 86 27 87 - app.get("/", async (c) => { 88 - return c.text(` 28 + app.get("/", (c) => 29 + c.text(` 89 30 _____ ____ 90 31 / ___/____ _____ ____/ / /_ ____ _ __ 91 32 \\__ \\/ __ \`/ __ \\/ __ / __ \\/ __ \\| |/_/ 92 33 ___/ / /_/ / / / / /_/ / /_/ / /_/ /> < 93 34 /____/\\__,_/_/ /_/\\__,_/_.___/\\____/_/|_| 94 35 95 - `); 96 - }); 97 - 98 - app.post("/cp", async (c) => { 99 - if (!c.var.did) { 100 - return c.json({ error: "Unauthorized" }, 401); 101 - } 102 - const formData = await c.req.formData(); 103 - const file = formData.get("file") as File; 104 - 105 - if (!file) { 106 - return c.json({ error: "No file uploaded" }, 400); 107 - } 108 - 109 - const fileBuffer = await file.arrayBuffer(); 110 - const uuid = c.req.query("uuid") || crypto.randomUUID(); 111 - await env.POCKETENV_COPY.put(uuid, fileBuffer); 112 - 113 - c.executionCtx.waitUntil( 114 - c.var.db 115 - .insert(sandboxCp) 116 - .values({ 117 - copyUuid: uuid, 118 - }) 119 - .execute(), 120 - ); 121 - 122 - return c.json({ uuid }); 123 - }); 124 - 125 - app.get("/cp/:uuid", async (c) => { 126 - const { uuid } = c.req.param(); 127 - const file = await env.POCKETENV_COPY.get(uuid); 128 - if (!file) { 129 - return c.json({ error: "File not found" }, 404); 130 - } 131 - 132 - await c.var.db 133 - .delete(sandboxCp) 134 - .where(eq(sandboxCp.copyUuid, uuid)) 135 - .execute(); 136 - 137 - await env.POCKETENV_COPY.delete(uuid); 138 - 139 - return c.body(file.body, 200, { 140 - "Content-Type": "application/gzip", 141 - "Content-Disposition": `attachment; filename="${uuid}.tar.gz"`, 142 - }); 143 - }); 144 - 145 - app.post("/v1/sandboxes", async (c) => { 146 - const body = await c.req.json<SandboxConfig>(); 147 - 148 - let suffix = Math.random().toString(36).substring(2, 6); 149 - let name = await generateUniqueAsync( 150 - { dictionaries: [adjectives, nouns], separator: "-" }, 151 - () => false, 152 - ); 153 - 154 - try { 155 - const params = SandboxConfigSchema.parse(body); 156 - name = params.name || `${name}-${suffix}`; 157 - let existing: SelectSandbox[] = []; 158 - 159 - if (params.name) { 160 - existing = await c.var.db 161 - .select() 162 - .from(sandboxes) 163 - .where(and(eq(sandboxes.name, params.name), isNull(sandboxes.userId))) 164 - .execute(); 165 - } 166 - 167 - const canBeClaimed = existing.length !== 0; 168 - 169 - if (!canBeClaimed) { 170 - do { 171 - existing = await c.var.db 172 - .select() 173 - .from(sandboxes) 174 - .where(eq(sandboxes.name, name)) 175 - .execute(); 176 - if (existing.length === 0) { 177 - break; 178 - } 179 - 180 - name = await generateUniqueAsync( 181 - { dictionaries: [adjectives, nouns], separator: "-" }, 182 - () => false, 183 - ); 184 - suffix = Math.random().toString(36).substring(2, 6); 185 - name = `${name}-${suffix}`; 186 - } while (true); 187 - } 188 - 189 - const record = await c.var.db.transaction(async (tx) => { 190 - const user = await tx 191 - .select() 192 - .from(users) 193 - .where(eq(users.did, c.var.did || "")) 194 - .execute() 195 - .then(([row]) => row); 196 - 197 - let record: SelectSandbox | undefined = undefined; 198 - if (canBeClaimed) { 199 - record = await tx 200 - .update(sandboxes) 201 - .set({ userId: user?.id }) 202 - .where(eq(sandboxes.id, existing[0].id)) 203 - .returning() 204 - .execute() 205 - .then(([row]) => row); 206 - } else { 207 - record = await tx 208 - .insert(sandboxes) 209 - .values({ 210 - base: params.base, 211 - name, 212 - repo: params.repo, 213 - provider: params.provider, 214 - publicKey: env.PUBLIC_KEY, 215 - userId: user?.id, 216 - instanceType: "standard-3", 217 - keepAlive: params.keepAlive, 218 - sleepAfter: params.sleepAfter, 219 - vcpus: params.vcpus, 220 - memory: params.memory, 221 - disk: params.disk, 222 - status: "INITIALIZING", 223 - }) 224 - .returning() 225 - .execute() 226 - .then(([row]) => row); 227 - } 228 - 229 - if (params.secrets.length > 0) { 230 - await saveSecrets(tx, record!, { secrets: params.secrets }); 231 - } 232 - 233 - if (params.variables.length > 0) { 234 - await saveVariables(tx, record!, { variables: params.variables }); 235 - } 236 - 237 - const sandboxId = Array.from( 238 - crypto.getRandomValues(new Uint8Array(16)), 239 - (b) => b.toString(16).padStart(2, "0"), 240 - ).join(""); 241 - 242 - const sandboxInstance = await createSandbox(params.provider, { 243 - id: sandboxId, 244 - keepAlive: params.keepAlive, 245 - sleepAfter: params.sleepAfter, 246 - }); 247 - 248 - await sandboxInstance.start(); 249 - 250 - [record] = await tx 251 - .update(sandboxes) 252 - .set({ 253 - status: "RUNNING", 254 - sandboxId, 255 - startedAt: new Date(), 256 - }) 257 - .where(eq(sandboxes.id, record!.id)) 258 - .returning() 259 - .execute(); 260 - 261 - if (params.repo) { 262 - c.executionCtx.waitUntil( 263 - sandboxInstance 264 - .clone(params.repo) 265 - .then(() => 266 - consola.success( 267 - `Git Repository successfully cloned: ${params.repo}`, 268 - ), 269 - ) 270 - .catch((e) => consola.error(`Failed to Clone Repository: ${e}`)), 271 - ); 272 - } 273 - 274 - const baseSandbox = await tx 275 - .select() 276 - .from(sandboxes) 277 - .where(eq(sandboxes.name, params.base)) 278 - .execute() 279 - .then((rows) => rows[0]); 280 - 281 - await tx 282 - .update(sandboxes) 283 - .set({ installs: (baseSandbox?.installs || 0) + 1 }) 284 - .where(eq(sandboxes.name, params.base)) 285 - .execute(); 286 - 287 - return record; 288 - }); 289 - 290 - return c.json(record); 291 - } catch (err) { 292 - console.log(err); 293 - return c.json( 294 - { error: err instanceof Error ? err.message : "Unknown error" }, 295 - 400, 296 - ); 297 - } 298 - }); 299 - 300 - app.get("/v1/sandboxes/:sandboxId", async (c) => { 301 - const { sandboxes: record } = await getSandboxById( 302 - c.var.db, 303 - c.req.param("sandboxId"), 304 - ); 305 - return c.json(record); 306 - }); 307 - 308 - app.put("/v1/sandboxes/:sandboxId", async (c) => { 309 - return c.json({}); 310 - }); 311 - 312 - app.get("/v1/sandboxes", async (c) => { 313 - const records = await c.var.db.select().from(sandboxes).execute(); 314 - return c.json(records); 315 - }); 316 - 317 - app.post("/v1/sandboxes/:sandboxId/start", async (c) => { 318 - const { sandboxes: record } = await getSandboxById( 319 - c.var.db, 320 - c.req.param("sandboxId"), 321 - ); 322 - 323 - const body = await c.req.json<StartSandboxConfig>(); 324 - const { repo, keepAlive } = StartSandboxConfigSchema.parse(body); 325 - 326 - if (!record) { 327 - return c.json({ error: "Sandbox not found" }, 404); 328 - } 329 - 330 - const sandboxId = Array.from( 331 - crypto.getRandomValues(new Uint8Array(16)), 332 - (b) => b.toString(16).padStart(2, "0"), 333 - ).join(""); 334 - 335 - await c.var.db 336 - .update(sandboxes) 337 - .set({ sandboxId: record.sandboxId || sandboxId }) 338 - .where( 339 - and( 340 - or( 341 - eq(sandboxes.id, c.req.param("sandboxId")), 342 - eq(sandboxes.sandboxId, c.req.param("sandboxId")), 343 - eq(sandboxes.name, c.req.param("sandboxId")), 344 - ), 345 - isNull(sandboxes.sandboxId), 346 - eq(sandboxes.provider, "cloudflare"), 347 - ), 348 - ) 349 - .returning() 350 - .execute(); 351 - 352 - let sandbox: BaseSandbox | null = null; 353 - 354 - if (record.provider !== "cloudflare") { 355 - return c.json({ error: "Sandbox provider not supported" }, 400); 356 - } 357 - 358 - try { 359 - sandbox = await createSandbox("cloudflare", { 360 - id: record.sandboxId || sandboxId, 361 - memory: "4GiB", 362 - keepAlive, 363 - }); 364 - 365 - if (!sandbox) { 366 - return c.json({ error: "Sandbox provider not supported" }, 400); 367 - } 368 - 369 - if (repo) { 370 - c.executionCtx.waitUntil( 371 - sandbox 372 - .clone(repo) 373 - .then(() => 374 - consola.success(`Git Repository successfully cloned: ${repo}`), 375 - ) 376 - .catch((e) => consola.error(`Failed to Clone Repository: ${e}`)), 377 - ); 378 - } 36 + `), 37 + ); 379 38 380 - await c.var.db 381 - .update(sandboxes) 382 - .set({ 383 - status: "RUNNING", 384 - sandboxId: record.sandboxId || sandboxId, 385 - startedAt: new Date(), 386 - }) 387 - .where(eq(sandboxes.id, c.req.param("sandboxId"))) 388 - .execute(); 389 - 390 - const params = await Promise.all([ 391 - c.var.db 392 - .select() 393 - .from(sandboxVariables) 394 - .leftJoin(variables, eq(variables.id, sandboxVariables.variableId)) 395 - .where(eq(sandboxVariables.sandboxId, c.req.param("sandboxId"))) 396 - .execute(), 397 - c.var.db 398 - .select() 399 - .from(sandboxSecrets) 400 - .leftJoin(secrets, eq(secrets.id, sandboxSecrets.secretId)) 401 - .where(eq(sandboxSecrets.sandboxId, c.req.param("sandboxId"))) 402 - .execute(), 403 - c.var.db 404 - .select() 405 - .from(sandboxFiles) 406 - .leftJoin(files, eq(files.id, sandboxFiles.fileId)) 407 - .where(eq(sandboxFiles.sandboxId, c.req.param("sandboxId"))) 408 - .execute(), 409 - c.var.db 410 - .select() 411 - .from(sshKeys) 412 - .where(eq(sshKeys.sandboxId, c.req.param("sandboxId"))) 413 - .execute(), 414 - c.var.db 415 - .select() 416 - .from(tailscaleAuthKeys) 417 - .where(eq(tailscaleAuthKeys.sandboxId, c.req.param("sandboxId"))) 418 - .execute(), 419 - c.var.db 420 - .select() 421 - .from(sandboxVolumes) 422 - .leftJoin(sandboxes, eq(sandboxVolumes.sandboxId, sandboxes.id)) 423 - .leftJoin(users, eq(sandboxes.userId, users.id)) 424 - .where(eq(sandboxVolumes.sandboxId, c.req.param("sandboxId"))) 425 - .execute(), 426 - c.var.db 427 - .select() 428 - .from(sandboxPorts) 429 - .leftJoin(sandboxes, eq(sandboxPorts.sandboxId, sandboxes.id)) 430 - .leftJoin(users, eq(sandboxes.userId, users.id)) 431 - .where(eq(sandboxPorts.sandboxId, c.req.param("sandboxId"))) 432 - .execute(), 433 - c.var.db 434 - .select() 435 - .from(services) 436 - .where(eq(services.sandboxId, c.req.param("sandboxId"))) 437 - .execute(), 438 - ]); 439 - 440 - c.executionCtx.waitUntil( 441 - sandbox.setEnvs({ 442 - ...params[0] 443 - .map(({ variables }) => variables) 444 - .filter((v) => v !== null) 445 - .reduce( 446 - (acc, v) => { 447 - acc[v.name] = v.value; 448 - return acc; 449 - }, 450 - {} as Record<string, string>, 451 - ), 452 - ...Object.fromEntries( 453 - await Promise.all( 454 - params[1] 455 - .map(({ secrets }) => secrets) 456 - .filter((v) => v !== null) 457 - .map(async (v) => [v.name, await decrypt(v.value)] as const), 458 - ), 459 - ), 460 - }), 461 - ); 462 - 463 - c.executionCtx.waitUntil( 464 - sandbox.sh`[ -f /root/.ssh/id_ed25519 ] || ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -q -N "" || true`, 465 - ); 466 - 467 - const { hostname } = new URL(c.req.url); 468 - 469 - c.executionCtx.waitUntil( 470 - Promise.all([ 471 - ...params[2] 472 - .filter((x) => x.files !== null) 473 - .map(async (record) => 474 - sandbox?.writeFile( 475 - record.sandbox_files.path, 476 - await decrypt(record.files!.content), 477 - ), 478 - ), 479 - ...params[3].map(async (record) => 480 - sandbox?.setupSshKeys( 481 - await decrypt(record.privateKey), 482 - record.publicKey, 483 - ), 484 - ), 485 - params[4].length > 0 && 486 - sandbox?.setupTailscale(await decrypt(params[4][0].authKey)), 487 - ...params[5].map((volume) => 488 - sandbox?.mount( 489 - volume.sandbox_volumes.path, 490 - `/${volume.users?.did || ""}${volume.users?.did ? "/" : ""}${volume.sandbox_volumes.id}/`, 491 - ), 492 - ), 493 - ]), 494 - ); 495 - 496 - if (record.repo) { 497 - c.executionCtx.waitUntil( 498 - sandbox 499 - .clone(record.repo) 500 - .then(() => 501 - consola.success( 502 - `Git Repository successfully cloned: ${record.repo}`, 503 - ), 504 - ) 505 - .catch((e) => consola.error(`Failed to Clone Repository: ${e}`)), 506 - ); 507 - } 508 - 509 - const previewUrls = await Promise.all( 510 - params[6].map((port) => 511 - sandbox?.expose(port.sandbox_ports.exposedPort, hostname), 512 - ), 513 - ); 514 - 515 - await Promise.all( 516 - previewUrls.map((url, i) => { 517 - if (url) { 518 - return c.var.db 519 - .update(sandboxPorts) 520 - .set({ previewUrl: url }) 521 - .where( 522 - and( 523 - eq(sandboxPorts.sandboxId, record.id), 524 - eq( 525 - sandboxPorts.exposedPort, 526 - params[6][i].sandbox_ports.exposedPort, 527 - ), 528 - ), 529 - ) 530 - .execute(); 531 - } 532 - }), 533 - ); 534 - 535 - c.executionCtx.waitUntil( 536 - Promise.all( 537 - params[7].map(async (service) => { 538 - const id = await sandbox?.startService(service.command); 539 - if (id) { 540 - await c.var.db 541 - .update(services) 542 - .set({ serviceId: id, status: "RUNNING" }) 543 - .where(eq(services.id, service.id)) 544 - .execute(); 545 - } 546 - }), 547 - ), 548 - ); 549 - } catch (err) { 550 - const errorMessage = err instanceof Error ? err.message : "Unknown error"; 551 - consola.log("Failed to start sandbox:", errorMessage); 552 - return c.json({ error: `Failed to start sandbox: ${errorMessage}` }, 500); 553 - } 554 - return c.json({}); 555 - }); 556 - 557 - app.post("/v1/sandboxes/:sandboxId/stop", async (c) => { 558 - const { sandboxes: record } = await getSandboxById( 559 - c.var.db, 560 - c.req.param("sandboxId"), 561 - ); 562 - 563 - if (!record) { 564 - return c.json({ error: "Sandbox not found" }, 404); 565 - } 566 - 567 - if (record.provider !== "cloudflare") { 568 - return c.json({ error: "Sandbox provider not supported" }, 400); 569 - } 570 - 571 - try { 572 - let sandbox: BaseSandbox | null = null; 573 - 574 - if (!record.sandboxId) { 575 - return c.json({ error: "Sandbox is not running" }, 400); 576 - } 577 - 578 - sandbox = await createSandbox("cloudflare", { 579 - id: record.sandboxId, 580 - }); 581 - 582 - if (!sandbox) { 583 - return c.json({ error: "Sandbox provider not supported" }, 400); 584 - } 585 - const volumes = await c.var.db 586 - .select() 587 - .from(sandboxVolumes) 588 - .where(eq(sandboxVolumes.sandboxId, c.req.param("sandboxId"))) 589 - .execute(); 590 - 591 - try { 592 - await Promise.all(volumes.map((volume) => sandbox?.unmount(volume.path))); 593 - } catch (e) { 594 - console.error(e); 595 - } 596 - 597 - await sandbox.stop(); 598 - await Promise.all([ 599 - c.var.db 600 - .update(sandboxes) 601 - .set({ status: "STOPPED" }) 602 - .where(eq(sandboxes.id, c.req.param("sandboxId"))) 603 - .execute(), 604 - c.var.db 605 - .update(services) 606 - .set({ status: "STOPPED" }) 607 - .where(eq(services.sandboxId, c.req.param("sandboxId"))) 608 - .execute(), 609 - ]); 610 - 611 - return c.json({}); 612 - } catch (err) { 613 - const errorMessage = err instanceof Error ? err.message : "Unknown error"; 614 - consola.error("Failed to stop sandbox:", errorMessage); 615 - return c.json({ error: `Failed to stop sandbox: ${errorMessage}` }, 500); 616 - } 617 - }); 618 - 619 - app.post("/v1/sandboxes/:sandboxId/runs", async (c) => { 620 - const { sandboxes: record } = await getSandboxById( 621 - c.var.db, 622 - c.req.param("sandboxId"), 623 - ); 624 - 625 - if (!record) { 626 - return c.json({ error: "Sandbox not found" }, 404); 627 - } 628 - 629 - if (record.provider !== "cloudflare") { 630 - return c.json({ error: "Sandbox provider not supported" }, 400); 631 - } 632 - 633 - if (record.status !== "RUNNING") { 634 - return c.json({ error: "Sandbox is not running" }, 400); 635 - } 636 - 637 - try { 638 - let sandbox: BaseSandbox | null = null; 639 - 640 - sandbox = await createSandbox("cloudflare", { 641 - id: record.sandboxId!, 642 - }); 643 - 644 - const { command } = await c.req.json(); 645 - const res = await sandbox.sh`${command}`; 646 - return c.json(res); 647 - } catch (err) { 648 - const errorMessage = err instanceof Error ? err.message : "Unknown error"; 649 - consola.error("Failed to run command in sandbox:", errorMessage); 650 - return c.json({ error: `Failed to run command: ${errorMessage}` }, 500); 651 - } 652 - }); 653 - 654 - app.delete("/v1/sandboxes/:sandboxId", async (c) => { 655 - const { sandboxes: record } = await getSandboxById( 656 - c.var.db, 657 - c.req.param("sandboxId"), 658 - ); 659 - 660 - if (!record) { 661 - return c.json({ error: "Sandbox not found" }, 404); 662 - } 663 - 664 - if (record.provider !== "cloudflare") { 665 - return c.json({ error: "Sandbox provider not supported" }, 400); 666 - } 667 - 668 - try { 669 - let sandbox: BaseSandbox | null = null; 670 - 671 - sandbox = await createSandbox("cloudflare", { 672 - id: record.sandboxId!, 673 - }); 674 - 675 - await sandbox.delete(); 676 - 677 - await c.var.db 678 - .delete(sandboxes) 679 - .where( 680 - or( 681 - eq(sandboxes.id, c.req.param("sandboxId")), 682 - eq(sandboxes.name, c.req.param("sandboxId")), 683 - ), 684 - ) 685 - .execute(); 686 - 687 - return c.json({ success: true }, 200); 688 - } catch (err) { 689 - const errorMessage = err instanceof Error ? err.message : "Unknown error"; 690 - consola.error("Failed to delete sandbox:", errorMessage); 691 - return c.json({ error: `Failed to delete sandbox: ${errorMessage}` }, 500); 692 - } 693 - }); 694 - 695 - app.get("/v1/sandboxes/:sandboxId/ws/terminal", async (c) => { 696 - if (c.req.header("upgrade")?.toLowerCase() !== "websocket") { 697 - return c.text("Expected WebSocket connection", 426); 698 - } 699 - const token = c.req.query("t"); 700 - 701 - const { sandboxes: record, users: user } = await getSandboxById( 702 - c.var.db, 703 - c.req.param("sandboxId"), 704 - ); 705 - if (!record) { 706 - return c.text("Sandbox not found", 404); 707 - } 708 - 709 - if (token) { 710 - const decoded = await jwt.verify(token, process.env.JWT_SECRET!); 711 - if (record.userId && user && user?.did !== decoded?.payload?.sub) { 712 - return c.text("Unauthorized", 403); 713 - } 714 - } 715 - 716 - if (!record.sandboxId) { 717 - return c.text("Sandbox not started", 400); 718 - } 719 - const sandbox = getSandbox(c.env.Sandbox, record.sandboxId); 720 - await sandbox.start(); 721 - 722 - const sessionId = c.req.query("session"); 723 - 724 - try { 725 - if (sessionId) { 726 - const session = await sandbox.getSession(sessionId); 727 - return session.terminal(c.req.raw); 728 - } 729 - 730 - return sandbox.terminal(c.req.raw); 731 - } catch (err) { 732 - console.log(err); 733 - return c.text("Failed to connect to sandbox", 500); 734 - } 735 - }); 736 - 737 - app.post("/v1/sandboxes/:sandboxId/ports", async (c) => { 738 - const { sandboxes: record } = await getSandboxById( 739 - c.var.db, 740 - c.req.param("sandboxId"), 741 - ); 742 - 743 - if (!record) { 744 - return c.json({ error: "Sandbox not found" }, 404); 745 - } 746 - 747 - if (record.provider !== "cloudflare") { 748 - return c.json({ error: "Sandbox provider not supported" }, 400); 749 - } 750 - 751 - try { 752 - let sandbox: BaseSandbox | null = null; 753 - 754 - sandbox = await createSandbox("cloudflare", { 755 - id: record.sandboxId!, 756 - }); 757 - 758 - const { port } = await c.req.json<{ port: number }>(); 759 - 760 - if (!port || port < 1025 || port > 65535 || port == 3000) { 761 - return c.json({ error: "Invalid port number" }, 400); 762 - } 763 - 764 - const { hostname } = new URL(c.req.url); 765 - const previewUrl = await sandbox.expose(port, hostname); 766 - return c.json({ previewUrl }); 767 - } catch (err) { 768 - const errorMessage = err instanceof Error ? err.message : "Unknown error"; 769 - consola.error( 770 - c.req.param("sandboxId"), 771 - "Failed to expose port:", 772 - errorMessage, 773 - ); 774 - return c.json({ error: `Failed to expose port: ${errorMessage}` }, 500); 775 - } 776 - }); 777 - 778 - app.delete("/v1/sandboxes/:sandboxId/ports", async (c) => { 779 - const { sandboxes: record } = await getSandboxById( 780 - c.var.db, 781 - c.req.param("sandboxId"), 782 - ); 783 - 784 - if (!record) { 785 - return c.json({ error: "Sandbox not found" }, 404); 786 - } 787 - 788 - if (record.provider !== "cloudflare") { 789 - return c.json({ error: "Sandbox provider not supported" }, 400); 790 - } 791 - 792 - try { 793 - let sandbox: BaseSandbox | null = null; 794 - 795 - sandbox = await createSandbox("cloudflare", { 796 - id: record.sandboxId!, 797 - }); 798 - 799 - const port = parseInt(c.req.query("port") || "0", 10); 800 - 801 - if (!port || port < 1024 || port > 65535 || port == 3000) { 802 - return c.json({ error: "Invalid port number" }, 400); 803 - } 804 - 805 - await sandbox.unexpose(port); 806 - return c.json({}); 807 - } catch (err) { 808 - const errorMessage = err instanceof Error ? err.message : "Unknown error"; 809 - consola.error( 810 - c.req.param("sandboxId"), 811 - "Failed to unexpose port:", 812 - errorMessage, 813 - ); 814 - return c.json({ error: `Failed to unexpose port: ${errorMessage}` }, 500); 815 - } 816 - }); 817 - 818 - app.post("/v1/sandboxes/:sandboxId/vscode", async (c) => { 819 - const { sandboxes: record } = await getSandboxById( 820 - c.var.db, 821 - c.req.param("sandboxId"), 822 - ); 823 - 824 - if (!record) { 825 - return c.json({ error: "Sandbox not found" }, 404); 826 - } 827 - 828 - if (record.provider !== "cloudflare") { 829 - return c.json({ error: "Sandbox provider not supported" }, 400); 830 - } 831 - 832 - try { 833 - let sandbox: BaseSandbox | null = null; 834 - 835 - sandbox = await createSandbox("cloudflare", { 836 - id: record.sandboxId!, 837 - }); 838 - 839 - const { hostname } = new URL(c.req.url); 840 - const previewUrl = await sandbox.exposeVscode(hostname); 841 - return c.json({ previewUrl }); 842 - } catch (err) { 843 - const errorMessage = err instanceof Error ? err.message : "Unknown error"; 844 - consola.error( 845 - c.req.param("sandboxId"), 846 - "Failed to expose vscode:", 847 - errorMessage, 848 - ); 849 - return c.json({ error: `Failed to expose vscode: ${errorMessage}` }, 500); 850 - } 851 - }); 852 - 853 - app.delete("/v1/sandboxes/:sandboxId/vscode", async (c) => { 854 - const { sandboxes: record } = await getSandboxById( 855 - c.var.db, 856 - c.req.param("sandboxId"), 857 - ); 858 - 859 - if (!record) { 860 - return c.json({ error: "Sandbox not found" }, 404); 861 - } 862 - 863 - if (record.provider !== "cloudflare") { 864 - return c.json({ error: "Sandbox provider not supported" }, 400); 865 - } 866 - try { 867 - let sandbox: BaseSandbox | null = null; 868 - 869 - sandbox = await createSandbox("cloudflare", { 870 - id: record.sandboxId!, 871 - }); 872 - 873 - await sandbox.unexposeVscode(); 874 - return c.json({}); 875 - } catch (err) { 876 - const errorMessage = err instanceof Error ? err.message : "Unknown error"; 877 - consola.error( 878 - c.req.param("sandboxId"), 879 - "Failed to unexpose vscode:", 880 - errorMessage, 881 - ); 882 - return c.json({ error: `Failed to unexpose vscode: ${errorMessage}` }, 500); 883 - } 884 - }); 885 - 886 - app.post("/v1/sandboxes/:sandboxId/services/:serviceId", async (c) => { 887 - const { sandboxes: record } = await getSandboxById( 888 - c.var.db, 889 - c.req.param("sandboxId"), 890 - ); 891 - 892 - if (!record) { 893 - return c.json({ error: "Sandbox not found" }, 404); 894 - } 895 - 896 - if (record.provider !== "cloudflare") { 897 - return c.json({ error: "Sandbox provider not supported" }, 400); 898 - } 899 - 900 - try { 901 - let sandbox: BaseSandbox | null = null; 902 - 903 - sandbox = await createSandbox("cloudflare", { 904 - id: record.sandboxId!, 905 - }); 906 - 907 - const [service] = await c.var.db 908 - .select() 909 - .from(services) 910 - .where( 911 - and( 912 - eq(services.id, c.req.param("serviceId")), 913 - eq(services.sandboxId, record.id), 914 - ), 915 - ) 916 - .execute(); 917 - 918 - if (!service) { 919 - return c.json({ error: "Service not found" }, 404); 920 - } 921 - 922 - if (service.status === "RUNNING") { 923 - return c.json({}); 924 - } 925 - 926 - const serviceId = await sandbox.startService(service.command); 927 - 928 - await c.var.db 929 - .update(services) 930 - .set({ serviceId, status: "RUNNING" }) 931 - .where(eq(services.id, service.id)) 932 - .execute(); 933 - 934 - return c.json({ serviceId }); 935 - } catch (err) { 936 - console.log(`Failed to start service:`, err); 937 - } 938 - 939 - return c.json({}); 940 - }); 941 - 942 - app.delete("/v1/sandboxes/:sandboxId/services/:serviceId", async (c) => { 943 - const { sandboxes: record } = await getSandboxById( 944 - c.var.db, 945 - c.req.param("sandboxId"), 946 - ); 947 - 948 - if (!record) { 949 - return c.json({ error: "Sandbox not found" }, 404); 950 - } 951 - 952 - if (record.provider !== "cloudflare") { 953 - return c.json({ error: "Sandbox provider not supported" }, 400); 954 - } 955 - 956 - try { 957 - let sandbox: BaseSandbox | null = null; 958 - 959 - sandbox = await createSandbox("cloudflare", { 960 - id: record.sandboxId!, 961 - }); 962 - 963 - const [service] = await c.var.db 964 - .select() 965 - .from(services) 966 - .where( 967 - and( 968 - eq(services.id, c.req.param("serviceId")), 969 - eq(services.sandboxId, record.id), 970 - ), 971 - ) 972 - .execute(); 973 - 974 - if (!service) { 975 - return c.json({ error: "Service not found" }, 404); 976 - } 977 - 978 - if (service.status !== "RUNNING" || !service.serviceId) { 979 - return c.json({}); 980 - } 981 - 982 - await sandbox.stopService(service.serviceId!); 983 - 984 - await c.var.db 985 - .update(services) 986 - .set({ status: "STOPPED" }) 987 - .where(eq(services.id, service.id)) 988 - .execute(); 989 - } catch (err) { 990 - console.log(`Failed to stop service:`, err); 991 - } 992 - return c.json({}); 993 - }); 994 - 995 - app.post("/v1/sandboxes/:sandboxId/pull-directory", async (c) => { 996 - const { sandboxes: record } = await getSandboxById( 997 - c.var.db, 998 - c.req.param("sandboxId"), 999 - ); 1000 - 1001 - if (!record) { 1002 - return c.json({ error: "Sandbox not found" }, 404); 1003 - } 1004 - 1005 - if (record.provider !== "cloudflare") { 1006 - return c.json({ error: "Sandbox provider not supported" }, 400); 1007 - } 1008 - 1009 - const token = c.req.header("Authorization"); 1010 - const params = await c.req.json<PullDirectoryParams>(); 1011 - await pullSchema.parseAsync(params); 1012 - 1013 - const outdir = crypto.randomUUID(); 1014 - 1015 - let sandbox: BaseSandbox | null = null; 1016 - 1017 - sandbox = await createSandbox("cloudflare", { 1018 - id: record.sandboxId!, 1019 - }); 1020 - await sandbox.sh`mkdir -p /tmp/${outdir} && cd /tmp/${outdir} && curl https://sandbox.pocketenv.io/cp/${params.uuid} -H "Authorization: ${token}" --output - | tar xzvf -`; 1021 - await sandbox.sh`mkdir -p ${params.directoryPath} || sudo mkdir -p ${params.directoryPath}`; 1022 - await sandbox.sh`(shopt -s dotglob && cp -r /tmp/${outdir}/* ${params.directoryPath}) || (shopt -s dotglob && sudo cp -r /tmp/${outdir}/* ${params.directoryPath})`; 1023 - 1024 - return c.json({ success: true }); 1025 - }); 1026 - 1027 - app.post("/v1/sandboxes/:sandboxId/push-directory", async (c) => { 1028 - const { sandboxes: record } = await getSandboxById( 1029 - c.var.db, 1030 - c.req.param("sandboxId"), 1031 - ); 1032 - 1033 - if (!record) { 1034 - return c.json({ error: "Sandbox not found" }, 404); 1035 - } 1036 - 1037 - if (record.provider !== "cloudflare") { 1038 - return c.json({ error: "Sandbox provider not supported" }, 400); 1039 - } 1040 - 1041 - const params = await c.req.json<PushDirectoryParams>(); 1042 - await pushSchema.parseAsync(params); 1043 - 1044 - const token = c.req.header("Authorization"); 1045 - await pushSchema.parseAsync(params); 1046 - const uuid = crypto.randomUUID(); 1047 - 1048 - let sandbox: BaseSandbox | null = null; 1049 - 1050 - sandbox = await createSandbox("cloudflare", { 1051 - id: record.sandboxId!, 1052 - }); 1053 - 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`; 1054 - 1055 - return c.json({ uuid }); 1056 - }); 1057 - 1058 - export const getSandboxById = async (db: Context["db"], sandboxId: string) => { 1059 - const [record] = await db 1060 - .select() 1061 - .from(sandboxes) 1062 - .leftJoin(users, eq(sandboxes.userId, users.id)) 1063 - .where( 1064 - or( 1065 - eq(sandboxes.id, sandboxId), 1066 - eq(sandboxes.sandboxId, sandboxId), 1067 - eq(sandboxes.name, sandboxId), 1068 - ), 1069 - ) 1070 - .execute(); 1071 - 1072 - return record; 1073 - }; 1074 - 1075 - export const saveSecrets = async ( 1076 - tx: PgTransaction< 1077 - NodePgQueryResultHKT, 1078 - Record<string, never>, 1079 - ExtractTablesWithRelations<Record<string, never>> 1080 - >, 1081 - sandbox: SelectSandbox, 1082 - values: { secrets: { name: string; value: string }[] }, 1083 - ) => { 1084 - const insertedSecrets = await tx 1085 - .insert(secrets) 1086 - .values(values.secrets) 1087 - .returning() 1088 - .execute(); 1089 - 1090 - await tx 1091 - .insert(sandboxSecrets) 1092 - .values( 1093 - insertedSecrets.map((secret) => ({ 1094 - sandboxId: sandbox.id, 1095 - secretId: secret.id, 1096 - })), 1097 - ) 1098 - .execute(); 1099 - }; 1100 - 1101 - export const saveVariables = async ( 1102 - tx: PgTransaction< 1103 - NodePgQueryResultHKT, 1104 - Record<string, never>, 1105 - ExtractTablesWithRelations<Record<string, never>> 1106 - >, 1107 - sandbox: SelectSandbox, 1108 - values: { variables: { name: string; value: string }[] }, 1109 - ) => { 1110 - const insertedVariables = await tx 1111 - .insert(variables) 1112 - .values(values.variables) 1113 - .returning() 1114 - .execute(); 1115 - 1116 - await tx 1117 - .insert(sandboxVariables) 1118 - .values( 1119 - insertedVariables.map((variable) => ({ 1120 - sandboxId: sandbox.id, 1121 - variableId: variable.id, 1122 - name: variable.name, 1123 - })), 1124 - ) 1125 - .execute(); 1126 - }; 39 + app.route("/", cpRoutes); 40 + app.route("/", sandboxRoutes); 41 + app.route("/", terminalRoutes); 1127 42 1128 43 export { Sandbox } from "@cloudflare/sandbox"; 1129 44
+159
apps/cf-sandbox/src/lib/sandbox-helpers.ts
··· 1 + import { 2 + and, 3 + eq, 4 + ExtractTablesWithRelations, 5 + isNull, 6 + or, 7 + } from "drizzle-orm"; 8 + import { Context } from "../context"; 9 + import { 10 + sandboxes, 11 + sandboxSecrets, 12 + sandboxVariables, 13 + secrets, 14 + users, 15 + variables, 16 + } from "../schema"; 17 + import { BaseSandbox, createSandbox } from "../providers"; 18 + import { SelectSandbox } from "../schema/sandboxes"; 19 + import { SelectUser } from "../schema/users"; 20 + import crypto from "node:crypto"; 21 + import { consola } from "consola"; 22 + import { PgTransaction } from "drizzle-orm/pg-core"; 23 + import { NodePgQueryResultHKT } from "drizzle-orm/node-postgres"; 24 + 25 + export type SandboxRecord = { 26 + sandbox: SelectSandbox; 27 + user: SelectUser | null; 28 + }; 29 + 30 + /** Look up a sandbox by id/sandboxId/name, returning the record and its owner. */ 31 + export async function getSandboxRecord( 32 + db: Context["db"], 33 + sandboxId: string, 34 + ): Promise<SandboxRecord | undefined> { 35 + const [row] = await db 36 + .select() 37 + .from(sandboxes) 38 + .leftJoin(users, eq(sandboxes.userId, users.id)) 39 + .where( 40 + or( 41 + eq(sandboxes.id, sandboxId), 42 + eq(sandboxes.sandboxId, sandboxId), 43 + eq(sandboxes.name, sandboxId), 44 + ), 45 + ) 46 + .execute(); 47 + if (!row) return undefined; 48 + return { sandbox: row.sandboxes, user: row.users }; 49 + } 50 + 51 + export async function getCloudflareInstance( 52 + sandboxId: string, 53 + opts?: { memory?: string; keepAlive?: boolean; sleepAfter?: string }, 54 + ): Promise<BaseSandbox> { 55 + return createSandbox("cloudflare", { id: sandboxId, ...opts }); 56 + } 57 + 58 + export function generateSandboxId(): string { 59 + return Array.from( 60 + crypto.getRandomValues(new Uint8Array(16)), 61 + (b) => b.toString(16).padStart(2, "0"), 62 + ).join(""); 63 + } 64 + 65 + export function scheduleRepoClone( 66 + ctx: { waitUntil: (p: Promise<unknown>) => void }, 67 + sandbox: BaseSandbox, 68 + repo: string, 69 + ): void { 70 + ctx.waitUntil( 71 + sandbox 72 + .clone(repo) 73 + .then(() => 74 + consola.success(`Git Repository successfully cloned: ${repo}`), 75 + ) 76 + .catch((e) => consola.error(`Failed to Clone Repository: ${e}`)), 77 + ); 78 + } 79 + 80 + export function toErrorMessage(err: unknown): string { 81 + return err instanceof Error ? err.message : "Unknown error"; 82 + } 83 + 84 + /** 85 + * Assigns a sandboxId to a sandbox record that doesn't have one yet. 86 + * No-op when the record already has a sandboxId. 87 + */ 88 + export async function ensureSandboxId( 89 + db: Context["db"], 90 + routeId: string, 91 + sandboxId: string, 92 + ): Promise<void> { 93 + await db 94 + .update(sandboxes) 95 + .set({ sandboxId }) 96 + .where( 97 + and( 98 + or( 99 + eq(sandboxes.id, routeId), 100 + eq(sandboxes.sandboxId, routeId), 101 + eq(sandboxes.name, routeId), 102 + ), 103 + isNull(sandboxes.sandboxId), 104 + eq(sandboxes.provider, "cloudflare"), 105 + ), 106 + ) 107 + .execute(); 108 + } 109 + 110 + type Tx = PgTransaction< 111 + NodePgQueryResultHKT, 112 + Record<string, never>, 113 + ExtractTablesWithRelations<Record<string, never>> 114 + >; 115 + 116 + export async function saveSecrets( 117 + tx: Tx, 118 + sandbox: SelectSandbox, 119 + values: { secrets: { name: string; value: string }[] }, 120 + ): Promise<void> { 121 + const insertedSecrets = await tx 122 + .insert(secrets) 123 + .values(values.secrets) 124 + .returning() 125 + .execute(); 126 + 127 + await tx 128 + .insert(sandboxSecrets) 129 + .values( 130 + insertedSecrets.map((secret) => ({ 131 + sandboxId: sandbox.id, 132 + secretId: secret.id, 133 + })), 134 + ) 135 + .execute(); 136 + } 137 + 138 + export async function saveVariables( 139 + tx: Tx, 140 + sandbox: SelectSandbox, 141 + values: { variables: { name: string; value: string }[] }, 142 + ): Promise<void> { 143 + const insertedVariables = await tx 144 + .insert(variables) 145 + .values(values.variables) 146 + .returning() 147 + .execute(); 148 + 149 + await tx 150 + .insert(sandboxVariables) 151 + .values( 152 + insertedVariables.map((variable) => ({ 153 + sandboxId: sandbox.id, 154 + variableId: variable.id, 155 + name: variable.name, 156 + })), 157 + ) 158 + .execute(); 159 + }
+194
apps/cf-sandbox/src/lib/sandbox-resources.ts
··· 1 + import { and, eq } from "drizzle-orm"; 2 + import { Context } from "../context"; 3 + import { 4 + files, 5 + sandboxFiles, 6 + sandboxPorts, 7 + sandboxSecrets, 8 + sandboxVariables, 9 + sandboxVolumes, 10 + secrets, 11 + sshKeys, 12 + tailscaleAuthKeys, 13 + users, 14 + variables, 15 + } from "../schema"; 16 + import sandboxes from "../schema/sandboxes"; 17 + import services from "../schema/services"; 18 + import { BaseSandbox } from "../providers"; 19 + import { SelectSandbox } from "../schema/sandboxes"; 20 + import decrypt from "./decrypt"; 21 + 22 + export async function fetchSandboxResources( 23 + db: Context["db"], 24 + sandboxId: string, 25 + ) { 26 + const [vars, secs, fils, sshKeyList, tailscaleKeys, volumes, ports, serviceList] = 27 + await Promise.all([ 28 + db 29 + .select() 30 + .from(sandboxVariables) 31 + .leftJoin(variables, eq(variables.id, sandboxVariables.variableId)) 32 + .where(eq(sandboxVariables.sandboxId, sandboxId)) 33 + .execute(), 34 + db 35 + .select() 36 + .from(sandboxSecrets) 37 + .leftJoin(secrets, eq(secrets.id, sandboxSecrets.secretId)) 38 + .where(eq(sandboxSecrets.sandboxId, sandboxId)) 39 + .execute(), 40 + db 41 + .select() 42 + .from(sandboxFiles) 43 + .leftJoin(files, eq(files.id, sandboxFiles.fileId)) 44 + .where(eq(sandboxFiles.sandboxId, sandboxId)) 45 + .execute(), 46 + db 47 + .select() 48 + .from(sshKeys) 49 + .where(eq(sshKeys.sandboxId, sandboxId)) 50 + .execute(), 51 + db 52 + .select() 53 + .from(tailscaleAuthKeys) 54 + .where(eq(tailscaleAuthKeys.sandboxId, sandboxId)) 55 + .execute(), 56 + db 57 + .select() 58 + .from(sandboxVolumes) 59 + .leftJoin(sandboxes, eq(sandboxVolumes.sandboxId, sandboxes.id)) 60 + .leftJoin(users, eq(sandboxes.userId, users.id)) 61 + .where(eq(sandboxVolumes.sandboxId, sandboxId)) 62 + .execute(), 63 + db 64 + .select() 65 + .from(sandboxPorts) 66 + .leftJoin(sandboxes, eq(sandboxPorts.sandboxId, sandboxes.id)) 67 + .leftJoin(users, eq(sandboxes.userId, users.id)) 68 + .where(eq(sandboxPorts.sandboxId, sandboxId)) 69 + .execute(), 70 + db 71 + .select() 72 + .from(services) 73 + .where(eq(services.sandboxId, sandboxId)) 74 + .execute(), 75 + ]); 76 + 77 + return { vars, secs, fils, sshKeyList, tailscaleKeys, volumes, ports, serviceList }; 78 + } 79 + 80 + export type SandboxResources = Awaited<ReturnType<typeof fetchSandboxResources>>; 81 + 82 + export async function buildSandboxEnvs( 83 + resources: Pick<SandboxResources, "vars" | "secs">, 84 + ): Promise<Record<string, string>> { 85 + return { 86 + ...resources.vars 87 + .map(({ variables: v }) => v) 88 + .filter((v) => v !== null) 89 + .reduce( 90 + (acc, v) => { 91 + acc[v.name] = v.value; 92 + return acc; 93 + }, 94 + {} as Record<string, string>, 95 + ), 96 + ...Object.fromEntries( 97 + await Promise.all( 98 + resources.secs 99 + .map(({ secrets: s }) => s) 100 + .filter((s) => s !== null) 101 + .map(async (s) => [s.name, await decrypt(s.value)] as const), 102 + ), 103 + ), 104 + }; 105 + } 106 + 107 + /** Write files, set up SSH keys, configure Tailscale, and mount volumes. */ 108 + export async function scheduleInfraSetup( 109 + ctx: { waitUntil: (p: Promise<unknown>) => void }, 110 + sandbox: BaseSandbox, 111 + resources: Pick<SandboxResources, "fils" | "sshKeyList" | "tailscaleKeys" | "volumes">, 112 + ): Promise<void> { 113 + ctx.waitUntil( 114 + Promise.all([ 115 + ...resources.fils 116 + .filter((x) => x.files !== null) 117 + .map(async (record) => 118 + sandbox.writeFile( 119 + record.sandbox_files.path, 120 + await decrypt(record.files!.content), 121 + ), 122 + ), 123 + ...resources.sshKeyList.map(async (record) => 124 + sandbox.setupSshKeys( 125 + await decrypt(record.privateKey), 126 + record.publicKey, 127 + ), 128 + ), 129 + resources.tailscaleKeys.length > 0 && 130 + sandbox.setupTailscale(await decrypt(resources.tailscaleKeys[0].authKey)), 131 + ...resources.volumes.map((volume) => 132 + sandbox.mount( 133 + volume.sandbox_volumes.path, 134 + `/${volume.users?.did || ""}${volume.users?.did ? "/" : ""}${volume.sandbox_volumes.id}/`, 135 + ), 136 + ), 137 + ]), 138 + ); 139 + } 140 + 141 + /** Expose sandbox ports and persist the resulting preview URLs to the database. */ 142 + export async function exposePortsAndUpdate( 143 + db: Context["db"], 144 + sandbox: BaseSandbox, 145 + hostname: string, 146 + record: SelectSandbox, 147 + ports: SandboxResources["ports"], 148 + ): Promise<void> { 149 + const previewUrls = await Promise.all( 150 + ports.map((port) => 151 + sandbox.expose(port.sandbox_ports.exposedPort, hostname), 152 + ), 153 + ); 154 + 155 + await Promise.all( 156 + previewUrls.map((url, i) => { 157 + if (url) { 158 + return db 159 + .update(sandboxPorts) 160 + .set({ previewUrl: url }) 161 + .where( 162 + and( 163 + eq(sandboxPorts.sandboxId, record.id), 164 + eq(sandboxPorts.exposedPort, ports[i].sandbox_ports.exposedPort), 165 + ), 166 + ) 167 + .execute(); 168 + } 169 + }), 170 + ); 171 + } 172 + 173 + /** Start all services for a sandbox and mark them as RUNNING in the database. */ 174 + export async function startAndTrackServices( 175 + ctx: { waitUntil: (p: Promise<unknown>) => void }, 176 + db: Context["db"], 177 + sandbox: BaseSandbox, 178 + serviceList: SandboxResources["serviceList"], 179 + ): Promise<void> { 180 + ctx.waitUntil( 181 + Promise.all( 182 + serviceList.map(async (service) => { 183 + const id = await sandbox.startService(service.command); 184 + if (id) { 185 + await db 186 + .update(services) 187 + .set({ serviceId: id, status: "RUNNING" }) 188 + .where(eq(services.id, service.id)) 189 + .execute(); 190 + } 191 + }), 192 + ), 193 + ); 194 + }
+37
apps/cf-sandbox/src/middleware/auth.ts
··· 1 + import { MiddlewareHandler } from "hono"; 2 + import { Context } from "../context"; 3 + import { getSandbox } from "@cloudflare/sandbox"; 4 + import { Sandbox } from "@cloudflare/sandbox"; 5 + import { getConnection } from "../drizzle"; 6 + import jwt from "@tsndr/cloudflare-worker-jwt"; 7 + import { consola } from "consola"; 8 + 9 + type Bindings = { Sandbox: DurableObjectNamespace<Sandbox<Env>> }; 10 + 11 + export const authMiddleware: MiddlewareHandler<{ 12 + Variables: Context; 13 + Bindings: Bindings; 14 + }> = async (c, next) => { 15 + c.set("db", getConnection()); 16 + 17 + const token = c.req.header("Authorization")?.split(" ")[1]?.trim(); 18 + if (token) { 19 + try { 20 + const decoded = await jwt.verify(token, process.env.JWT_SECRET!); 21 + c.set( 22 + "did", 23 + decoded?.payload?.sub || (decoded?.payload as { did: string })?.did, 24 + ); 25 + } catch (err) { 26 + consola.error("JWT verification failed:", err); 27 + return c.json({ error: "Unauthorized" }, 401); 28 + } 29 + } else { 30 + if (!c.req.path.endsWith("/ws/terminal") && c.req.path !== "/") { 31 + consola.warn("No Authorization header found"); 32 + return c.json({ error: "Unauthorized" }, 401); 33 + } 34 + } 35 + 36 + await next(); 37 + };
+50
apps/cf-sandbox/src/routes/cp.ts
··· 1 + import { Hono } from "hono"; 2 + import { Context } from "../context"; 3 + import { Sandbox } from "@cloudflare/sandbox"; 4 + import { env } from "cloudflare:workers"; 5 + import { eq } from "drizzle-orm"; 6 + import crypto from "node:crypto"; 7 + import { sandboxCp } from "../schema"; 8 + 9 + type Bindings = { Sandbox: DurableObjectNamespace<Sandbox<Env>> }; 10 + type App = { Variables: Context; Bindings: Bindings }; 11 + 12 + export const cpRoutes = new Hono<App>(); 13 + 14 + cpRoutes.post("/cp", async (c) => { 15 + if (!c.var.did) { 16 + return c.json({ error: "Unauthorized" }, 401); 17 + } 18 + 19 + const formData = await c.req.formData(); 20 + const file = formData.get("file") as File; 21 + if (!file) { 22 + return c.json({ error: "No file uploaded" }, 400); 23 + } 24 + 25 + const fileBuffer = await file.arrayBuffer(); 26 + const uuid = c.req.query("uuid") || crypto.randomUUID(); 27 + await env.POCKETENV_COPY.put(uuid, fileBuffer); 28 + 29 + c.executionCtx.waitUntil( 30 + c.var.db.insert(sandboxCp).values({ copyUuid: uuid }).execute(), 31 + ); 32 + 33 + return c.json({ uuid }); 34 + }); 35 + 36 + cpRoutes.get("/cp/:uuid", async (c) => { 37 + const { uuid } = c.req.param(); 38 + const file = await env.POCKETENV_COPY.get(uuid); 39 + if (!file) { 40 + return c.json({ error: "File not found" }, 404); 41 + } 42 + 43 + await c.var.db.delete(sandboxCp).where(eq(sandboxCp.copyUuid, uuid)).execute(); 44 + await env.POCKETENV_COPY.delete(uuid); 45 + 46 + return c.body(file.body, 200, { 47 + "Content-Type": "application/gzip", 48 + "Content-Disposition": `attachment; filename="${uuid}.tar.gz"`, 49 + }); 50 + });
+528
apps/cf-sandbox/src/routes/sandboxes.ts
··· 1 + import { Hono } from "hono"; 2 + import { Context } from "../context"; 3 + import { Sandbox } from "@cloudflare/sandbox"; 4 + import { eq, and, isNull, or } from "drizzle-orm"; 5 + import { adjectives, nouns, generateUniqueAsync } from "unique-username-generator"; 6 + import { env } from "cloudflare:workers"; 7 + import { consola } from "consola"; 8 + import { 9 + SandboxConfig, 10 + SandboxConfigSchema, 11 + StartSandboxConfig, 12 + StartSandboxConfigSchema, 13 + } from "../types/sandbox"; 14 + import { createSandbox } from "../providers"; 15 + import { SelectSandbox } from "../schema/sandboxes"; 16 + import { sandboxes, users, services } from "../schema"; 17 + import { 18 + getSandboxRecord, 19 + generateSandboxId, 20 + saveSecrets, 21 + saveVariables, 22 + scheduleRepoClone, 23 + ensureSandboxId, 24 + toErrorMessage, 25 + } from "../lib/sandbox-helpers"; 26 + import { 27 + fetchSandboxResources, 28 + buildSandboxEnvs, 29 + scheduleInfraSetup, 30 + exposePortsAndUpdate, 31 + startAndTrackServices, 32 + } from "../lib/sandbox-resources"; 33 + import { PushDirectoryParams, pushSchema } from "../types/push"; 34 + import { PullDirectoryParams, pullSchema } from "../types/pull"; 35 + 36 + type Bindings = { Sandbox: DurableObjectNamespace<Sandbox<Env>> }; 37 + type App = { Variables: Context; Bindings: Bindings }; 38 + 39 + export const sandboxRoutes = new Hono<App>(); 40 + 41 + sandboxRoutes.post("/v1/sandboxes", async (c) => { 42 + const body = await c.req.json<SandboxConfig>(); 43 + 44 + let suffix = Math.random().toString(36).substring(2, 6); 45 + let name = await generateUniqueAsync( 46 + { dictionaries: [adjectives, nouns], separator: "-" }, 47 + () => false, 48 + ); 49 + 50 + try { 51 + const params = SandboxConfigSchema.parse(body); 52 + name = params.name || `${name}-${suffix}`; 53 + let existing: SelectSandbox[] = []; 54 + 55 + if (params.name) { 56 + existing = await c.var.db 57 + .select() 58 + .from(sandboxes) 59 + .where(and(eq(sandboxes.name, params.name), isNull(sandboxes.userId))) 60 + .execute(); 61 + } 62 + 63 + const canBeClaimed = existing.length !== 0; 64 + 65 + if (!canBeClaimed) { 66 + do { 67 + existing = await c.var.db 68 + .select() 69 + .from(sandboxes) 70 + .where(eq(sandboxes.name, name)) 71 + .execute(); 72 + if (existing.length === 0) break; 73 + 74 + name = await generateUniqueAsync( 75 + { dictionaries: [adjectives, nouns], separator: "-" }, 76 + () => false, 77 + ); 78 + suffix = Math.random().toString(36).substring(2, 6); 79 + name = `${name}-${suffix}`; 80 + } while (true); 81 + } 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(([row]) => row); 90 + 91 + let record: SelectSandbox | undefined; 92 + 93 + if (canBeClaimed) { 94 + record = await tx 95 + .update(sandboxes) 96 + .set({ userId: user?.id }) 97 + .where(eq(sandboxes.id, existing[0].id)) 98 + .returning() 99 + .execute() 100 + .then(([row]) => row); 101 + } else { 102 + record = await tx 103 + .insert(sandboxes) 104 + .values({ 105 + base: params.base, 106 + name, 107 + repo: params.repo, 108 + provider: params.provider, 109 + publicKey: env.PUBLIC_KEY, 110 + userId: user?.id, 111 + instanceType: "standard-3", 112 + keepAlive: params.keepAlive, 113 + sleepAfter: params.sleepAfter, 114 + vcpus: params.vcpus, 115 + memory: params.memory, 116 + disk: params.disk, 117 + status: "INITIALIZING", 118 + }) 119 + .returning() 120 + .execute() 121 + .then(([row]) => row); 122 + } 123 + 124 + if (params.secrets.length > 0) await saveSecrets(tx, record!, { secrets: params.secrets }); 125 + if (params.variables.length > 0) await saveVariables(tx, record!, { variables: params.variables }); 126 + 127 + const sandboxId = generateSandboxId(); 128 + const sandboxInstance = await createSandbox(params.provider, { 129 + id: sandboxId, 130 + keepAlive: params.keepAlive, 131 + sleepAfter: params.sleepAfter, 132 + }); 133 + 134 + await sandboxInstance.start(); 135 + 136 + [record] = await tx 137 + .update(sandboxes) 138 + .set({ status: "RUNNING", sandboxId, startedAt: new Date() }) 139 + .where(eq(sandboxes.id, record!.id)) 140 + .returning() 141 + .execute(); 142 + 143 + if (params.repo) scheduleRepoClone(c.executionCtx, sandboxInstance, params.repo); 144 + 145 + const baseSandbox = await tx 146 + .select() 147 + .from(sandboxes) 148 + .where(eq(sandboxes.name, params.base)) 149 + .execute() 150 + .then((rows) => rows[0]); 151 + 152 + await tx 153 + .update(sandboxes) 154 + .set({ installs: (baseSandbox?.installs || 0) + 1 }) 155 + .where(eq(sandboxes.name, params.base)) 156 + .execute(); 157 + 158 + return record; 159 + }); 160 + 161 + return c.json(record); 162 + } catch (err) { 163 + console.log(err); 164 + return c.json( 165 + { error: toErrorMessage(err) }, 166 + 400, 167 + ); 168 + } 169 + }); 170 + 171 + sandboxRoutes.get("/v1/sandboxes", async (c) => { 172 + const records = await c.var.db.select().from(sandboxes).execute(); 173 + return c.json(records); 174 + }); 175 + 176 + sandboxRoutes.get("/v1/sandboxes/:sandboxId", async (c) => { 177 + const result = await getSandboxRecord(c.var.db, c.req.param("sandboxId")); 178 + return c.json(result?.sandbox); 179 + }); 180 + 181 + sandboxRoutes.put("/v1/sandboxes/:sandboxId", async (c) => { 182 + return c.json({}); 183 + }); 184 + 185 + sandboxRoutes.post("/v1/sandboxes/:sandboxId/start", async (c) => { 186 + const result = await getSandboxRecord(c.var.db, c.req.param("sandboxId")); 187 + const record = result?.sandbox; 188 + 189 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 190 + if (record.provider !== "cloudflare") return c.json({ error: "Sandbox provider not supported" }, 400); 191 + 192 + const body = await c.req.json<StartSandboxConfig>(); 193 + const { repo, keepAlive } = StartSandboxConfigSchema.parse(body); 194 + 195 + const sandboxId = generateSandboxId(); 196 + await ensureSandboxId(c.var.db, c.req.param("sandboxId"), sandboxId); 197 + 198 + try { 199 + const sandbox = await createSandbox("cloudflare", { 200 + id: record.sandboxId || sandboxId, 201 + memory: "4GiB", 202 + keepAlive, 203 + }); 204 + 205 + if (!sandbox) return c.json({ error: "Sandbox provider not supported" }, 400); 206 + 207 + if (repo) scheduleRepoClone(c.executionCtx, sandbox, repo); 208 + 209 + await c.var.db 210 + .update(sandboxes) 211 + .set({ status: "RUNNING", sandboxId: record.sandboxId || sandboxId, startedAt: new Date() }) 212 + .where(eq(sandboxes.id, c.req.param("sandboxId"))) 213 + .execute(); 214 + 215 + const resources = await fetchSandboxResources(c.var.db, c.req.param("sandboxId")); 216 + 217 + c.executionCtx.waitUntil( 218 + sandbox.setEnvs(await buildSandboxEnvs(resources)), 219 + ); 220 + 221 + c.executionCtx.waitUntil( 222 + sandbox.sh`[ -f /root/.ssh/id_ed25519 ] || ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -q -N "" || true`, 223 + ); 224 + 225 + await scheduleInfraSetup(c.executionCtx, sandbox, resources); 226 + 227 + if (record.repo) scheduleRepoClone(c.executionCtx, sandbox, record.repo); 228 + 229 + const { hostname } = new URL(c.req.url); 230 + await exposePortsAndUpdate(c.var.db, sandbox, hostname, record, resources.ports); 231 + 232 + await startAndTrackServices(c.executionCtx, c.var.db, sandbox, resources.serviceList); 233 + } catch (err) { 234 + const msg = toErrorMessage(err); 235 + consola.log("Failed to start sandbox:", msg); 236 + return c.json({ error: `Failed to start sandbox: ${msg}` }, 500); 237 + } 238 + 239 + return c.json({}); 240 + }); 241 + 242 + sandboxRoutes.post("/v1/sandboxes/:sandboxId/stop", async (c) => { 243 + const result = await getSandboxRecord(c.var.db, c.req.param("sandboxId")); 244 + const record = result?.sandbox; 245 + 246 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 247 + if (record.provider !== "cloudflare") return c.json({ error: "Sandbox provider not supported" }, 400); 248 + if (!record.sandboxId) return c.json({ error: "Sandbox is not running" }, 400); 249 + 250 + try { 251 + const sandbox = await createSandbox("cloudflare", { id: record.sandboxId }); 252 + if (!sandbox) return c.json({ error: "Sandbox provider not supported" }, 400); 253 + 254 + const { sandboxVolumes } = await import("../schema"); 255 + const volumes = await c.var.db 256 + .select() 257 + .from(sandboxVolumes) 258 + .where(eq(sandboxVolumes.sandboxId, c.req.param("sandboxId"))) 259 + .execute(); 260 + 261 + try { 262 + await Promise.all(volumes.map((v) => sandbox.unmount(v.path))); 263 + } catch (e) { 264 + console.error(e); 265 + } 266 + 267 + await sandbox.stop(); 268 + await Promise.all([ 269 + c.var.db.update(sandboxes).set({ status: "STOPPED" }).where(eq(sandboxes.id, c.req.param("sandboxId"))).execute(), 270 + c.var.db.update(services).set({ status: "STOPPED" }).where(eq(services.sandboxId, c.req.param("sandboxId"))).execute(), 271 + ]); 272 + 273 + return c.json({}); 274 + } catch (err) { 275 + const msg = toErrorMessage(err); 276 + consola.error("Failed to stop sandbox:", msg); 277 + return c.json({ error: `Failed to stop sandbox: ${msg}` }, 500); 278 + } 279 + }); 280 + 281 + sandboxRoutes.post("/v1/sandboxes/:sandboxId/runs", async (c) => { 282 + const result = await getSandboxRecord(c.var.db, c.req.param("sandboxId")); 283 + const record = result?.sandbox; 284 + 285 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 286 + if (record.provider !== "cloudflare") return c.json({ error: "Sandbox provider not supported" }, 400); 287 + if (record.status !== "RUNNING") return c.json({ error: "Sandbox is not running" }, 400); 288 + 289 + try { 290 + const sandbox = await createSandbox("cloudflare", { id: record.sandboxId! }); 291 + const { command } = await c.req.json(); 292 + const res = await sandbox.sh`${command}`; 293 + return c.json(res); 294 + } catch (err) { 295 + const msg = toErrorMessage(err); 296 + consola.error("Failed to run command in sandbox:", msg); 297 + return c.json({ error: `Failed to run command: ${msg}` }, 500); 298 + } 299 + }); 300 + 301 + sandboxRoutes.delete("/v1/sandboxes/:sandboxId", async (c) => { 302 + const result = await getSandboxRecord(c.var.db, c.req.param("sandboxId")); 303 + const record = result?.sandbox; 304 + 305 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 306 + if (record.provider !== "cloudflare") return c.json({ error: "Sandbox provider not supported" }, 400); 307 + 308 + try { 309 + const sandbox = await createSandbox("cloudflare", { id: record.sandboxId! }); 310 + await sandbox.delete(); 311 + await c.var.db 312 + .delete(sandboxes) 313 + .where( 314 + or( 315 + eq(sandboxes.id, c.req.param("sandboxId")), 316 + eq(sandboxes.name, c.req.param("sandboxId")), 317 + ), 318 + ) 319 + .execute(); 320 + return c.json({ success: true }, 200); 321 + } catch (err) { 322 + const msg = toErrorMessage(err); 323 + consola.error("Failed to delete sandbox:", msg); 324 + return c.json({ error: `Failed to delete sandbox: ${msg}` }, 500); 325 + } 326 + }); 327 + 328 + sandboxRoutes.post("/v1/sandboxes/:sandboxId/ports", async (c) => { 329 + const result = await getSandboxRecord(c.var.db, c.req.param("sandboxId")); 330 + const record = result?.sandbox; 331 + 332 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 333 + if (record.provider !== "cloudflare") return c.json({ error: "Sandbox provider not supported" }, 400); 334 + 335 + try { 336 + const sandbox = await createSandbox("cloudflare", { id: record.sandboxId! }); 337 + const { port } = await c.req.json<{ port: number }>(); 338 + 339 + if (!port || port < 1025 || port > 65535 || port === 3000) { 340 + return c.json({ error: "Invalid port number" }, 400); 341 + } 342 + 343 + const { hostname } = new URL(c.req.url); 344 + const previewUrl = await sandbox.expose(port, hostname); 345 + return c.json({ previewUrl }); 346 + } catch (err) { 347 + const msg = toErrorMessage(err); 348 + consola.error(c.req.param("sandboxId"), "Failed to expose port:", msg); 349 + return c.json({ error: `Failed to expose port: ${msg}` }, 500); 350 + } 351 + }); 352 + 353 + sandboxRoutes.delete("/v1/sandboxes/:sandboxId/ports", async (c) => { 354 + const result = await getSandboxRecord(c.var.db, c.req.param("sandboxId")); 355 + const record = result?.sandbox; 356 + 357 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 358 + if (record.provider !== "cloudflare") return c.json({ error: "Sandbox provider not supported" }, 400); 359 + 360 + try { 361 + const sandbox = await createSandbox("cloudflare", { id: record.sandboxId! }); 362 + const port = parseInt(c.req.query("port") || "0", 10); 363 + 364 + if (!port || port < 1024 || port > 65535 || port === 3000) { 365 + return c.json({ error: "Invalid port number" }, 400); 366 + } 367 + 368 + await sandbox.unexpose(port); 369 + return c.json({}); 370 + } catch (err) { 371 + const msg = toErrorMessage(err); 372 + consola.error(c.req.param("sandboxId"), "Failed to unexpose port:", msg); 373 + return c.json({ error: `Failed to unexpose port: ${msg}` }, 500); 374 + } 375 + }); 376 + 377 + sandboxRoutes.post("/v1/sandboxes/:sandboxId/vscode", async (c) => { 378 + const result = await getSandboxRecord(c.var.db, c.req.param("sandboxId")); 379 + const record = result?.sandbox; 380 + 381 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 382 + if (record.provider !== "cloudflare") return c.json({ error: "Sandbox provider not supported" }, 400); 383 + 384 + try { 385 + const sandbox = await createSandbox("cloudflare", { id: record.sandboxId! }); 386 + const { hostname } = new URL(c.req.url); 387 + const previewUrl = await sandbox.exposeVscode(hostname); 388 + return c.json({ previewUrl }); 389 + } catch (err) { 390 + const msg = toErrorMessage(err); 391 + consola.error(c.req.param("sandboxId"), "Failed to expose vscode:", msg); 392 + return c.json({ error: `Failed to expose vscode: ${msg}` }, 500); 393 + } 394 + }); 395 + 396 + sandboxRoutes.delete("/v1/sandboxes/:sandboxId/vscode", async (c) => { 397 + const result = await getSandboxRecord(c.var.db, c.req.param("sandboxId")); 398 + const record = result?.sandbox; 399 + 400 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 401 + if (record.provider !== "cloudflare") return c.json({ error: "Sandbox provider not supported" }, 400); 402 + 403 + try { 404 + const sandbox = await createSandbox("cloudflare", { id: record.sandboxId! }); 405 + await sandbox.unexposeVscode(); 406 + return c.json({}); 407 + } catch (err) { 408 + const msg = toErrorMessage(err); 409 + consola.error(c.req.param("sandboxId"), "Failed to unexpose vscode:", msg); 410 + return c.json({ error: `Failed to unexpose vscode: ${msg}` }, 500); 411 + } 412 + }); 413 + 414 + sandboxRoutes.post("/v1/sandboxes/:sandboxId/services/:serviceId", async (c) => { 415 + const result = await getSandboxRecord(c.var.db, c.req.param("sandboxId")); 416 + const record = result?.sandbox; 417 + 418 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 419 + if (record.provider !== "cloudflare") return c.json({ error: "Sandbox provider not supported" }, 400); 420 + 421 + try { 422 + const sandbox = await createSandbox("cloudflare", { id: record.sandboxId! }); 423 + 424 + const [service] = await c.var.db 425 + .select() 426 + .from(services) 427 + .where( 428 + and( 429 + eq(services.id, c.req.param("serviceId")), 430 + eq(services.sandboxId, record.id), 431 + ), 432 + ) 433 + .execute(); 434 + 435 + if (!service) return c.json({ error: "Service not found" }, 404); 436 + if (service.status === "RUNNING") return c.json({}); 437 + 438 + const serviceId = await sandbox.startService(service.command); 439 + await c.var.db 440 + .update(services) 441 + .set({ serviceId, status: "RUNNING" }) 442 + .where(eq(services.id, service.id)) 443 + .execute(); 444 + 445 + return c.json({ serviceId }); 446 + } catch (err) { 447 + console.log("Failed to start service:", err); 448 + } 449 + 450 + return c.json({}); 451 + }); 452 + 453 + sandboxRoutes.delete("/v1/sandboxes/:sandboxId/services/:serviceId", async (c) => { 454 + const result = await getSandboxRecord(c.var.db, c.req.param("sandboxId")); 455 + const record = result?.sandbox; 456 + 457 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 458 + if (record.provider !== "cloudflare") return c.json({ error: "Sandbox provider not supported" }, 400); 459 + 460 + try { 461 + const sandbox = await createSandbox("cloudflare", { id: record.sandboxId! }); 462 + 463 + const [service] = await c.var.db 464 + .select() 465 + .from(services) 466 + .where( 467 + and( 468 + eq(services.id, c.req.param("serviceId")), 469 + eq(services.sandboxId, record.id), 470 + ), 471 + ) 472 + .execute(); 473 + 474 + if (!service) return c.json({ error: "Service not found" }, 404); 475 + if (service.status !== "RUNNING" || !service.serviceId) return c.json({}); 476 + 477 + await sandbox.stopService(service.serviceId!); 478 + await c.var.db 479 + .update(services) 480 + .set({ status: "STOPPED" }) 481 + .where(eq(services.id, service.id)) 482 + .execute(); 483 + } catch (err) { 484 + console.log("Failed to stop service:", err); 485 + } 486 + 487 + return c.json({}); 488 + }); 489 + 490 + sandboxRoutes.post("/v1/sandboxes/:sandboxId/pull-directory", async (c) => { 491 + const result = await getSandboxRecord(c.var.db, c.req.param("sandboxId")); 492 + const record = result?.sandbox; 493 + 494 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 495 + if (record.provider !== "cloudflare") return c.json({ error: "Sandbox provider not supported" }, 400); 496 + 497 + const token = c.req.header("Authorization"); 498 + const params = await c.req.json<PullDirectoryParams>(); 499 + await pullSchema.parseAsync(params); 500 + 501 + const outdir = crypto.randomUUID(); 502 + const sandbox = await createSandbox("cloudflare", { id: record.sandboxId! }); 503 + 504 + await sandbox.sh`mkdir -p /tmp/${outdir} && cd /tmp/${outdir} && curl https://sandbox.pocketenv.io/cp/${params.uuid} -H "Authorization: ${token}" --output - | tar xzvf -`; 505 + await sandbox.sh`mkdir -p ${params.directoryPath} || sudo mkdir -p ${params.directoryPath}`; 506 + await sandbox.sh`(shopt -s dotglob && cp -r /tmp/${outdir}/* ${params.directoryPath}) || (shopt -s dotglob && sudo cp -r /tmp/${outdir}/* ${params.directoryPath})`; 507 + 508 + return c.json({ success: true }); 509 + }); 510 + 511 + sandboxRoutes.post("/v1/sandboxes/:sandboxId/push-directory", async (c) => { 512 + const result = await getSandboxRecord(c.var.db, c.req.param("sandboxId")); 513 + const record = result?.sandbox; 514 + 515 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 516 + if (record.provider !== "cloudflare") return c.json({ error: "Sandbox provider not supported" }, 400); 517 + 518 + const params = await c.req.json<PushDirectoryParams>(); 519 + await pushSchema.parseAsync(params); 520 + 521 + const token = c.req.header("Authorization"); 522 + const uuid = crypto.randomUUID(); 523 + 524 + const sandbox = await createSandbox("cloudflare", { id: record.sandboxId! }); 525 + 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`; 526 + 527 + return c.json({ uuid }); 528 + });
+48
apps/cf-sandbox/src/routes/terminal.ts
··· 1 + import { Hono } from "hono"; 2 + import { Context } from "../context"; 3 + import { getSandbox, Sandbox } from "@cloudflare/sandbox"; 4 + import jwt from "@tsndr/cloudflare-worker-jwt"; 5 + import { getSandboxRecord } from "../lib/sandbox-helpers"; 6 + 7 + type Bindings = { Sandbox: DurableObjectNamespace<Sandbox<Env>> }; 8 + type App = { Variables: Context; Bindings: Bindings }; 9 + 10 + export const terminalRoutes = new Hono<App>(); 11 + 12 + terminalRoutes.get("/v1/sandboxes/:sandboxId/ws/terminal", async (c) => { 13 + if (c.req.header("upgrade")?.toLowerCase() !== "websocket") { 14 + return c.text("Expected WebSocket connection", 426); 15 + } 16 + 17 + const result = await getSandboxRecord(c.var.db, c.req.param("sandboxId")); 18 + const record = result?.sandbox; 19 + const user = result?.user; 20 + 21 + if (!record) return c.text("Sandbox not found", 404); 22 + 23 + const token = c.req.query("t"); 24 + if (token) { 25 + const decoded = await jwt.verify(token, process.env.JWT_SECRET!); 26 + if (record.userId && user && user.did !== decoded?.payload?.sub) { 27 + return c.text("Unauthorized", 403); 28 + } 29 + } 30 + 31 + if (!record.sandboxId) return c.text("Sandbox not started", 400); 32 + 33 + const sandbox = getSandbox(c.env.Sandbox, record.sandboxId); 34 + await sandbox.start(); 35 + 36 + const sessionId = c.req.query("session"); 37 + 38 + try { 39 + if (sessionId) { 40 + const session = await sandbox.getSession(sessionId); 41 + return session.terminal(c.req.raw); 42 + } 43 + return sandbox.terminal(c.req.raw); 44 + } catch (err) { 45 + console.log(err); 46 + return c.text("Failed to connect to sandbox", 500); 47 + } 48 + });
+92 -547
apps/sandbox/src/index.ts
··· 51 51 import crypto from "node:crypto"; 52 52 import { PullDirectoryParams, pullSchema } from "./types/pull.ts"; 53 53 import { PushDirectoryParams, pushSchema } from "./types/push.ts"; 54 + import { 55 + getAuthParams, 56 + buildCredentials, 57 + resolveSandboxInstance, 58 + } from "./lib/sandbox-helpers.ts"; 59 + 60 + const SUPPORTED_PROVIDERS = ["daytona", "vercel", "deno", "sprites"]; 54 61 55 62 const app = new Hono<{ Variables: Context }>(); 56 63 ··· 271 278 272 279 app.post("/v1/sandboxes/:sandboxId/start", async (c) => { 273 280 const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 274 - 275 - if (!record) { 276 - return c.json({ error: "Sandbox not found" }, 404); 277 - } 278 - 279 - let sandbox: BaseSandbox | null = null; 280 - 281 - if (!["daytona", "vercel", "deno", "sprites"].includes(record.provider)) { 281 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 282 + if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 282 283 return c.json({ error: "Sandbox provider not supported" }, 400); 283 284 } 284 285 285 - const body = await c.req.json<StartSandboxInput>(); 286 - const { repo } = StartSandboxInputSchema.parse(body); 287 - 288 - const [ 289 - [spriteAuthParams], 290 - [daytonaAuthParams], 291 - [denoAuthParams], 292 - [vercelAuthParams], 293 - ] = await Promise.all([ 294 - c.var.db 295 - .select() 296 - .from(spriteAuth) 297 - .where(eq(spriteAuth.sandboxId, record.id)) 298 - .execute(), 299 - c.var.db 300 - .select() 301 - .from(daytonaAuth) 302 - .where(eq(daytonaAuth.sandboxId, record.id)) 303 - .execute(), 304 - c.var.db 305 - .select() 306 - .from(denoAuth) 307 - .where(eq(denoAuth.sandboxId, record.id)) 308 - .execute(), 309 - c.var.db 310 - .select() 311 - .from(vercelAuth) 312 - .where(eq(vercelAuth.sandboxId, record.id)) 313 - .execute(), 314 - ]); 315 - 316 - if (!record.sandboxId) { 317 - sandbox = await createSandbox(record.provider as Provider, { 318 - id: record.id, 319 - daytonaApiKey: decrypt(daytonaAuthParams?.apiKey), 320 - organizationId: daytonaAuthParams?.organizationId, 321 - spriteToken: decrypt(spriteAuthParams?.spriteToken), 322 - denoDeployToken: decrypt(denoAuthParams?.deployToken), 323 - vercelApiToken: decrypt(vercelAuthParams?.vercelToken), 324 - vercelProjectId: vercelAuthParams?.projectId, 325 - vercelTeamId: vercelAuthParams?.teamId, 326 - }); 327 - const sandboxId = await sandbox.id(); 328 - await c.var.db 329 - .update(sandboxes) 330 - .set({ sandboxId }) 331 - .where(eq(sandboxes.id, record.id)) 332 - .execute(); 333 - record.sandboxId = sandboxId; 334 - } 335 - 336 - sandbox = await getSandboxById( 337 - record.provider as Provider, 338 - record.sandboxId!, 339 - { 340 - daytonaApiKey: decrypt(daytonaAuthParams?.apiKey), 341 - spriteToken: decrypt(spriteAuthParams?.spriteToken), 342 - denoDeployToken: decrypt(denoAuthParams?.deployToken), 343 - organizationId: daytonaAuthParams?.organizationId, 344 - vercelApiToken: decrypt(vercelAuthParams?.vercelToken), 345 - vercelProjectId: vercelAuthParams?.projectId, 346 - vercelTeamId: vercelAuthParams?.teamId, 347 - }, 286 + const { repo } = StartSandboxInputSchema.parse( 287 + await c.req.json<StartSandboxInput>(), 348 288 ); 349 289 350 - if (!sandbox) { 351 - return c.json({ error: "Sandbox provider not supported" }, 400); 352 - } 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); 353 293 354 294 await sandbox.start(); 355 295 ··· 367 307 ), 368 308 ); 369 309 370 - const params = await Promise.all([ 371 - c.var.db 372 - .select() 373 - .from(sandboxFiles) 374 - .leftJoin(files, eq(files.id, sandboxFiles.fileId)) 375 - .where(eq(sandboxFiles.sandboxId, c.req.param("sandboxId"))) 376 - .execute(), 377 - c.var.db 378 - .select() 379 - .from(sshKeys) 380 - .where(eq(sshKeys.sandboxId, c.req.param("sandboxId"))) 381 - .execute(), 382 - c.var.db 383 - .select() 384 - .from(tailscaleAuthKeys) 385 - .where(eq(tailscaleAuthKeys.sandboxId, c.req.param("sandboxId"))) 386 - .execute(), 387 - c.var.db 388 - .select() 389 - .from(sandboxVolumes) 390 - .leftJoin(sandboxes, eq(sandboxes.id, sandboxVolumes.sandboxId)) 391 - .leftJoin(users, eq(users.id, sandboxes.userId)) 392 - .where(eq(sandboxVolumes.sandboxId, c.req.param("sandboxId"))) 393 - .execute(), 394 - ]); 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 + ]); 395 336 396 337 await sandbox.setupDefaultSshKeys(); 397 338 398 339 Promise.all([ 399 - ...params[0] 340 + ...sandboxFileRecords 400 341 .filter((x) => x.files !== null) 401 - .map((record) => 402 - sandbox?.writeFile( 403 - record.sandbox_files.path, 404 - decrypt(record.files!.content)!, 405 - ), 342 + .map((r) => 343 + sandbox.writeFile(r.sandbox_files.path, decrypt(r.files!.content)!), 406 344 ), 407 - ...params[1].map((record) => 408 - sandbox?.setupSshKeys(decrypt(record.privateKey)!, record.publicKey), 345 + ...sshKeyRecords.map((r) => 346 + sandbox.setupSshKeys(decrypt(r.privateKey)!, r.publicKey), 409 347 ), 410 - params[2].length > 0 && 411 - sandbox?.setupTailscale(decrypt(params[2][0].authKey)!), 412 - ...params[3].map((volume) => 413 - sandbox?.mount( 414 - volume.sandbox_volumes.path, 415 - `/${volume.users?.did || ""}${volume.users?.did ? "/" : ""}${volume.sandbox_volumes.id}/`, 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}/`, 416 354 ), 417 355 ), 418 356 ]) ··· 456 394 457 395 app.post("/v1/sandboxes/:sandboxId/stop", async (c) => { 458 396 const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 459 - 460 - if (!record) { 461 - return c.json({ error: "Sandbox not found" }, 404); 462 - } 463 - 464 - let sandbox: BaseSandbox | null = null; 465 - 466 - if (!["daytona", "vercel", "deno", "sprites"].includes(record.provider)) { 397 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 398 + if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 467 399 return c.json({ error: "Sandbox provider not supported" }, 400); 468 400 } 469 401 470 - const [ 471 - [spriteAuthParams], 472 - [daytonaAuthParams], 473 - [denoAuthParams], 474 - [vercelAuthParams], 475 - ] = await Promise.all([ 476 - c.var.db 477 - .select() 478 - .from(spriteAuth) 479 - .where(eq(spriteAuth.sandboxId, record.id)) 480 - .execute(), 481 - c.var.db 482 - .select() 483 - .from(daytonaAuth) 484 - .where(eq(daytonaAuth.sandboxId, record.id)) 485 - .execute(), 486 - c.var.db 487 - .select() 488 - .from(denoAuth) 489 - .where(eq(denoAuth.sandboxId, record.id)) 490 - .execute(), 491 - c.var.db 492 - .select() 493 - .from(vercelAuth) 494 - .where(eq(vercelAuth.sandboxId, record.id)) 495 - .execute(), 496 - ]); 497 - 498 - if (!record.sandboxId) { 499 - sandbox = await createSandbox(record.provider as Provider, { 500 - id: record.id, 501 - daytonaApiKey: decrypt(daytonaAuthParams?.apiKey), 502 - organizationId: daytonaAuthParams?.organizationId, 503 - spriteToken: decrypt(spriteAuthParams?.spriteToken), 504 - denoDeployToken: decrypt(denoAuthParams?.deployToken), 505 - vercelApiToken: decrypt(vercelAuthParams?.vercelToken), 506 - vercelProjectId: vercelAuthParams?.projectId, 507 - vercelTeamId: vercelAuthParams?.teamId, 508 - }); 509 - const sandboxId = await sandbox.id(); 510 - await c.var.db 511 - .update(sandboxes) 512 - .set({ sandboxId }) 513 - .where(eq(sandboxes.id, record.id)) 514 - .execute(); 515 - record.sandboxId = sandboxId; 516 - } 517 - 518 - sandbox = await getSandboxById( 519 - record.provider as Provider, 520 - record.sandboxId!, 521 - { 522 - daytonaApiKey: decrypt(daytonaAuthParams?.apiKey), 523 - spriteToken: decrypt(spriteAuthParams?.spriteToken), 524 - denoDeployToken: decrypt(denoAuthParams?.deployToken), 525 - organizationId: daytonaAuthParams?.organizationId, 526 - vercelApiToken: decrypt(vercelAuthParams?.vercelToken), 527 - vercelProjectId: vercelAuthParams?.projectId, 528 - vercelTeamId: vercelAuthParams?.teamId, 529 - }, 402 + const auth = await getAuthParams(c.var.db, record.id); 403 + const sandbox = await resolveSandboxInstance( 404 + c.var.db, 405 + record, 406 + buildCredentials(auth), 530 407 ); 531 408 532 - if (!sandbox) { 533 - return c.json({ error: "Sandbox provider not supported" }, 400); 534 - } 535 - 536 409 await sandbox.stop(); 537 410 await c.var.db 538 411 .update(sandboxes) ··· 549 422 550 423 app.post("/v1/sandboxes/:sandboxId/runs", async (c) => { 551 424 const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 552 - 553 - if (!record) { 554 - return c.json({ error: "Sandbox not found" }, 404); 555 - } 556 - 557 - let sandbox: BaseSandbox | null = null; 558 - 559 - if (!["daytona", "vercel", "deno", "sprites"].includes(record.provider)) { 425 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 426 + if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 560 427 return c.json({ error: "Sandbox provider not supported" }, 400); 561 428 } 562 429 563 - const [ 564 - [spriteAuthParams], 565 - [daytonaAuthParams], 566 - [denoAuthParams], 567 - [vercelAuthParams], 568 - ] = await Promise.all([ 569 - c.var.db 570 - .select() 571 - .from(spriteAuth) 572 - .where(eq(spriteAuth.sandboxId, record.id)) 573 - .execute(), 574 - c.var.db 575 - .select() 576 - .from(daytonaAuth) 577 - .where(eq(daytonaAuth.sandboxId, record.id)) 578 - .execute(), 579 - c.var.db 580 - .select() 581 - .from(denoAuth) 582 - .where(eq(denoAuth.sandboxId, record.id)) 583 - .execute(), 584 - c.var.db 585 - .select() 586 - .from(vercelAuth) 587 - .where(eq(vercelAuth.sandboxId, record.id)) 588 - .execute(), 589 - ]); 590 - 591 - if (!record.sandboxId) { 592 - sandbox = await createSandbox(record.provider as Provider, { 593 - id: record.id, 594 - daytonaApiKey: decrypt(daytonaAuthParams?.apiKey), 595 - organizationId: daytonaAuthParams?.organizationId, 596 - spriteToken: decrypt(spriteAuthParams?.spriteToken), 597 - denoDeployToken: decrypt(denoAuthParams?.deployToken), 598 - vercelApiToken: decrypt(vercelAuthParams?.vercelToken), 599 - vercelProjectId: vercelAuthParams?.projectId, 600 - vercelTeamId: vercelAuthParams?.teamId, 601 - }); 602 - const sandboxId = await sandbox.id(); 603 - await c.var.db 604 - .update(sandboxes) 605 - .set({ sandboxId }) 606 - .where(eq(sandboxes.id, record.id)) 607 - .execute(); 608 - record.sandboxId = sandboxId; 609 - } 610 - 611 - sandbox = await getSandboxById( 612 - record.provider as Provider, 613 - record.sandboxId!, 614 - { 615 - daytonaApiKey: decrypt(daytonaAuthParams?.apiKey), 616 - spriteToken: decrypt(spriteAuthParams?.spriteToken), 617 - denoDeployToken: decrypt(denoAuthParams?.deployToken), 618 - organizationId: daytonaAuthParams?.organizationId, 619 - vercelApiToken: decrypt(vercelAuthParams?.vercelToken), 620 - vercelProjectId: vercelAuthParams?.projectId, 621 - vercelTeamId: vercelAuthParams?.teamId, 622 - }, 430 + const auth = await getAuthParams(c.var.db, record.id); 431 + const sandbox = await resolveSandboxInstance( 432 + c.var.db, 433 + record, 434 + buildCredentials(auth), 623 435 ); 624 436 625 - if (!sandbox) { 626 - return c.json({ error: "Sandbox provider not supported" }, 400); 627 - } 628 - 629 437 const { command } = await c.req.json(); 630 438 const res = await sandbox.sh`${command}`; 631 439 return c.json(res); ··· 633 441 634 442 app.delete("/v1/sandboxes/:sandboxId", async (c) => { 635 443 const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 636 - 637 - if (!record) { 638 - return c.json({ error: "Sandbox not found" }, 404); 639 - } 640 - 641 - let sandbox: BaseSandbox | null = null; 642 - 643 - if (!["daytona", "vercel", "deno", "sprites"].includes(record.provider)) { 444 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 445 + if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 644 446 return c.json({ error: "Sandbox provider not supported" }, 400); 645 447 } 646 448 647 - const [ 648 - [spriteAuthParams], 649 - [daytonaAuthParams], 650 - [denoAuthParams], 651 - [vercelAuthParams], 652 - ] = await Promise.all([ 653 - c.var.db 654 - .select() 655 - .from(spriteAuth) 656 - .where(eq(spriteAuth.sandboxId, record.id)) 657 - .execute(), 658 - c.var.db 659 - .select() 660 - .from(daytonaAuth) 661 - .where(eq(daytonaAuth.sandboxId, record.id)) 662 - .execute(), 663 - c.var.db 664 - .select() 665 - .from(denoAuth) 666 - .where(eq(denoAuth.sandboxId, record.id)) 667 - .execute(), 668 - c.var.db 669 - .select() 670 - .from(vercelAuth) 671 - .where(eq(vercelAuth.sandboxId, record.id)) 672 - .execute(), 673 - ]); 674 - 675 - if (!record.sandboxId) { 676 - sandbox = await createSandbox(record.provider as Provider, { 677 - id: record.id, 678 - daytonaApiKey: decrypt(daytonaAuthParams?.apiKey), 679 - organizationId: daytonaAuthParams?.organizationId, 680 - spriteToken: decrypt(spriteAuthParams?.spriteToken), 681 - denoDeployToken: decrypt(denoAuthParams?.deployToken), 682 - vercelApiToken: decrypt(vercelAuthParams?.vercelToken), 683 - vercelProjectId: vercelAuthParams?.projectId, 684 - vercelTeamId: vercelAuthParams?.teamId, 685 - }); 686 - const sandboxId = await sandbox.id(); 687 - await c.var.db 688 - .update(sandboxes) 689 - .set({ sandboxId }) 690 - .where(eq(sandboxes.id, record.id)) 691 - .execute(); 692 - record.sandboxId = sandboxId; 693 - } 694 - 695 - sandbox = await getSandboxById( 696 - record.provider as Provider, 697 - record.sandboxId!, 698 - { 699 - daytonaApiKey: decrypt(daytonaAuthParams?.apiKey), 700 - spriteToken: decrypt(spriteAuthParams?.spriteToken), 701 - denoDeployToken: decrypt(denoAuthParams?.deployToken), 702 - organizationId: daytonaAuthParams?.organizationId, 703 - vercelApiToken: decrypt(vercelAuthParams?.vercelToken), 704 - vercelProjectId: vercelAuthParams?.projectId, 705 - vercelTeamId: vercelAuthParams?.teamId, 706 - }, 449 + const auth = await getAuthParams(c.var.db, record.id); 450 + const sandbox = await resolveSandboxInstance( 451 + c.var.db, 452 + record, 453 + buildCredentials(auth), 707 454 ); 708 455 709 - if (!sandbox) { 710 - return c.json({ error: "Sandbox provider not supported" }, 400); 711 - } 712 - 713 456 await sandbox.delete(); 714 - 715 457 await c.var.db 716 458 .delete(sandboxes) 717 459 .where(eq(sandboxes.id, c.req.param("sandboxId"))) 718 460 .execute(); 719 - 720 461 return c.json({ success: true }, 200); 721 462 }); 722 463 723 464 app.get("/v1/sandboxes/:sandboxId/ssh", async (c) => { 724 465 const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 725 - 726 - if (!record) { 727 - return c.json({ error: "Sandbox not found" }, 404); 728 - } 729 - 730 - let sandbox: BaseSandbox | null = null; 731 - 466 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 732 467 if (!["daytona", "deno"].includes(record.provider)) { 733 468 return c.json({ error: "Sandbox provider not supported" }, 400); 734 469 } 735 470 736 - const [ 737 - [spriteAuthParams], 738 - [daytonaAuthParams], 739 - [denoAuthParams], 740 - [vercelAuthParams], 741 - ] = await Promise.all([ 742 - c.var.db 743 - .select() 744 - .from(spriteAuth) 745 - .where(eq(spriteAuth.sandboxId, record.id)) 746 - .execute(), 747 - c.var.db 748 - .select() 749 - .from(daytonaAuth) 750 - .where(eq(daytonaAuth.sandboxId, record.id)) 751 - .execute(), 752 - c.var.db 753 - .select() 754 - .from(denoAuth) 755 - .where(eq(denoAuth.sandboxId, record.id)) 756 - .execute(), 757 - c.var.db 758 - .select() 759 - .from(vercelAuth) 760 - .where(eq(vercelAuth.sandboxId, record.id)) 761 - .execute(), 762 - ]); 763 - 764 - if (!record.sandboxId) { 765 - sandbox = await createSandbox(record.provider as Provider, { 766 - id: record.id, 767 - daytonaApiKey: decrypt(daytonaAuthParams?.apiKey), 768 - organizationId: daytonaAuthParams?.organizationId, 769 - spriteToken: decrypt(spriteAuthParams?.spriteToken), 770 - denoDeployToken: decrypt(denoAuthParams?.deployToken), 771 - vercelApiToken: decrypt(vercelAuthParams?.vercelToken), 772 - vercelProjectId: vercelAuthParams?.projectId, 773 - vercelTeamId: vercelAuthParams?.teamId, 774 - }); 775 - const sandboxId = await sandbox.id(); 776 - await c.var.db 777 - .update(sandboxes) 778 - .set({ sandboxId }) 779 - .where(eq(sandboxes.id, record.id)) 780 - .execute(); 781 - record.sandboxId = sandboxId; 782 - } 783 - 784 - sandbox = await getSandboxById( 785 - record.provider as Provider, 786 - record.sandboxId!, 787 - { 788 - daytonaApiKey: decrypt(daytonaAuthParams?.apiKey), 789 - spriteToken: decrypt(spriteAuthParams?.spriteToken), 790 - denoDeployToken: decrypt(denoAuthParams?.deployToken), 791 - organizationId: daytonaAuthParams?.organizationId, 792 - vercelApiToken: decrypt(vercelAuthParams?.vercelToken), 793 - vercelProjectId: vercelAuthParams?.projectId, 794 - vercelTeamId: vercelAuthParams?.teamId, 795 - }, 471 + const auth = await getAuthParams(c.var.db, record.id); 472 + const sandbox = await resolveSandboxInstance( 473 + c.var.db, 474 + record, 475 + buildCredentials(auth), 796 476 ); 797 - 798 - if (!sandbox) { 799 - return c.json({ error: "Sandbox provider not supported" }, 400); 800 - } 801 477 802 478 c.var.db 803 479 .update(sandboxes) ··· 828 504 829 505 app.post("/v1/sandboxes/:sandboxId/pull-directory", async (c) => { 830 506 const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 831 - 832 - if (!record) { 833 - return c.json({ error: "Sandbox not found" }, 404); 834 - } 835 - 836 - let sandbox: BaseSandbox | null = null; 837 - 838 - if (!["daytona", "vercel", "deno", "sprites"].includes(record.provider)) { 507 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 508 + if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 839 509 return c.json({ error: "Sandbox provider not supported" }, 400); 840 510 } 841 511 842 - const [ 843 - [spriteAuthParams], 844 - [daytonaAuthParams], 845 - [denoAuthParams], 846 - [vercelAuthParams], 847 - ] = await Promise.all([ 848 - c.var.db 849 - .select() 850 - .from(spriteAuth) 851 - .where(eq(spriteAuth.sandboxId, record.id)) 852 - .execute(), 853 - c.var.db 854 - .select() 855 - .from(daytonaAuth) 856 - .where(eq(daytonaAuth.sandboxId, record.id)) 857 - .execute(), 858 - c.var.db 859 - .select() 860 - .from(denoAuth) 861 - .where(eq(denoAuth.sandboxId, record.id)) 862 - .execute(), 863 - c.var.db 864 - .select() 865 - .from(vercelAuth) 866 - .where(eq(vercelAuth.sandboxId, record.id)) 867 - .execute(), 868 - ]); 869 - 870 - if (!record.sandboxId) { 871 - sandbox = await createSandbox(record.provider as Provider, { 872 - id: record.id, 873 - daytonaApiKey: decrypt(daytonaAuthParams?.apiKey), 874 - organizationId: daytonaAuthParams?.organizationId, 875 - spriteToken: decrypt(spriteAuthParams?.spriteToken), 876 - denoDeployToken: decrypt(denoAuthParams?.deployToken), 877 - vercelApiToken: decrypt(vercelAuthParams?.vercelToken), 878 - vercelProjectId: vercelAuthParams?.projectId, 879 - vercelTeamId: vercelAuthParams?.teamId, 880 - }); 881 - const sandboxId = await sandbox.id(); 882 - await c.var.db 883 - .update(sandboxes) 884 - .set({ sandboxId }) 885 - .where(eq(sandboxes.id, record.id)) 886 - .execute(); 887 - record.sandboxId = sandboxId; 888 - } 889 - 890 - sandbox = await getSandboxById( 891 - record.provider as Provider, 892 - record.sandboxId!, 893 - { 894 - daytonaApiKey: decrypt(daytonaAuthParams?.apiKey), 895 - spriteToken: decrypt(spriteAuthParams?.spriteToken), 896 - denoDeployToken: decrypt(denoAuthParams?.deployToken), 897 - organizationId: daytonaAuthParams?.organizationId, 898 - vercelApiToken: decrypt(vercelAuthParams?.vercelToken), 899 - vercelProjectId: vercelAuthParams?.projectId, 900 - vercelTeamId: vercelAuthParams?.teamId, 901 - }, 512 + const auth = await getAuthParams(c.var.db, record.id); 513 + const sandbox = await resolveSandboxInstance( 514 + c.var.db, 515 + record, 516 + buildCredentials(auth), 902 517 ); 903 - 904 - if (!sandbox) { 905 - return c.json({ error: "Sandbox provider not supported" }, 400); 906 - } 907 518 908 519 const token = c.req.header("Authorization"); 909 520 const params = await c.req.json<PullDirectoryParams>(); 910 521 await pullSchema.parseAsync(params); 911 522 912 523 const outdir = crypto.randomUUID(); 913 - 914 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 -`; 915 525 await sandbox.sh`mkdir -p ${params.directoryPath} || sudo mkdir -p ${params.directoryPath}`; 916 526 await sandbox.sh`(shopt -s dotglob && cp -r /tmp/${outdir}/* ${params.directoryPath}) || (shopt -s dotglob && sudo cp -r /tmp/${outdir}/* ${params.directoryPath})`; ··· 925 535 926 536 app.post("/v1/sandboxes/:sandboxId/push-directory", async (c) => { 927 537 const record = await getSandbox(c.var.db, c.req.param("sandboxId")); 928 - 929 - if (!record) { 930 - return c.json({ error: "Sandbox not found" }, 404); 931 - } 932 - 933 - let sandbox: BaseSandbox | null = null; 934 - 935 - if (!["daytona", "vercel", "deno", "sprites"].includes(record.provider)) { 538 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 539 + if (!SUPPORTED_PROVIDERS.includes(record.provider)) { 936 540 return c.json({ error: "Sandbox provider not supported" }, 400); 937 541 } 938 542 939 - const [ 940 - [spriteAuthParams], 941 - [daytonaAuthParams], 942 - [denoAuthParams], 943 - [vercelAuthParams], 944 - ] = await Promise.all([ 945 - c.var.db 946 - .select() 947 - .from(spriteAuth) 948 - .where(eq(spriteAuth.sandboxId, record.id)) 949 - .execute(), 950 - c.var.db 951 - .select() 952 - .from(daytonaAuth) 953 - .where(eq(daytonaAuth.sandboxId, record.id)) 954 - .execute(), 955 - c.var.db 956 - .select() 957 - .from(denoAuth) 958 - .where(eq(denoAuth.sandboxId, record.id)) 959 - .execute(), 960 - c.var.db 961 - .select() 962 - .from(vercelAuth) 963 - .where(eq(vercelAuth.sandboxId, record.id)) 964 - .execute(), 965 - ]); 966 - 967 - if (!record.sandboxId) { 968 - sandbox = await createSandbox(record.provider as Provider, { 969 - id: record.id, 970 - daytonaApiKey: decrypt(daytonaAuthParams?.apiKey), 971 - organizationId: daytonaAuthParams?.organizationId, 972 - spriteToken: decrypt(spriteAuthParams?.spriteToken), 973 - denoDeployToken: decrypt(denoAuthParams?.deployToken), 974 - vercelApiToken: decrypt(vercelAuthParams?.vercelToken), 975 - vercelProjectId: vercelAuthParams?.projectId, 976 - vercelTeamId: vercelAuthParams?.teamId, 977 - }); 978 - const sandboxId = await sandbox.id(); 979 - await c.var.db 980 - .update(sandboxes) 981 - .set({ sandboxId }) 982 - .where(eq(sandboxes.id, record.id)) 983 - .execute(); 984 - record.sandboxId = sandboxId; 985 - } 986 - 987 - sandbox = await getSandboxById( 988 - record.provider as Provider, 989 - record.sandboxId!, 990 - { 991 - daytonaApiKey: decrypt(daytonaAuthParams?.apiKey), 992 - spriteToken: decrypt(spriteAuthParams?.spriteToken), 993 - denoDeployToken: decrypt(denoAuthParams?.deployToken), 994 - organizationId: daytonaAuthParams?.organizationId, 995 - vercelApiToken: decrypt(vercelAuthParams?.vercelToken), 996 - vercelProjectId: vercelAuthParams?.projectId, 997 - vercelTeamId: vercelAuthParams?.teamId, 998 - }, 543 + const auth = await getAuthParams(c.var.db, record.id); 544 + const sandbox = await resolveSandboxInstance( 545 + c.var.db, 546 + record, 547 + buildCredentials(auth), 999 548 ); 1000 - 1001 - if (!sandbox) { 1002 - return c.json({ error: "Sandbox provider not supported" }, 400); 1003 - } 1004 549 1005 550 const token = c.req.header("Authorization"); 1006 551 const params = await c.req.json<PushDirectoryParams>();
+97
apps/sandbox/src/lib/sandbox-helpers.ts
··· 1 + import { eq } from "drizzle-orm"; 2 + import { Context } from "../context.ts"; 3 + import { 4 + sandboxes, 5 + spriteAuth, 6 + daytonaAuth, 7 + denoAuth, 8 + vercelAuth, 9 + } from "../schema/mod.ts"; 10 + import { 11 + BaseSandbox, 12 + createSandbox, 13 + getSandboxById, 14 + Provider, 15 + SandboxOptions, 16 + } from "../providers/mod.ts"; 17 + import { SelectSandbox } from "../schema/sandboxes.ts"; 18 + import decrypt from "./decrypt.ts"; 19 + 20 + export interface AuthParams { 21 + spriteAuthParams?: { spriteToken?: string } | null; 22 + daytonaAuthParams?: { apiKey?: string; organizationId?: string } | null; 23 + denoAuthParams?: { deployToken?: string } | null; 24 + vercelAuthParams?: { 25 + vercelToken?: string; 26 + projectId?: string; 27 + teamId?: string; 28 + } | null; 29 + } 30 + 31 + export async function getAuthParams( 32 + db: Context["db"], 33 + sandboxDbId: string, 34 + ): Promise<AuthParams> { 35 + const [ 36 + [spriteAuthParams], 37 + [daytonaAuthParams], 38 + [denoAuthParams], 39 + [vercelAuthParams], 40 + ] = await Promise.all([ 41 + db 42 + .select() 43 + .from(spriteAuth) 44 + .where(eq(spriteAuth.sandboxId, sandboxDbId)) 45 + .execute(), 46 + db 47 + .select() 48 + .from(daytonaAuth) 49 + .where(eq(daytonaAuth.sandboxId, sandboxDbId)) 50 + .execute(), 51 + db 52 + .select() 53 + .from(denoAuth) 54 + .where(eq(denoAuth.sandboxId, sandboxDbId)) 55 + .execute(), 56 + db 57 + .select() 58 + .from(vercelAuth) 59 + .where(eq(vercelAuth.sandboxId, sandboxDbId)) 60 + .execute(), 61 + ]); 62 + return { spriteAuthParams, daytonaAuthParams, denoAuthParams, vercelAuthParams }; 63 + } 64 + 65 + export function buildCredentials(auth: AuthParams): SandboxOptions { 66 + return { 67 + daytonaApiKey: decrypt(auth.daytonaAuthParams?.apiKey), 68 + organizationId: auth.daytonaAuthParams?.organizationId, 69 + spriteToken: decrypt(auth.spriteAuthParams?.spriteToken), 70 + denoDeployToken: decrypt(auth.denoAuthParams?.deployToken), 71 + vercelApiToken: decrypt(auth.vercelAuthParams?.vercelToken), 72 + vercelProjectId: auth.vercelAuthParams?.projectId, 73 + vercelTeamId: auth.vercelAuthParams?.teamId, 74 + }; 75 + } 76 + 77 + export async function resolveSandboxInstance( 78 + db: Context["db"], 79 + record: SelectSandbox, 80 + credentials: SandboxOptions, 81 + ): Promise<BaseSandbox> { 82 + if (!record.sandboxId) { 83 + const sandbox = await createSandbox(record.provider as Provider, { 84 + id: record.id, 85 + ...credentials, 86 + }); 87 + const sandboxId = await sandbox.id(); 88 + await db 89 + .update(sandboxes) 90 + .set({ sandboxId }) 91 + .where(eq(sandboxes.id, record.id)) 92 + .execute(); 93 + record.sandboxId = sandboxId; 94 + } 95 + 96 + return getSandboxById(record.provider as Provider, record.sandboxId!, credentials); 97 + }