diff --git a/.changeset/cli-template-list-api-key.md b/.changeset/cli-template-list-api-key.md new file mode 100644 index 0000000000..a16443d3a8 --- /dev/null +++ b/.changeset/cli-template-list-api-key.md @@ -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. diff --git a/packages/cli/src/api.ts b/packages/cli/src/api.ts index 29b39a50e1..bcbfe2393f 100644 --- a/packages/cli/src/api.ts +++ b/packages/cli/src/api.ts @@ -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() { diff --git a/packages/cli/src/commands/template/create.ts b/packages/cli/src/commands/template/create.ts index 2480b87ea5..fc64317144 100644 --- a/packages/cli/src/commands/template/create.ts +++ b/packages/cli/src/commands/template/create.ts @@ -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, @@ -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 diff --git a/packages/cli/src/commands/template/list.ts b/packages/cli/src/commands/template/list.ts index c0a934ae7c..abe40ab0cf 100644 --- a/packages/cli/src/commands/template/list.ts +++ b/packages/cli/src/commands/template/list.ts @@ -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' @@ -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() process.stdout.write('\n') const templates = await listSandboxTemplates({ diff --git a/packages/cli/tests/commands/template/create.test.ts b/packages/cli/tests/commands/template/create.test.ts new file mode 100644 index 0000000000..4c877d2ef8 --- /dev/null +++ b/packages/cli/tests/commands/template/create.test.ts @@ -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' + ) + } + 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 { + // 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, + }) +}