@@ -10,6 +10,7 @@ import { createRpcLogger } from '../logs'
10
10
import { Commands , Pty } from './commands'
11
11
import { Filesystem } from './filesystem'
12
12
import { SandboxApi } from './sandboxApi'
13
+ import crypto from 'crypto'
13
14
14
15
/**
15
16
* Options for creating a new Sandbox.
@@ -21,6 +22,7 @@ export interface SandboxOpts extends ConnectionOpts {
21
22
* @default {}
22
23
*/
23
24
metadata ?: Record < string , string >
25
+
24
26
/**
25
27
* Custom environment variables for the sandbox.
26
28
*
@@ -30,13 +32,21 @@ export interface SandboxOpts extends ConnectionOpts {
30
32
* @default {}
31
33
*/
32
34
envs ?: Record < string , string >
35
+
33
36
/**
34
37
* Timeout for the sandbox in **milliseconds**.
35
38
* 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.
36
39
*
37
40
* @default 300_000 // 5 minutes
38
41
*/
39
42
timeoutMs ?: number
43
+
44
+ /**
45
+ * Secure all traffic coming to the sandbox controller with auth token
46
+ *
47
+ * @default false
48
+ */
49
+ secure ?: boolean
40
50
}
41
51
42
52
/**
@@ -86,6 +96,7 @@ export class Sandbox extends SandboxApi {
86
96
87
97
protected readonly connectionConfig : ConnectionConfig
88
98
private readonly envdApiUrl : string
99
+ private readonly envdAccessToken ?: string
89
100
private readonly envdApi : EnvdApiClient
90
101
91
102
/**
@@ -100,15 +111,16 @@ export class Sandbox extends SandboxApi {
100
111
opts : Omit < SandboxOpts , 'timeoutMs' | 'envs' | 'metadata' > & {
101
112
sandboxId : string
102
113
envdVersion ?: string
114
+ envdAccessToken ?: string
103
115
}
104
116
) {
105
117
super ( )
106
118
107
119
this . sandboxId = opts . sandboxId
108
120
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 ) } `
112
124
113
125
const rpcTransport = createConnectTransport ( {
114
126
baseUrl : this . envdApiUrl ,
@@ -119,10 +131,18 @@ export class Sandbox extends SandboxApi {
119
131
// connect-web doesn't allow to configure redirect option - https://github.com/connectrpc/connect-es/pull/1082
120
132
// connect-web package uses redirect: "error" which is not supported in edge runtimes
121
133
// 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
+
122
140
options = {
123
141
...( options ?? { } ) ,
142
+ headers : headers ,
124
143
redirect : 'follow' ,
125
144
}
145
+
126
146
return fetch ( url , options )
127
147
} ,
128
148
} )
@@ -131,6 +151,8 @@ export class Sandbox extends SandboxApi {
131
151
{
132
152
apiUrl : this . envdApiUrl ,
133
153
logger : opts ?. logger ,
154
+ accessToken : this . envdAccessToken ,
155
+ headers : this . envdAccessToken ? { 'X-Access-Token' : this . envdAccessToken } : { } ,
134
156
} ,
135
157
{
136
158
version : opts ?. envdVersion ,
@@ -193,7 +215,6 @@ export class Sandbox extends SandboxApi {
193
215
: { template : this . defaultTemplate , sandboxOpts : templateOrOpts }
194
216
195
217
const config = new ConnectionConfig ( sandboxOpts )
196
-
197
218
if ( config . debug ) {
198
219
return new this ( {
199
220
sandboxId : 'debug_sandbox_id' ,
@@ -233,9 +254,12 @@ export class Sandbox extends SandboxApi {
233
254
opts ?: Omit < SandboxOpts , 'metadata' | 'envs' | 'timeoutMs' >
234
255
) : Promise < InstanceType < S > > {
235
256
const config = new ConnectionConfig ( opts )
257
+ //const info = await this.getInfo(sandboxId, opts)
236
258
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 >
239
263
}
240
264
241
265
/**
@@ -344,26 +368,77 @@ export class Sandbox extends SandboxApi {
344
368
*
345
369
* @param path the directory where to upload the file, defaults to user's home directory.
346
370
*
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
+ *
347
375
* @returns URL for uploading file.
348
376
*/
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
351
401
}
352
402
353
403
/**
354
404
* Get the URL to download a file from the sandbox.
355
405
*
356
406
* @param path path to the file to download.
357
407
*
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
+ *
358
412
* @returns URL for downloading file.
359
413
*/
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
362
437
}
363
438
364
- private fileUrl ( path ?: string ) {
439
+ private fileUrl ( path ?: string , username ?: string ) {
365
440
const url = new URL ( '/files' , this . envdApiUrl )
366
- url . searchParams . set ( 'username' , defaultUsername )
441
+ url . searchParams . set ( 'username' , username ?? defaultUsername )
367
442
if ( path ) {
368
443
url . searchParams . set ( 'path' , path )
369
444
}
@@ -384,4 +459,29 @@ export class Sandbox extends SandboxApi {
384
459
...opts ,
385
460
} )
386
461
}
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
+ }
387
487
}
0 commit comments