Skip to content
Draft
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
21 changes: 21 additions & 0 deletions packages/cli/src/commands/template/generators/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,27 @@ export const languageDisplay = {
[Language.PythonAsync]: 'Python (async)',
}

/**
* Choices for the interactive target language prompt.
*/
export const languageChoices = [
{
name: languageDisplay[Language.TypeScript],
value: Language.TypeScript,
description: 'Generate .ts files for JavaScript/TypeScript projects',
},
{
name: languageDisplay[Language.PythonSync],
value: Language.PythonSync,
description: 'Generate synchronous Python template files',
},
{
name: languageDisplay[Language.PythonAsync],
value: Language.PythonAsync,
description: 'Generate asynchronous Python template files',
},
]

export interface TemplateJSON {
fromImage?: string
fromTemplate?: string
Expand Down
20 changes: 2 additions & 18 deletions packages/cli/src/commands/template/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
generateAndWriteTemplateFiles,
GeneratedFiles,
Language,
languageChoices,
languageDisplay,
} from './generators'
import { generateReadmeContent } from './generators/handlebars'
Expand Down Expand Up @@ -202,24 +203,7 @@ export const initCommand = new commander.Command('init')
} else {
language = await select({
message: 'Select target language for template files:',
choices: [
{
name: languageDisplay[Language.TypeScript],
value: Language.TypeScript,
description:
'Generate .ts files for JavaScript/TypeScript projects',
},
{
name: languageDisplay[Language.PythonSync],
value: Language.PythonSync,
description: 'Generate synchronous Python template files',
},
{
name: languageDisplay[Language.PythonAsync],
value: Language.PythonAsync,
description: 'Generate asynchronous Python template files',
},
],
choices: languageChoices,
default: Language.TypeScript,
})
}
Expand Down
20 changes: 2 additions & 18 deletions packages/cli/src/commands/template/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { validateTemplateName } from '../../utils/templateName'
import {
generateAndWriteTemplateFiles,
Language,
languageChoices,
languageDisplay,
} from './generators'

Expand Down Expand Up @@ -211,24 +212,7 @@ export const migrateCommand = new commander.Command('migrate')
// Prompt for language selection
language = await select({
message: 'Select target language for Template SDK:',
choices: [
{
name: languageDisplay[Language.TypeScript],
value: Language.TypeScript,
description:
'Generate .ts files for JavaScript/TypeScript projects',
},
{
name: languageDisplay[Language.PythonSync],
value: Language.PythonSync,
description: 'Generate synchronous Python template files',
},
{
name: languageDisplay[Language.PythonAsync],
value: Language.PythonAsync,
description: 'Generate asynchronous Python template files',
},
],
choices: languageChoices,
default: Language.TypeScript,
})
}
Expand Down
4 changes: 0 additions & 4 deletions packages/cli/src/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,6 @@ export function asLocalRelative(absolutePathInLocal?: string) {
return asLocal('./' + cwdRelative(absolutePathInLocal))
}

export function asBuildLogs(content: string) {
return chalk.default.blueBright(content)
}

export function withUnderline(content: string) {
return chalk.default.underline(content)
}
Expand Down
129 changes: 50 additions & 79 deletions packages/js-sdk/src/sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,46 +750,7 @@ export class Sandbox extends SandboxApi {
* @returns URL for uploading file.
*/
async uploadUrl(path?: string, opts?: SandboxUrlOpts) {
opts = opts ?? {}

const useSignature = !!this.envdAccessToken

if (!useSignature && opts.useSignatureExpiration != undefined) {
throw new InvalidArgumentError(
'Signature expiration can be used only when sandbox is created as secured.'
)
}

let username = opts.user
if (
username == undefined &&
compareVersions(this.envdApi.version, ENVD_DEFAULT_USER) < 0
) {
username = defaultUsername
}

const filePath = path ?? ''
const fileUrl = this.fileUrl(filePath, username)

if (useSignature) {
const url = new URL(fileUrl)
const sig = await getSignature({
path: filePath,
operation: 'write',
user: username,
expirationInSeconds: opts.useSignatureExpiration,
envdAccessToken: this.envdAccessToken,
})

url.searchParams.set('signature', sig.signature)
if (sig.expiration) {
url.searchParams.set('signature_expiration', sig.expiration.toString())
}

return url.toString()
}

return fileUrl
return await this.fileOperationUrl(path ?? '', 'write', opts)
}

/**
Expand All @@ -802,45 +763,7 @@ export class Sandbox extends SandboxApi {
* @returns URL for downloading file.
*/
async downloadUrl(path: string, opts?: SandboxUrlOpts) {
opts = opts ?? {}

const useSignature = !!this.envdAccessToken

if (!useSignature && opts.useSignatureExpiration != undefined) {
throw new InvalidArgumentError(
'Signature expiration can be used only when sandbox is created as secured.'
)
}

let username = opts.user
if (
username == undefined &&
compareVersions(this.envdApi.version, ENVD_DEFAULT_USER) < 0
) {
username = defaultUsername
}

const fileUrl = this.fileUrl(path, username)

if (useSignature) {
const url = new URL(fileUrl)
const sig = await getSignature({
path,
operation: 'read',
user: username,
expirationInSeconds: opts.useSignatureExpiration,
envdAccessToken: this.envdAccessToken,
})

url.searchParams.set('signature', sig.signature)
if (sig.expiration) {
url.searchParams.set('signature_expiration', sig.expiration.toString())
}

return url.toString()
}

return fileUrl
return await this.fileOperationUrl(path, 'read', opts)
}

/**
Expand Down Expand Up @@ -896,6 +819,54 @@ export class Sandbox extends SandboxApi {
) as ConnectionOpts & T
}

/**
* Build the `/files` URL for a read or write operation, signing it when the
* sandbox is secured.
*/
private async fileOperationUrl(
path: string,
operation: 'read' | 'write',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-1/T-11 — the Python mirror of this helper takes the named Operation alias from signature.py, so the JS side should have the same named type rather than a third inline copy of 'read' | 'write' (the other copy is in signature.ts). Export export type Operation = 'read' | 'write' from src/sandbox/signature.ts, use it in getSignature, and reference it here so both SDKs write the vocabulary down once.

opts?: SandboxUrlOpts
): Promise<string> {
const useSignature = !!this.envdAccessToken

if (!useSignature && opts?.useSignatureExpiration != undefined) {
throw new InvalidArgumentError(
'Signature expiration can be used only when sandbox is created as secured.'
)
}

let username = opts?.user
if (
username == undefined &&
compareVersions(this.envdApi.version, ENVD_DEFAULT_USER) < 0
) {
username = defaultUsername
}

const fileUrl = this.fileUrl(path, username)

if (!useSignature) {
return fileUrl
}

const url = new URL(fileUrl)
const sig = await getSignature({
path,
operation,
user: username,
expirationInSeconds: opts?.useSignatureExpiration,
envdAccessToken: this.envdAccessToken,
})

url.searchParams.set('signature', sig.signature)
if (sig.expiration) {
url.searchParams.set('signature_expiration', sig.expiration.toString())
}

return url.toString()
}

private fileUrl(path: string | undefined, username: string | undefined) {
const url = new URL('/files', this.envdDirectUrl)

Expand Down
52 changes: 22 additions & 30 deletions packages/js-sdk/src/secret.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { FetchResponse } from 'openapi-fetch'

import { ApiClient, components, handleApiError } from './api'
import {
ClientFactory,
Expand Down Expand Up @@ -100,6 +102,23 @@ function convertSecretInfo(
}
}

/**
* Convert a response returning a single secret into {@link SecretInfo},
* throwing for error responses and empty bodies.
*/
function secretInfoFromResponse(res: FetchResponse<any, any, any>): SecretInfo {
const err = handleApiError(res, SecretError)
if (err) {
throw err
}

if (!res.data) {
throw new Error('Response data is missing')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-57/T-58 — one base class per domain, and prefer the specific error over a generic one: secret failures must be SecretError (already imported here), never a bare Error, or a caller's catch (e) { if (e instanceof SecretError) } misses this path entirely. T-62 also wants the message to say what to do, not just what failed.

Suggested change
throw new Error('Response data is missing')
throw new SecretError(
'The API returned a success status with an empty body. Retry the request, and contact support@e2b.dev if it keeps happening.'
)

}

return convertSecretInfo(res.data)
}

/**
* Paginator for listing secrets.
*
Expand Down Expand Up @@ -182,16 +201,7 @@ export class Secret extends ClientFactory {
signal: config.getSignal(apiOpts?.requestTimeoutMs, apiOpts?.signal),
})

const err = handleApiError(res, SecretError)
if (err) {
throw err
}

if (!res.data) {
throw new Error('Response data is missing')
}

return convertSecretInfo(res.data)
return secretInfoFromResponse(res)
}

/**
Expand Down Expand Up @@ -230,16 +240,7 @@ export class Secret extends ClientFactory {
throw new SecretNotFoundError(`Secret ${secret} not found`)
}

const err = handleApiError(res, SecretError)
if (err) {
throw err
}

if (!res.data) {
throw new Error('Response data is missing')
}

return convertSecretInfo(res.data)
return secretInfoFromResponse(res)
}

/**
Expand Down Expand Up @@ -272,16 +273,7 @@ export class Secret extends ClientFactory {
throw new SecretNotFoundError(`Secret ${secret} not found`)
}

const err = handleApiError(res, SecretError)
if (err) {
throw err
}

if (!res.data) {
throw new Error('Response data is missing')
}

return convertSecretInfo(res.data)
return secretInfoFromResponse(res)
}

/**
Expand Down
5 changes: 2 additions & 3 deletions packages/js-sdk/src/volume/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ import createClient from 'openapi-fetch'
import type { components, paths } from './schema.gen'
import { defaultHeaders, getEnvVar } from '../api/metadata'
import { createApiFetch } from '../api/http2'
import { buildRequestSignal } from '../connectionConfig'
import { buildRequestSignal, REQUEST_TIMEOUT_MS } from '../connectionConfig'
import { createApiLogger, Logger } from '../logs'
import type { Volume } from './index'

const REQUEST_TIMEOUT_MS = 60_000 // 60 seconds
const FILE_TIMEOUT_MS = 3_600_000 // 1 hour

export interface VolumeApiOpts {
Expand Down Expand Up @@ -135,5 +134,5 @@ class VolumeApiClient {
}
}

export type { components as VolumeApiComponents, paths as VolumeApiPaths }
export type { components as VolumeApiComponents }
export { VolumeApiClient, FILE_TIMEOUT_MS }
Loading
Loading