Skip to content

Commit ac02df2

Browse files
committed
added support for secure flag to javascript sdk
1 parent 37cf0bc commit ac02df2

File tree

5 files changed

+259
-15
lines changed

5 files changed

+259
-15
lines changed

packages/js-sdk/src/envd/api.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,16 @@ class EnvdApiClient {
9898
readonly version: string | undefined
9999

100100
constructor(
101-
config: Pick<ConnectionConfig, 'apiUrl' | 'logger'>,
101+
config: Pick<ConnectionConfig, 'apiUrl' | 'logger' | 'accessToken'> & { fetch?: (request: Request) => ReturnType<typeof fetch>, headers?: Record<string, string> },
102102
metadata: {
103103
version?: string
104-
}
104+
},
105105
) {
106106
this.api = createClient({
107107
baseUrl: config.apiUrl,
108-
// keepalive: true, // TODO: Return keepalive
108+
fetch: config?.fetch,
109+
headers: config?.headers,
110+
keepalive: true,
109111
})
110112
this.version = metadata.version
111113

packages/js-sdk/src/sandbox/index.ts

+112-12
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { createRpcLogger } from '../logs'
1010
import { Commands, Pty } from './commands'
1111
import { Filesystem } from './filesystem'
1212
import { SandboxApi } from './sandboxApi'
13+
import crypto from 'crypto'
1314

1415
/**
1516
* Options for creating a new Sandbox.
@@ -21,6 +22,7 @@ export interface SandboxOpts extends ConnectionOpts {
2122
* @default {}
2223
*/
2324
metadata?: Record<string, string>
25+
2426
/**
2527
* Custom environment variables for the sandbox.
2628
*
@@ -30,13 +32,21 @@ export interface SandboxOpts extends ConnectionOpts {
3032
* @default {}
3133
*/
3234
envs?: Record<string, string>
35+
3336
/**
3437
* Timeout for the sandbox in **milliseconds**.
3538
* Maximum time a sandbox can be kept alive is 24 hours (86_400_000 milliseconds) for Pro users and 1 hour (3_600_000 milliseconds) for Hobby users.
3639
*
3740
* @default 300_000 // 5 minutes
3841
*/
3942
timeoutMs?: number
43+
44+
/**
45+
* Secure all traffic coming to the sandbox controller with auth token
46+
*
47+
* @default false
48+
*/
49+
secure?: boolean
4050
}
4151

4252
/**
@@ -86,6 +96,7 @@ export class Sandbox extends SandboxApi {
8696

8797
protected readonly connectionConfig: ConnectionConfig
8898
private readonly envdApiUrl: string
99+
private readonly envdAccessToken?: string
89100
private readonly envdApi: EnvdApiClient
90101

91102
/**
@@ -100,15 +111,16 @@ export class Sandbox extends SandboxApi {
100111
opts: Omit<SandboxOpts, 'timeoutMs' | 'envs' | 'metadata'> & {
101112
sandboxId: string
102113
envdVersion?: string
114+
envdAccessToken?: string
103115
}
104116
) {
105117
super()
106118

107119
this.sandboxId = opts.sandboxId
108120
this.connectionConfig = new ConnectionConfig(opts)
109-
this.envdApiUrl = `${
110-
this.connectionConfig.debug ? 'http' : 'https'
111-
}://${this.getHost(this.envdPort)}`
121+
122+
this.envdAccessToken = opts.envdAccessToken
123+
this.envdApiUrl = `${this.connectionConfig.debug ? 'http' : 'https'}://${this.getHost(this.envdPort)}`
112124

113125
const rpcTransport = createConnectTransport({
114126
baseUrl: this.envdApiUrl,
@@ -119,10 +131,18 @@ export class Sandbox extends SandboxApi {
119131
// connect-web doesn't allow to configure redirect option - https://github.com/connectrpc/connect-es/pull/1082
120132
// connect-web package uses redirect: "error" which is not supported in edge runtimes
121133
// E2B endpoints should be safe to use with redirect: "follow" https://github.com/e2b-dev/E2B/issues/531#issuecomment-2779492867
134+
135+
const headers = new Headers(options?.headers)
136+
if (this.envdAccessToken) {
137+
headers.append('X-Access-Token', this.envdAccessToken)
138+
}
139+
122140
options = {
123141
...(options ?? {}),
142+
headers: headers,
124143
redirect: 'follow',
125144
}
145+
126146
return fetch(url, options)
127147
},
128148
})
@@ -131,6 +151,8 @@ export class Sandbox extends SandboxApi {
131151
{
132152
apiUrl: this.envdApiUrl,
133153
logger: opts?.logger,
154+
accessToken: this.envdAccessToken,
155+
headers: this.envdAccessToken ? { 'X-Access-Token': this.envdAccessToken } : { },
134156
},
135157
{
136158
version: opts?.envdVersion,
@@ -193,7 +215,6 @@ export class Sandbox extends SandboxApi {
193215
: { template: this.defaultTemplate, sandboxOpts: templateOrOpts }
194216

195217
const config = new ConnectionConfig(sandboxOpts)
196-
197218
if (config.debug) {
198219
return new this({
199220
sandboxId: 'debug_sandbox_id',
@@ -233,9 +254,12 @@ export class Sandbox extends SandboxApi {
233254
opts?: Omit<SandboxOpts, 'metadata' | 'envs' | 'timeoutMs'>
234255
): Promise<InstanceType<S>> {
235256
const config = new ConnectionConfig(opts)
257+
//const info = await this.getInfo(sandboxId, opts)
236258

237-
const sbx = new this({ sandboxId, ...config }) as InstanceType<S>
238-
return sbx
259+
return new this(
260+
// { sandboxId, envdAccessToken: info.envdAccessToken, envdVersion: info.envdVersion, ...config }
261+
{ sandboxId, ...config }
262+
) as InstanceType<S>
239263
}
240264

241265
/**
@@ -344,26 +368,77 @@ export class Sandbox extends SandboxApi {
344368
*
345369
* @param path the directory where to upload the file, defaults to user's home directory.
346370
*
371+
* @param useSignature URL will be signed with the access token, this can be used for uploading files to the sandbox from a different environment (e.g. browser).
372+
*
373+
* @param signatureExpirationInSeconds URL will be signed with the access token, this can be used for uploading files to the sandbox from a different environment (e.g. browser).
374+
*
347375
* @returns URL for uploading file.
348376
*/
349-
uploadUrl(path?: string) {
350-
return this.fileUrl(path)
377+
uploadUrl(path?: string, useSignature?: boolean, signatureExpirationInSeconds?: number) {
378+
if (!this.envdAccessToken && (useSignature != undefined || signatureExpirationInSeconds != undefined)) {
379+
throw new Error('Signature can be used only when sandbox is spawned with secure option.')
380+
}
381+
382+
if (useSignature == undefined && signatureExpirationInSeconds != undefined) {
383+
throw new Error('Signature expiration can be used only when signature is set to true.')
384+
}
385+
386+
const fileUrl = this.fileUrl(path, defaultUsername)
387+
388+
if (useSignature) {
389+
const url = new URL(fileUrl)
390+
const sig = this.getSignature(path ?? '', 'write', defaultUsername, signatureExpirationInSeconds)
391+
392+
url.searchParams.set('signature', sig.signature)
393+
if (sig.expiration) {
394+
url.searchParams.set('signature_expiration', sig.expiration.toString())
395+
}
396+
397+
return url.toString()
398+
}
399+
400+
return fileUrl
351401
}
352402

353403
/**
354404
* Get the URL to download a file from the sandbox.
355405
*
356406
* @param path path to the file to download.
357407
*
408+
* @param useSignature URL will be signed with the access token, this can be used for uploading files to the sandbox from a different environment (e.g. browser).
409+
*
410+
* @param signatureExpirationInSeconds URL will be signed with the access token, this can be used for uploading files to the sandbox from a different environment (e.g. browser).
411+
*
358412
* @returns URL for downloading file.
359413
*/
360-
downloadUrl(path: string) {
361-
return this.fileUrl(path)
414+
downloadUrl(path: string, useSignature?: boolean, signatureExpirationInSeconds?: number) {
415+
if (!this.envdAccessToken && (useSignature != undefined || signatureExpirationInSeconds != undefined)) {
416+
throw new Error('Signature can be used only when sandbox is spawned with secure option.')
417+
}
418+
419+
if (useSignature == undefined && signatureExpirationInSeconds != undefined) {
420+
throw new Error('Signature expiration can be used only when signature is set to true.')
421+
}
422+
423+
const fileUrl = this.fileUrl(path, defaultUsername)
424+
425+
if (useSignature) {
426+
const url = new URL(fileUrl)
427+
const sig = this.getSignature(path, 'read', defaultUsername, signatureExpirationInSeconds)
428+
url.searchParams.set('signature', sig.signature)
429+
if (sig.expiration) {
430+
url.searchParams.set('signature_expiration', sig.expiration.toString())
431+
}
432+
433+
return url.toString()
434+
}
435+
436+
return fileUrl
362437
}
363438

364-
private fileUrl(path?: string) {
439+
private fileUrl(path?: string, username?: string) {
365440
const url = new URL('/files', this.envdApiUrl)
366-
url.searchParams.set('username', defaultUsername)
441+
url.searchParams.set('username', username ?? defaultUsername)
367442
if (path) {
368443
url.searchParams.set('path', path)
369444
}
@@ -384,4 +459,29 @@ export class Sandbox extends SandboxApi {
384459
...opts,
385460
})
386461
}
462+
463+
private getSignature(filePath: string, fileOperation: 'read' | 'write', user: string, expirationInSeconds?: number): { signature: string; expiration: number | null } {
464+
if (!this.envdAccessToken) {
465+
throw new Error('Access token is not set and signature cannot be generated!')
466+
}
467+
468+
// expiration is unix timestamp
469+
const signatureExpiration = expirationInSeconds ? Math.floor(Date.now() / 1000) + expirationInSeconds : null
470+
let signatureRaw: string
471+
472+
if (signatureExpiration === undefined || signatureExpiration === null) {
473+
signatureRaw = `${filePath}:${fileOperation}:${user}:${this.envdAccessToken}`
474+
} else {
475+
signatureRaw = `${filePath}:${fileOperation}:${user}:${this.envdAccessToken}:${signatureExpiration.toString()}`
476+
}
477+
478+
const buff = Buffer.from(signatureRaw, 'utf8')
479+
const hash = crypto.createHash('sha256').update(buff).digest()
480+
const signature = 'v1_' + hash.toString('base64').replace(/=+$/, '')
481+
482+
return {
483+
signature: signature,
484+
expiration: signatureExpiration
485+
}
486+
}
387487
}

packages/js-sdk/src/sandbox/sandboxApi.ts

+17
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ export interface SandboxInfo {
4040
*/
4141
name?: string
4242

43+
/**
44+
* Envd access token.
45+
*/
46+
envdAccessToken?: string
47+
48+
/**
49+
* Envd version.
50+
*/
51+
envdVersion?: string
52+
4353
/**
4454
* Saved sandbox metadata.
4555
*/
@@ -187,6 +197,8 @@ export class SandboxApi {
187197
templateId: res.data.templateID,
188198
...(res.data.alias && { name: res.data.alias }),
189199
metadata: res.data.metadata ?? {},
200+
envdVersion: res.data.envdVersion,
201+
envdAccessToken: res.data.envdAccessToken,
190202
startedAt: new Date(res.data.startedAt),
191203
endAt: new Date(res.data.endAt),
192204
}
@@ -236,10 +248,12 @@ export class SandboxApi {
236248
opts?: SandboxApiOpts & {
237249
metadata?: Record<string, string>
238250
envs?: Record<string, string>
251+
secure?: boolean
239252
}
240253
): Promise<{
241254
sandboxId: string
242255
envdVersion: string
256+
envdAccessToken?: string
243257
}> {
244258
const config = new ConnectionConfig(opts)
245259
const client = new ApiClient(config)
@@ -251,6 +265,7 @@ export class SandboxApi {
251265
metadata: opts?.metadata,
252266
envVars: opts?.envs,
253267
timeout: this.timeoutToSeconds(timeoutMs),
268+
secure: opts?.secure,
254269
},
255270
signal: config.getSignal(opts?.requestTimeoutMs),
256271
})
@@ -273,12 +288,14 @@ export class SandboxApi {
273288
'You can do this by running `e2b template build` in the directory with the template.'
274289
)
275290
}
291+
276292
return {
277293
sandboxId: this.getSandboxId({
278294
sandboxId: res.data!.sandboxID,
279295
clientId: res.data!.clientID,
280296
}),
281297
envdVersion: res.data!.envdVersion,
298+
envdAccessToken: res.data!.envdAccessToken
282299
}
283300
}
284301

0 commit comments

Comments
 (0)