Skip to content
5 changes: 5 additions & 0 deletions .changeset/cli-template-list-api-key.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@e2b/cli': patch
---

`e2b template list` and `e2b template create` now authenticate with `E2B_API_KEY` instead of requiring `E2B_ACCESS_TOKEN`. `E2B_ACCESS_TOKEN` is deprecated, so commands whose endpoints accept either credential now use the API key. This also unblocks API-key-only environments (e.g. CI/CD) that previously could not create or list templates.
52 changes: 20 additions & 32 deletions packages/cli/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,28 @@ export let apiKey = process.env.E2B_API_KEY
export let accessToken = process.env.E2B_ACCESS_TOKEN
export const teamId = process.env.E2B_TEAM_ID

const authErrorBox = (keyName: string) => {
let link
let msg
switch (keyName) {
case 'E2B_API_KEY':
link = 'https://e2b.dev/dashboard?tab=keys'
msg = 'API key'
break
case 'E2B_ACCESS_TOKEN':
link = 'https://e2b.dev/dashboard?tab=personal'
msg = 'access token'
break
}
// throwing error in default in switch statement results in unreachable code,
// so we need to check if link and msg are defined here instead
if (!link || !msg) {
throw new Error(`Unknown key name: ${keyName}`)
}
return boxen.default(
`You must be logged in to use this command. Run ${asBold('e2b auth login')}.
const authErrorBox = (keyName: 'E2B_API_KEY' | 'E2B_ACCESS_TOKEN') => {
const link =
keyName === 'E2B_API_KEY'
? 'https://e2b.dev/dashboard?tab=keys'
: 'https://e2b.dev/dashboard?tab=personal'
const msg = keyName === 'E2B_API_KEY' ? 'API key' : 'access token'
const body = `You must be logged in to use this command. Run ${asBold(
'e2b auth login'
)}.

If you are seeing this message in CI/CD you may need to set the ${asBold(
`${keyName}`
)} environment variable.
Visit ${asPrimary(link)} to get the ${msg}.`,
{
width: 70,
float: 'center',
padding: 0.5,
margin: 1,
borderStyle: 'round',
borderColor: 'redBright',
}
)
keyName
)} environment variable.
Visit ${asPrimary(link)} to get the ${msg}.`
return boxen.default(body, {
width: 70,
float: 'center',
padding: 0.5,
margin: 1,
borderStyle: 'round',
borderColor: 'redBright',
})
}

export function ensureAPIKey() {
Expand Down
4 changes: 1 addition & 3 deletions packages/cli/src/commands/template/create.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as boxen from 'boxen'
import * as commander from 'commander'
import { defaultBuildLogger, Template, TemplateClass } from 'e2b'
import { connectionConfig, ensureAccessToken, ensureAPIKey } from 'src/api'
import { connectionConfig, ensureAPIKey } from 'src/api'
import {
defaultDockerfileName,
fallbackDockerfileName,
Expand Down Expand Up @@ -68,8 +68,6 @@ export const createCommand = new commander.Command('create')
}
) => {
try {
// Ensure we have access token
ensureAccessToken()
process.stdout.write('\n')

// Validate template name
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/template/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as e2b from 'e2b'

import { listAliases } from '../../utils/format'
import { sortTemplatesAliases } from 'src/utils/templateSort'
import { client, ensureAccessToken, resolveTeamId } from 'src/api'
import { client, ensureAPIKey, resolveTeamId } from 'src/api'
import { teamOption } from '../../options'
import { handleE2BRequestError } from '../../utils/errors'

Expand All @@ -16,7 +16,7 @@ export const listCommand = new commander.Command('list')
.action(async (opts: { team: string; format: string }) => {
try {
const format = opts.format || 'pretty'
ensureAccessToken()
ensureAPIKey()
Comment thread
mishushakov marked this conversation as resolved.
process.stdout.write('\n')

const templates = await listSandboxTemplates({
Expand Down
71 changes: 71 additions & 0 deletions packages/cli/tests/commands/template/create.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { spawnSync } from 'node:child_process'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { afterAll, beforeAll, describe, expect, test } from 'vitest'

const apiKey = process.env.E2B_API_KEY
const domain = process.env.E2B_DOMAIN || 'e2b.app'

const cliPath = path.join(process.cwd(), 'dist', 'index.js')
const templateName = `cli-create-api-key-test-${Date.now()}`

describe('template create cli backend integration', () => {
let testDir: string

beforeAll(async () => {
if (!apiKey) {
throw new Error(
'E2B_API_KEY must be set to run template create backend tests'
)
}
Comment thread
mishushakov marked this conversation as resolved.
testDir = await fs.mkdtemp('e2b-create-test-')
await fs.writeFile(
path.join(testDir, 'e2b.Dockerfile'),
'FROM ubuntu:latest\n'
)
})

afterAll(async () => {
if (!testDir) return
runCli(['template', 'delete', '--yes', templateName])
await fs.rm(testDir, { recursive: true, force: true })
})

test(
'template create succeeds with E2B_API_KEY alone (no E2B_ACCESS_TOKEN)',
{ timeout: 300_000 },
() => {
const result = runCli([
'template',
'create',
templateName,
'--path',
testDir,
])
const output = String(result.stdout || '') + String(result.stderr || '')

expect(result.status, output).toBe(0)
// Success marker printed by create.ts on a finished build; the failure
// path prints "❌ Template build failed." instead.
expect(output).toContain('✅ Building sandbox template')
expect(output).not.toContain('❌ Template build failed')
// Auth never fell through to the access-token error box.
expect(output).not.toMatch(/You must be logged in/)
}
)
})

function runCli(args: string[]): ReturnType<typeof spawnSync> {
// Intentionally exclude E2B_ACCESS_TOKEN from the child env so this test
// verifies the API-key-only auth path end-to-end.
return spawnSync('node', [cliPath, ...args], {
env: {
PATH: process.env.PATH,
HOME: process.env.HOME,
E2B_DOMAIN: domain,
E2B_API_KEY: apiKey,
},
encoding: 'utf8',
timeout: 300_000,
})
}
Loading