Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { COOKIE_KEYS } from '@/configs/cookies'
import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls'
import { getAuthContext } from '@/core/server/auth'
import { getTeamIdFromSlug } from '@/core/server/functions/team/get-team-id-from-slug'
import { createSandboxManagementAuth } from '@/core/shared/sandbox-management-auth.server'
import SandboxInspectView from '@/features/dashboard/sandbox/inspect/view'

const DEFAULT_ROOT_PATH = '/home/user'
Expand Down Expand Up @@ -41,13 +40,5 @@ export default async function SandboxInspectPage({
cookieStore.get(COOKIE_KEYS.SANDBOX_INSPECT_ROOT_PATH)?.value ||
DEFAULT_ROOT_PATH

return (
<SandboxInspectView
rootPath={rootPath}
sandboxManagementAuth={createSandboxManagementAuth(
authContext,
teamId.data
)}
/>
)
return <SandboxInspectView rootPath={rootPath} />
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { redirect } from 'next/navigation'
import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls'
import { getAuthContext } from '@/core/server/auth'
import { getTeamIdFromSlug } from '@/core/server/functions/team/get-team-id-from-slug'
import { createSandboxManagementAuth } from '@/core/shared/sandbox-management-auth.server'
import SandboxTerminalView from '@/features/dashboard/sandbox/terminal/view'

interface SandboxTerminalPageProps {
Expand Down Expand Up @@ -36,13 +35,5 @@ export default async function SandboxTerminalPage({
redirect(PROTECTED_URLS.DASHBOARD)
}

return (
<SandboxTerminalView
command={command}
sandboxManagementAuth={createSandboxManagementAuth(
authContext,
teamId.data
)}
/>
)
return <SandboxTerminalView command={command} userId={authContext.user.id} />
}
6 changes: 1 addition & 5 deletions src/app/dashboard/[teamSlug]/terminal/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
import { getAuthContext } from '@/core/server/auth'
import { infra } from '@/core/shared/clients/api'
import type { components as InfraComponents } from '@/core/shared/contracts/infra-api.types'
import { createSandboxManagementAuth } from '@/core/shared/sandbox-management-auth.server'
import { SandboxIdSchema } from '@/core/shared/schemas/api'
import DashboardTerminal from '@/features/dashboard/terminal/dashboard-terminal'
import { normalizeTerminalTemplate } from '@/features/dashboard/terminal/template'
Expand Down Expand Up @@ -132,11 +131,8 @@ export default async function TeamTerminalPage({
sandboxId: terminalSandboxId,
template: terminalTemplate,
}}
sandboxManagementAuth={createSandboxManagementAuth(
authContext,
team.id
)}
teamSlug={team.slug}
userId={authContext.user.id}
/>
</div>
)
Expand Down
204 changes: 203 additions & 1 deletion src/core/server/api/routers/sandbox.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TRPCError } from '@trpc/server'
import { millisecondsInDay } from 'date-fns/constants'
import { Sandbox } from 'e2b'
import { Sandbox, TimeoutError } from 'e2b'
import { z } from 'zod'
import { authHeaders } from '@/configs/api'
import {
Expand All @@ -17,8 +18,10 @@ import { withTeamAuthedRequestRepository } from '@/core/server/api/middlewares/r
import { createTRPCRouter } from '@/core/server/trpc/init'
import { protectedTeamProcedure } from '@/core/server/trpc/procedures'
import { SandboxIdSchema } from '@/core/shared/schemas/api'
import { SANDBOX_RESUME_TIMEOUT_MS } from '@/features/dashboard/sandbox/inspect/constants'
import { SANDBOX_MONITORING_METRICS_RETENTION_MS } from '@/features/dashboard/sandbox/monitoring/utils/constants'
import { TERMINAL_SANDBOX_TIMEOUT_MS } from '@/features/dashboard/terminal/constants'
import { normalizeTerminalTemplate } from '@/features/dashboard/terminal/template'

const sandboxRepositoryProcedure = protectedTeamProcedure.use(
withTeamAuthedRequestRepository(
Expand Down Expand Up @@ -209,6 +212,205 @@ export const sandboxRouter = createTRPCRouter({

// MUTATIONS

// Runs the control-plane create/connect server-side so the user's
// account-level access token never reaches the browser. Returns only the
// sandbox-scoped envd credentials the client needs for PTY access.
openTerminal: protectedTeamProcedure
.input(
z.object({
template: z.string().min(1, 'Template is required'),
sandboxId: SandboxIdSchema.optional(),
requestTimeoutMs: z.number().int().positive().optional(),
})
)
.mutation(async ({ ctx, input }) => {
const { sandboxId, template, requestTimeoutMs } = input
const { session, teamId } = ctx

const normalizedTemplate = normalizeTerminalTemplate(template)
if (!normalizedTemplate) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Invalid terminal template',
})
}

const connectionOpts = {
apiUrl: process.env.NEXT_PUBLIC_INFRA_API_URL,
domain: process.env.NEXT_PUBLIC_E2B_DOMAIN,
sandboxUrl: process.env.NEXT_PUBLIC_E2B_SANDBOX_URL,
headers: authHeaders(session.access_token, teamId),
}

try {
let resolvedSandboxId: string
if (sandboxId) {
const sandbox = await Sandbox.connect(sandboxId, {
...connectionOpts,
timeoutMs: TERMINAL_SANDBOX_TIMEOUT_MS,
requestTimeoutMs,
})
resolvedSandboxId = sandbox.sandboxId
} else {
const sandbox = await Sandbox.create(normalizedTemplate, {
...connectionOpts,
timeoutMs: TERMINAL_SANDBOX_TIMEOUT_MS,
requestTimeoutMs,
lifecycle: {
onTimeout: 'pause',
autoResume: true,
},
metadata: {
source: 'dashboard-terminal',
template: normalizedTemplate,
userId: session.user.id,
},
})
resolvedSandboxId = sandbox.sandboxId
}

// `Sandbox.create`/`connect` build a full SDK instance but only expose
// the sandbox id/domain publicly; fetch the envd credentials via the
// public info endpoint rather than reading the SDK's internal fields.
// Kept inside this try (with the same requestTimeoutMs) so a stalled
// GET times out promptly and is normalized like the connect timeout.
const info = await Sandbox.getFullInfo(resolvedSandboxId, {
...connectionOpts,
requestTimeoutMs,
})

// `envdAccessToken` is absent for `secure: false` sandboxes, whose envd
// is reachable without the `X-Access-Token` header — pass it through
// as-is rather than treating its absence as a failure.
return {
sandboxId: resolvedSandboxId,
sandboxDomain: info.sandboxDomain,
envdVersion: info.envdVersion,
envdAccessToken: info.envdAccessToken,
}
} catch (error) {
if (error instanceof TimeoutError) {
throw new TRPCError({
code: 'TIMEOUT',
message: sandboxId
? 'Timed out connecting to terminal sandbox'
: 'Timed out creating terminal sandbox',
cause: error,
})
}

throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: sandboxId
? 'Failed to connect to terminal sandbox'
: 'Failed to create terminal sandbox',
cause: error,
})
}
}),

// Explicit, user-triggered resume of a paused sandbox for the inspect view.
// The control-plane connect (which resumes + sets TTL) runs server-side so
// the account token never reaches the browser; returns the sandbox-scoped
// envd credentials the client uses to rebuild its envd-only client.
resume: protectedTeamProcedure
.input(
z.object({
sandboxId: SandboxIdSchema,
requestTimeoutMs: z.number().int().positive().optional(),
})
)
.mutation(async ({ ctx, input }) => {
const { sandboxId, requestTimeoutMs } = input
const { session, teamId } = ctx

const connectionOpts = {
apiUrl: process.env.NEXT_PUBLIC_INFRA_API_URL,
domain: process.env.NEXT_PUBLIC_E2B_DOMAIN,
sandboxUrl: process.env.NEXT_PUBLIC_E2B_SANDBOX_URL,
headers: authHeaders(session.access_token, teamId),
}

try {
await Sandbox.connect(sandboxId, {
...connectionOpts,
timeoutMs: SANDBOX_RESUME_TIMEOUT_MS,
requestTimeoutMs: requestTimeoutMs ?? SANDBOX_RESUME_TIMEOUT_MS,
})

const info = await Sandbox.getFullInfo(sandboxId, {
...connectionOpts,
requestTimeoutMs: requestTimeoutMs ?? SANDBOX_RESUME_TIMEOUT_MS,
})

return {
sandboxId,
sandboxDomain: info.sandboxDomain,
envdVersion: info.envdVersion,
envdAccessToken: info.envdAccessToken,
}
} catch (error) {
if (error instanceof TimeoutError) {
throw new TRPCError({
code: 'TIMEOUT',
message: 'Timed out resuming sandbox',
cause: error,
})
}

throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to resume sandbox',
cause: error,
})
}
}),

// Explicit, user-triggered pause of a running sandbox. Uses the SDK's
// control-plane pause (which snapshots and pauses) server-side so the
// account token never reaches the browser.
pause: protectedTeamProcedure
.input(
z.object({
sandboxId: SandboxIdSchema,
requestTimeoutMs: z.number().int().positive().optional(),
})
)
.mutation(async ({ ctx, input }) => {
const { sandboxId, requestTimeoutMs } = input
const { session, teamId } = ctx

const connectionOpts = {
apiUrl: process.env.NEXT_PUBLIC_INFRA_API_URL,
domain: process.env.NEXT_PUBLIC_E2B_DOMAIN,
sandboxUrl: process.env.NEXT_PUBLIC_E2B_SANDBOX_URL,
headers: authHeaders(session.access_token, teamId),
}

try {
// Returns false when the sandbox was already paused, which we treat
// as success since the desired end state is reached.
await Sandbox.pause(sandboxId, {
...connectionOpts,
...(requestTimeoutMs ? { requestTimeoutMs } : {}),
})
} catch (error) {
if (error instanceof TimeoutError) {
throw new TRPCError({
code: 'TIMEOUT',
message: 'Timed out pausing sandbox',
cause: error,
})
}

throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to pause sandbox',
cause: error,
})
}
}),

killTerminalPty: protectedTeamProcedure
.input(
z.object({
Expand Down
54 changes: 54 additions & 0 deletions src/core/shared/create-envd-sandbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Sandbox, { type SandboxConnectOpts } from 'e2b'

/**
* Sandbox-scoped credentials needed to talk to a sandbox's envd daemon
* (filesystem + PTY). These are safe to expose to the browser: they grant
* access to a single sandbox only, unlike the account-level access token.
*/
export interface EnvdSandboxParams {
sandboxId: string
sandboxDomain?: string | null
envdVersion: string
// Optional: `secure: false` sandboxes expose envd without an access token.
envdAccessToken?: string
domain?: string
sandboxUrl?: string
trafficAccessToken?: string
}

/**
* The {@link Sandbox} constructor is marked `@internal` in the SDK type defs
* (the public entry points are `Sandbox.create`/`Sandbox.connect`, both of
* which make a control-plane call that requires the account-level access
* token). We deliberately bypass those and build an envd-only instance
* directly from sandbox-scoped credentials: the constructor performs NO
* network call and authenticates every `files.*`/`pty.*` request with the
* `envdAccessToken` alone.
*
* The cast is isolated here so the rest of the codebase never touches the
* internal API. The option object is typed against the exported
* `SandboxConnectOpts`, so an SDK field rename surfaces as a compile error.
*/
type EnvdSandboxConstructor = new (
opts: SandboxConnectOpts & {
sandboxId: string
sandboxDomain?: string
envdVersion: string
envdAccessToken?: string
trafficAccessToken?: string
}
) => Sandbox

export function createEnvdSandbox(params: EnvdSandboxParams): Sandbox {
const SandboxCtor = Sandbox as unknown as EnvdSandboxConstructor

return new SandboxCtor({
sandboxId: params.sandboxId,
sandboxDomain: params.sandboxDomain ?? undefined,
envdVersion: params.envdVersion,
envdAccessToken: params.envdAccessToken,
trafficAccessToken: params.trafficAccessToken,
domain: params.domain,
sandboxUrl: params.sandboxUrl,
})
}
15 changes: 0 additions & 15 deletions src/core/shared/sandbox-management-auth.server.ts

This file was deleted.

4 changes: 0 additions & 4 deletions src/core/shared/sandbox-management-auth.ts

This file was deleted.

2 changes: 2 additions & 0 deletions src/features/dashboard/sandbox/header/controls.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import KillButton from './kill-button'
import PauseButton from './pause-button'

export default function SandboxDetailsControls() {
return (
<div className="flex items-center gap-2 md:pb-2">
<PauseButton />
<KillButton />
</div>
)
Expand Down
Loading
Loading