1+ import { TRPCError } from '@trpc/server'
12import { millisecondsInDay } from 'date-fns/constants'
2- import { Sandbox } from 'e2b'
3+ import { Sandbox , TimeoutError } from 'e2b'
34import { z } from 'zod'
45import { authHeaders } from '@/configs/api'
56import {
@@ -17,8 +18,10 @@ import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/r
1718import { createTRPCRouter } from '@/core/server/trpc/init'
1819import { protectedTeamProcedure } from '@/core/server/trpc/procedures'
1920import { SandboxIdSchema } from '@/core/shared/schemas/api'
21+ import { SANDBOX_RESUME_TIMEOUT_MS } from '@/features/dashboard/sandbox/inspect/constants'
2022import { SANDBOX_MONITORING_METRICS_RETENTION_MS } from '@/features/dashboard/sandbox/monitoring/utils/constants'
2123import { TERMINAL_SANDBOX_TIMEOUT_MS } from '@/features/dashboard/terminal/constants'
24+ import { normalizeTerminalTemplate } from '@/features/dashboard/terminal/template'
2225
2326const sandboxRepositoryProcedure = protectedTeamProcedure . use (
2427 withTeamAuthedRequestRepository (
@@ -209,6 +212,205 @@ export const sandboxRouter = createTRPCRouter({
209212
210213 // MUTATIONS
211214
215+ // Runs the control-plane create/connect server-side so the user's
216+ // account-level access token never reaches the browser. Returns only the
217+ // sandbox-scoped envd credentials the client needs for PTY access.
218+ openTerminal : protectedTeamProcedure
219+ . input (
220+ z . object ( {
221+ template : z . string ( ) . min ( 1 , 'Template is required' ) ,
222+ sandboxId : SandboxIdSchema . optional ( ) ,
223+ requestTimeoutMs : z . number ( ) . int ( ) . positive ( ) . optional ( ) ,
224+ } )
225+ )
226+ . mutation ( async ( { ctx, input } ) => {
227+ const { sandboxId, template, requestTimeoutMs } = input
228+ const { session, teamId } = ctx
229+
230+ const normalizedTemplate = normalizeTerminalTemplate ( template )
231+ if ( ! normalizedTemplate ) {
232+ throw new TRPCError ( {
233+ code : 'BAD_REQUEST' ,
234+ message : 'Invalid terminal template' ,
235+ } )
236+ }
237+
238+ const connectionOpts = {
239+ apiUrl : process . env . NEXT_PUBLIC_INFRA_API_URL ,
240+ domain : process . env . NEXT_PUBLIC_E2B_DOMAIN ,
241+ sandboxUrl : process . env . NEXT_PUBLIC_E2B_SANDBOX_URL ,
242+ headers : authHeaders ( session . access_token , teamId ) ,
243+ }
244+
245+ try {
246+ let resolvedSandboxId : string
247+ if ( sandboxId ) {
248+ const sandbox = await Sandbox . connect ( sandboxId , {
249+ ...connectionOpts ,
250+ timeoutMs : TERMINAL_SANDBOX_TIMEOUT_MS ,
251+ requestTimeoutMs,
252+ } )
253+ resolvedSandboxId = sandbox . sandboxId
254+ } else {
255+ const sandbox = await Sandbox . create ( normalizedTemplate , {
256+ ...connectionOpts ,
257+ timeoutMs : TERMINAL_SANDBOX_TIMEOUT_MS ,
258+ requestTimeoutMs,
259+ lifecycle : {
260+ onTimeout : 'pause' ,
261+ autoResume : true ,
262+ } ,
263+ metadata : {
264+ source : 'dashboard-terminal' ,
265+ template : normalizedTemplate ,
266+ userId : session . user . id ,
267+ } ,
268+ } )
269+ resolvedSandboxId = sandbox . sandboxId
270+ }
271+
272+ // `Sandbox.create`/`connect` build a full SDK instance but only expose
273+ // the sandbox id/domain publicly; fetch the envd credentials via the
274+ // public info endpoint rather than reading the SDK's internal fields.
275+ // Kept inside this try (with the same requestTimeoutMs) so a stalled
276+ // GET times out promptly and is normalized like the connect timeout.
277+ const info = await Sandbox . getFullInfo ( resolvedSandboxId , {
278+ ...connectionOpts ,
279+ requestTimeoutMs,
280+ } )
281+
282+ // `envdAccessToken` is absent for `secure: false` sandboxes, whose envd
283+ // is reachable without the `X-Access-Token` header — pass it through
284+ // as-is rather than treating its absence as a failure.
285+ return {
286+ sandboxId : resolvedSandboxId ,
287+ sandboxDomain : info . sandboxDomain ,
288+ envdVersion : info . envdVersion ,
289+ envdAccessToken : info . envdAccessToken ,
290+ }
291+ } catch ( error ) {
292+ if ( error instanceof TimeoutError ) {
293+ throw new TRPCError ( {
294+ code : 'TIMEOUT' ,
295+ message : sandboxId
296+ ? 'Timed out connecting to terminal sandbox'
297+ : 'Timed out creating terminal sandbox' ,
298+ cause : error ,
299+ } )
300+ }
301+
302+ throw new TRPCError ( {
303+ code : 'INTERNAL_SERVER_ERROR' ,
304+ message : sandboxId
305+ ? 'Failed to connect to terminal sandbox'
306+ : 'Failed to create terminal sandbox' ,
307+ cause : error ,
308+ } )
309+ }
310+ } ) ,
311+
312+ // Explicit, user-triggered resume of a paused sandbox for the inspect view.
313+ // The control-plane connect (which resumes + sets TTL) runs server-side so
314+ // the account token never reaches the browser; returns the sandbox-scoped
315+ // envd credentials the client uses to rebuild its envd-only client.
316+ resume : protectedTeamProcedure
317+ . input (
318+ z . object ( {
319+ sandboxId : SandboxIdSchema ,
320+ requestTimeoutMs : z . number ( ) . int ( ) . positive ( ) . optional ( ) ,
321+ } )
322+ )
323+ . mutation ( async ( { ctx, input } ) => {
324+ const { sandboxId, requestTimeoutMs } = input
325+ const { session, teamId } = ctx
326+
327+ const connectionOpts = {
328+ apiUrl : process . env . NEXT_PUBLIC_INFRA_API_URL ,
329+ domain : process . env . NEXT_PUBLIC_E2B_DOMAIN ,
330+ sandboxUrl : process . env . NEXT_PUBLIC_E2B_SANDBOX_URL ,
331+ headers : authHeaders ( session . access_token , teamId ) ,
332+ }
333+
334+ try {
335+ await Sandbox . connect ( sandboxId , {
336+ ...connectionOpts ,
337+ timeoutMs : SANDBOX_RESUME_TIMEOUT_MS ,
338+ requestTimeoutMs : requestTimeoutMs ?? SANDBOX_RESUME_TIMEOUT_MS ,
339+ } )
340+
341+ const info = await Sandbox . getFullInfo ( sandboxId , {
342+ ...connectionOpts ,
343+ requestTimeoutMs : requestTimeoutMs ?? SANDBOX_RESUME_TIMEOUT_MS ,
344+ } )
345+
346+ return {
347+ sandboxId,
348+ sandboxDomain : info . sandboxDomain ,
349+ envdVersion : info . envdVersion ,
350+ envdAccessToken : info . envdAccessToken ,
351+ }
352+ } catch ( error ) {
353+ if ( error instanceof TimeoutError ) {
354+ throw new TRPCError ( {
355+ code : 'TIMEOUT' ,
356+ message : 'Timed out resuming sandbox' ,
357+ cause : error ,
358+ } )
359+ }
360+
361+ throw new TRPCError ( {
362+ code : 'INTERNAL_SERVER_ERROR' ,
363+ message : 'Failed to resume sandbox' ,
364+ cause : error ,
365+ } )
366+ }
367+ } ) ,
368+
369+ // Explicit, user-triggered pause of a running sandbox. Uses the SDK's
370+ // control-plane pause (which snapshots and pauses) server-side so the
371+ // account token never reaches the browser.
372+ pause : protectedTeamProcedure
373+ . input (
374+ z . object ( {
375+ sandboxId : SandboxIdSchema ,
376+ requestTimeoutMs : z . number ( ) . int ( ) . positive ( ) . optional ( ) ,
377+ } )
378+ )
379+ . mutation ( async ( { ctx, input } ) => {
380+ const { sandboxId, requestTimeoutMs } = input
381+ const { session, teamId } = ctx
382+
383+ const connectionOpts = {
384+ apiUrl : process . env . NEXT_PUBLIC_INFRA_API_URL ,
385+ domain : process . env . NEXT_PUBLIC_E2B_DOMAIN ,
386+ sandboxUrl : process . env . NEXT_PUBLIC_E2B_SANDBOX_URL ,
387+ headers : authHeaders ( session . access_token , teamId ) ,
388+ }
389+
390+ try {
391+ // Returns false when the sandbox was already paused, which we treat
392+ // as success since the desired end state is reached.
393+ await Sandbox . pause ( sandboxId , {
394+ ...connectionOpts ,
395+ ...( requestTimeoutMs ? { requestTimeoutMs } : { } ) ,
396+ } )
397+ } catch ( error ) {
398+ if ( error instanceof TimeoutError ) {
399+ throw new TRPCError ( {
400+ code : 'TIMEOUT' ,
401+ message : 'Timed out pausing sandbox' ,
402+ cause : error ,
403+ } )
404+ }
405+
406+ throw new TRPCError ( {
407+ code : 'INTERNAL_SERVER_ERROR' ,
408+ message : 'Failed to pause sandbox' ,
409+ cause : error ,
410+ } )
411+ }
412+ } ) ,
413+
212414 killTerminalPty : protectedTeamProcedure
213415 . input (
214416 z . object ( {
0 commit comments