@adobe/aio-lib-sandbox 0.1.0-alpha.5 → 0.1.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +2 -0
- package/.eslintrc.json +21 -0
- package/README.md +21 -0
- package/package.json +14 -3
- package/src/Sandbox.js +35 -35
- package/src/utils.js +29 -2
- package/src/ws.js +27 -27
- package/test/Sandbox.test.js +49 -8
package/.eslintignore
ADDED
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": ["@adobe/eslint-config-aio-lib-config"],
|
|
3
|
+
"settings": {
|
|
4
|
+
"jsdoc": {
|
|
5
|
+
"ignorePrivate": true
|
|
6
|
+
}
|
|
7
|
+
},
|
|
8
|
+
"rules": {
|
|
9
|
+
"no-param-reassign": ["warn", { "props": true }],
|
|
10
|
+
"jsdoc/tag-lines": [
|
|
11
|
+
"error",
|
|
12
|
+
"never",
|
|
13
|
+
{
|
|
14
|
+
"startLines": null
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"parserOptions": {
|
|
19
|
+
"ecmaVersion": 2020
|
|
20
|
+
}
|
|
21
|
+
}
|
package/README.md
CHANGED
|
@@ -218,3 +218,24 @@ const sandbox = await Sandbox.create({
|
|
|
218
218
|
}
|
|
219
219
|
})
|
|
220
220
|
```
|
|
221
|
+
|
|
222
|
+
## Development
|
|
223
|
+
|
|
224
|
+
Install development dependencies:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
npm install
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
To run the same checks used by CI:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
npm test
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Linting is powered by ESLint:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
npm run lint
|
|
240
|
+
npm run lint-fix
|
|
241
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/aio-lib-sandbox",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.7",
|
|
4
4
|
"description": "JavaScript SDK for Adobe Runtime Sandboxes",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -19,11 +19,22 @@
|
|
|
19
19
|
"ws": "^8.19.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
+
"@adobe/eslint-config-aio-lib-config": "^4.0.0",
|
|
23
|
+
"eslint": "^8.57.1",
|
|
24
|
+
"eslint-config-standard": "^17.1.0",
|
|
25
|
+
"eslint-plugin-import": "^2.31.0",
|
|
26
|
+
"eslint-plugin-jest": "^27.9.0",
|
|
27
|
+
"eslint-plugin-jsdoc": "^48.11.0",
|
|
28
|
+
"eslint-plugin-n": "^15.7.0",
|
|
29
|
+
"eslint-plugin-node": "^11.1.0",
|
|
30
|
+
"eslint-plugin-promise": "^6.6.0",
|
|
22
31
|
"jest": "^29",
|
|
23
32
|
"jest-junit": "^16.0.0"
|
|
24
33
|
},
|
|
25
34
|
"scripts": {
|
|
26
|
-
"
|
|
27
|
-
"lint": "
|
|
35
|
+
"lint": "eslint src test",
|
|
36
|
+
"lint-fix": "eslint src test --fix",
|
|
37
|
+
"test": "npm run unit-tests && npm run lint",
|
|
38
|
+
"unit-tests": "jest --ci"
|
|
28
39
|
}
|
|
29
40
|
}
|
package/src/Sandbox.js
CHANGED
|
@@ -36,7 +36,7 @@ class Sandbox {
|
|
|
36
36
|
* @param {object} options sandbox options
|
|
37
37
|
* @private
|
|
38
38
|
*/
|
|
39
|
-
constructor(options) {
|
|
39
|
+
constructor (options) {
|
|
40
40
|
this.id = options.id
|
|
41
41
|
this.endpoint = options.endpoint
|
|
42
42
|
this.status = options.status
|
|
@@ -76,7 +76,7 @@ class Sandbox {
|
|
|
76
76
|
* @param {object} [options.policy] network policy (e.g. egress allowlist)
|
|
77
77
|
* @returns {Promise<Sandbox>} connected sandbox instance
|
|
78
78
|
*/
|
|
79
|
-
static async create(options = {}) {
|
|
79
|
+
static async create (options = {}) {
|
|
80
80
|
console.warn('[aio-lib-sandbox] alpha — APIs may change without notice')
|
|
81
81
|
const creds = resolveCredentials(options)
|
|
82
82
|
|
|
@@ -93,7 +93,7 @@ class Sandbox {
|
|
|
93
93
|
if (options.policy !== undefined) body.policy = options.policy
|
|
94
94
|
if (options.ports !== undefined) body.ports = options.ports
|
|
95
95
|
|
|
96
|
-
const url = `${creds.apiHost}/api/v1/namespaces/${creds.namespace}/
|
|
96
|
+
const url = `${creds.apiHost}/api/v1/namespaces/${creds.namespace}/sandboxes`
|
|
97
97
|
const payload = await apiRequest('POST', url, creds.apiKey, body)
|
|
98
98
|
|
|
99
99
|
const sandboxId = payload.sandboxId
|
|
@@ -131,10 +131,10 @@ class Sandbox {
|
|
|
131
131
|
* @param {string} [options.auth] Runtime API key
|
|
132
132
|
* @returns {Promise<Sandbox>} sandbox instance with `status` populated (not WebSocket-connected)
|
|
133
133
|
*/
|
|
134
|
-
static async get(sandboxId, options = {}) {
|
|
134
|
+
static async get (sandboxId, options = {}) {
|
|
135
135
|
console.warn('[aio-lib-sandbox] alpha — APIs may change without notice')
|
|
136
136
|
const creds = resolveCredentials(options)
|
|
137
|
-
const url = `${creds.apiHost}/api/v1/namespaces/${creds.namespace}/
|
|
137
|
+
const url = `${creds.apiHost}/api/v1/namespaces/${creds.namespace}/sandboxes/${sandboxId}`
|
|
138
138
|
const payload = await apiRequest('GET', url, creds.apiKey)
|
|
139
139
|
|
|
140
140
|
return new Sandbox({
|
|
@@ -157,7 +157,7 @@ class Sandbox {
|
|
|
157
157
|
*
|
|
158
158
|
* @type {object}
|
|
159
159
|
*/
|
|
160
|
-
static get sizes() {
|
|
160
|
+
static get sizes () {
|
|
161
161
|
return SANDBOX_SIZES
|
|
162
162
|
}
|
|
163
163
|
|
|
@@ -165,19 +165,19 @@ class Sandbox {
|
|
|
165
165
|
* Exposes `resolveCredentials` as a static helper (useful for testing).
|
|
166
166
|
*
|
|
167
167
|
* @param {object} overrides credential overrides
|
|
168
|
-
* @returns {{ apiHost: string, namespace: string, apiKey: string }}
|
|
168
|
+
* @returns {{ apiHost: string, namespace: string, apiKey: string }} resolved Runtime credentials
|
|
169
169
|
*/
|
|
170
|
-
static resolveCredentials(overrides = {}) {
|
|
170
|
+
static resolveCredentials (overrides = {}) {
|
|
171
171
|
return resolveCredentials(overrides)
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
175
|
* Exposes `normalizeSize` as a static helper (useful for testing).
|
|
176
176
|
*
|
|
177
|
-
* @param {string|object|undefined} size
|
|
178
|
-
* @returns {string}
|
|
177
|
+
* @param {string|object|undefined} size sandbox size name or resource spec
|
|
178
|
+
* @returns {string} normalized sandbox size name
|
|
179
179
|
*/
|
|
180
|
-
static normalizeSize(size) {
|
|
180
|
+
static normalizeSize (size) {
|
|
181
181
|
return normalizeSize(size)
|
|
182
182
|
}
|
|
183
183
|
|
|
@@ -190,7 +190,7 @@ class Sandbox {
|
|
|
190
190
|
*
|
|
191
191
|
* @returns {Promise<void>}
|
|
192
192
|
*/
|
|
193
|
-
connect() {
|
|
193
|
+
connect () {
|
|
194
194
|
if (!this.ws) {
|
|
195
195
|
this.ws = new SandboxSocket({
|
|
196
196
|
id: this.id,
|
|
@@ -215,10 +215,10 @@ class Sandbox {
|
|
|
215
215
|
* @param {number} [options.timeout] timeout in milliseconds (foreground only)
|
|
216
216
|
* @param {boolean} [options.detached] when true, run as a detached background process
|
|
217
217
|
* @param {string|Buffer} [options.stdin] data to send to stdin at startup
|
|
218
|
-
* @param {
|
|
219
|
-
* @returns {Promise}
|
|
218
|
+
* @param {Function} [options.onOutput] callback called with `(data, stream)` for each output chunk
|
|
219
|
+
* @returns {Promise} command result, or a detached command handle when `options.detached` is true
|
|
220
220
|
*/
|
|
221
|
-
exec(command, options = {}) {
|
|
221
|
+
exec (command, options = {}) {
|
|
222
222
|
try {
|
|
223
223
|
this.ensureOpen()
|
|
224
224
|
} catch (error) {
|
|
@@ -238,12 +238,12 @@ class Sandbox {
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
/**
|
|
241
|
-
* @param {string} execId
|
|
241
|
+
* @param {string} execId execution id to run inside the sandbox
|
|
242
242
|
* @param {string} command
|
|
243
243
|
* @param {object} options
|
|
244
244
|
* @private
|
|
245
245
|
*/
|
|
246
|
-
async sendExecFrameAndAwaitResponse(execId, command, options) {
|
|
246
|
+
async sendExecFrameAndAwaitResponse (execId, command, options) {
|
|
247
247
|
const detached = !!options.detached
|
|
248
248
|
const frame = { type: 'exec.run', execId, command, ...(detached && { detached: true }) }
|
|
249
249
|
|
|
@@ -283,10 +283,10 @@ class Sandbox {
|
|
|
283
283
|
*
|
|
284
284
|
* @param {string} execId the execId returned by the original `exec()` call
|
|
285
285
|
* @param {object} [options] re-attach options
|
|
286
|
-
* @param {
|
|
287
|
-
* @returns {Promise<{execId, command, pid, startedAt, detached, wait, kill, writeStdin, closeStdin}>}
|
|
286
|
+
* @param {Function} [options.onOutput] callback called with `(data, stream)` for live output
|
|
287
|
+
* @returns {Promise<{execId, command, pid, startedAt, detached, wait, kill, writeStdin, closeStdin}>} command handle
|
|
288
288
|
*/
|
|
289
|
-
getCommand(execId, options = {}) {
|
|
289
|
+
getCommand (execId, options = {}) {
|
|
290
290
|
try {
|
|
291
291
|
this.ensureOpen()
|
|
292
292
|
} catch (error) {
|
|
@@ -320,7 +320,7 @@ class Sandbox {
|
|
|
320
320
|
* @param {string} execId execution id
|
|
321
321
|
* @param {string} [signal] signal to deliver (default: `'SIGTERM'`)
|
|
322
322
|
*/
|
|
323
|
-
kill(execId, signal = 'SIGTERM') {
|
|
323
|
+
kill (execId, signal = 'SIGTERM') {
|
|
324
324
|
this.ensureOpen()
|
|
325
325
|
this.sendFrame({ type: 'exec.kill', execId, signal })
|
|
326
326
|
}
|
|
@@ -332,7 +332,7 @@ class Sandbox {
|
|
|
332
332
|
* @param {string} execId execution id from `exec()`
|
|
333
333
|
* @param {string|Buffer} data data to write
|
|
334
334
|
*/
|
|
335
|
-
writeStdin(execId, data) {
|
|
335
|
+
writeStdin (execId, data) {
|
|
336
336
|
this.ensureOpen()
|
|
337
337
|
const frame = { type: 'exec.input', execId }
|
|
338
338
|
if (Buffer.isBuffer(data)) {
|
|
@@ -350,7 +350,7 @@ class Sandbox {
|
|
|
350
350
|
*
|
|
351
351
|
* @param {string} execId execution id from `exec()`
|
|
352
352
|
*/
|
|
353
|
-
closeStdin(execId) {
|
|
353
|
+
closeStdin (execId) {
|
|
354
354
|
this.ensureOpen()
|
|
355
355
|
this.sendFrame({ type: 'exec.endInput', execId })
|
|
356
356
|
}
|
|
@@ -365,7 +365,7 @@ class Sandbox {
|
|
|
365
365
|
* @param {string} path path inside the sandbox
|
|
366
366
|
* @returns {Promise<string>} file contents as a UTF-8 string
|
|
367
367
|
*/
|
|
368
|
-
readFile(path) {
|
|
368
|
+
readFile (path) {
|
|
369
369
|
try {
|
|
370
370
|
this.ensureOpen()
|
|
371
371
|
} catch (error) {
|
|
@@ -395,7 +395,7 @@ class Sandbox {
|
|
|
395
395
|
* @param {string|Buffer} content file contents
|
|
396
396
|
* @returns {Promise<{path: string, size: number, ok: boolean}>} write confirmation
|
|
397
397
|
*/
|
|
398
|
-
writeFile(path, content) {
|
|
398
|
+
writeFile (path, content) {
|
|
399
399
|
try {
|
|
400
400
|
this.ensureOpen()
|
|
401
401
|
} catch (error) {
|
|
@@ -428,7 +428,7 @@ class Sandbox {
|
|
|
428
428
|
* @param {string} path directory path inside the sandbox
|
|
429
429
|
* @returns {Promise<Array<{name: string, type: string, size?: number}>>} directory entries
|
|
430
430
|
*/
|
|
431
|
-
listFiles(path) {
|
|
431
|
+
listFiles (path) {
|
|
432
432
|
try {
|
|
433
433
|
this.ensureOpen()
|
|
434
434
|
} catch (error) {
|
|
@@ -479,7 +479,7 @@ class Sandbox {
|
|
|
479
479
|
if (url === undefined) {
|
|
480
480
|
throw new SandboxPortNotProvisionedError(
|
|
481
481
|
`Port ${port} was not provisioned for sandbox '${this.id}'. ` +
|
|
482
|
-
|
|
482
|
+
'Declare it in create({ ports: [...] }) to get a preview URL.'
|
|
483
483
|
)
|
|
484
484
|
}
|
|
485
485
|
|
|
@@ -491,9 +491,9 @@ class Sandbox {
|
|
|
491
491
|
*
|
|
492
492
|
* @returns {Promise<object>} destroy response payload
|
|
493
493
|
*/
|
|
494
|
-
async destroy() {
|
|
494
|
+
async destroy () {
|
|
495
495
|
const base = this.managementEndpoint || this.apiHost
|
|
496
|
-
const url = `${base}/api/v1/namespaces/${this.namespace}/
|
|
496
|
+
const url = `${base}/api/v1/namespaces/${this.namespace}/sandboxes/${this.id}`
|
|
497
497
|
this.ws?.beginIntentionalClose()
|
|
498
498
|
|
|
499
499
|
let payload
|
|
@@ -516,33 +516,33 @@ class Sandbox {
|
|
|
516
516
|
/**
|
|
517
517
|
* Schedules a timeout that kills `execId` and rejects its pending entry.
|
|
518
518
|
*
|
|
519
|
-
* @param {string} execId
|
|
519
|
+
* @param {string} execId exec identifier to reject when the timeout fires
|
|
520
520
|
* @param {string} command human-readable command string (for the error message)
|
|
521
521
|
* @param {number} ms timeout in milliseconds
|
|
522
522
|
* @returns {ReturnType<setTimeout>} the timer handle (stored on the entry for cancellation)
|
|
523
523
|
*/
|
|
524
|
-
scheduleTimeout(execId, command, ms) {
|
|
524
|
+
scheduleTimeout (execId, command, ms) {
|
|
525
525
|
return setTimeout(() => {
|
|
526
526
|
try {
|
|
527
527
|
this.kill(execId)
|
|
528
528
|
} catch (_) {
|
|
529
529
|
// ignore errors
|
|
530
530
|
}
|
|
531
|
-
|
|
531
|
+
|
|
532
532
|
this.ws.rejectExec(execId, new SandboxTimeoutError(
|
|
533
533
|
`Command '${command}' exceeded timeout of ${ms}ms`
|
|
534
534
|
))
|
|
535
535
|
}, ms)
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
-
ensureOpen() {
|
|
538
|
+
ensureOpen () {
|
|
539
539
|
if (!this.ws) {
|
|
540
540
|
throw new SandboxWebSocketError(`Sandbox '${this.id}' is not connected`)
|
|
541
541
|
}
|
|
542
542
|
this.ws.ensureOpen()
|
|
543
543
|
}
|
|
544
544
|
|
|
545
|
-
sendFrame(frame) {
|
|
545
|
+
sendFrame (frame) {
|
|
546
546
|
this.ws.send(frame)
|
|
547
547
|
}
|
|
548
548
|
}
|
|
@@ -556,7 +556,7 @@ class Sandbox {
|
|
|
556
556
|
* every `getUrl()` call will throw `SandboxPortNotProvisionedError`).
|
|
557
557
|
*
|
|
558
558
|
* @param {object|null|undefined} raw the `previewUrls` field from the API response
|
|
559
|
-
* @returns {Map<number, string>}
|
|
559
|
+
* @returns {Map<number, string>} preview URLs keyed by port number
|
|
560
560
|
*/
|
|
561
561
|
function parsePreviewUrls (raw) {
|
|
562
562
|
if (!raw || typeof raw !== 'object') {
|
package/src/utils.js
CHANGED
|
@@ -18,10 +18,23 @@ const {
|
|
|
18
18
|
} = require('./errors')
|
|
19
19
|
const { SANDBOX_SIZES } = require('./constants')
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Builds a Basic authorization header from a Runtime API key.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} apiKey Runtime API key
|
|
25
|
+
* @returns {string} Basic authorization header value
|
|
26
|
+
*/
|
|
21
27
|
function buildAuthorizationHeader (apiKey) {
|
|
22
28
|
return `Basic ${Buffer.from(apiKey).toString('base64')}`
|
|
23
29
|
}
|
|
24
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Maps sandbox management API status codes to SDK error classes.
|
|
33
|
+
*
|
|
34
|
+
* @param {number} status HTTP response status
|
|
35
|
+
* @param {string} message error message
|
|
36
|
+
* @returns {SandboxClientError} SDK error matching the response status
|
|
37
|
+
*/
|
|
25
38
|
function createSandboxHttpError (status, message) {
|
|
26
39
|
if (status === 401 || status === 403) {
|
|
27
40
|
return new SandboxUnauthorizedError(message)
|
|
@@ -35,6 +48,12 @@ function createSandboxHttpError (status, message) {
|
|
|
35
48
|
return new SandboxClientError(message)
|
|
36
49
|
}
|
|
37
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Ensures the Runtime API host has a URL scheme.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} host Runtime API host
|
|
55
|
+
* @returns {string} API host with a URL scheme
|
|
56
|
+
*/
|
|
38
57
|
function normalizeApiHost (host) {
|
|
39
58
|
if (!host.match(/^https?:\/\//)) {
|
|
40
59
|
return `https://${host}`
|
|
@@ -42,10 +61,18 @@ function normalizeApiHost (host) {
|
|
|
42
61
|
return host
|
|
43
62
|
}
|
|
44
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Builds the sandbox execution WebSocket endpoint from Runtime API details.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} apiHost Runtime API host
|
|
68
|
+
* @param {string} namespace Runtime namespace
|
|
69
|
+
* @param {string} sandboxId sandbox id
|
|
70
|
+
* @returns {string} sandbox WebSocket endpoint
|
|
71
|
+
*/
|
|
45
72
|
function buildWebSocketEndpoint (apiHost, namespace, sandboxId) {
|
|
46
73
|
const url = new URL(apiHost)
|
|
47
74
|
url.protocol = url.protocol === 'http:' ? 'ws:' : 'wss:'
|
|
48
|
-
url.pathname = `/
|
|
75
|
+
url.pathname = `/api/v1/namespaces/${namespace}/sandboxes/${sandboxId}/exec`
|
|
49
76
|
url.search = ''
|
|
50
77
|
return url.toString()
|
|
51
78
|
}
|
|
@@ -55,7 +82,7 @@ function buildWebSocketEndpoint (apiHost, namespace, sandboxId) {
|
|
|
55
82
|
* explicit overrides. Throws `SandboxInitializationError` for missing values.
|
|
56
83
|
*
|
|
57
84
|
* @param {object} overrides explicit credential overrides
|
|
58
|
-
* @returns {{ apiHost: string, namespace: string, apiKey: string }}
|
|
85
|
+
* @returns {{ apiHost: string, namespace: string, apiKey: string }} resolved Runtime credentials
|
|
59
86
|
*/
|
|
60
87
|
function resolveCredentials (overrides = {}) {
|
|
61
88
|
const apiHost = overrides.apiHost || process.env.__OW_API_HOST
|
package/src/ws.js
CHANGED
|
@@ -25,7 +25,7 @@ const {
|
|
|
25
25
|
*/
|
|
26
26
|
class SandboxSocket {
|
|
27
27
|
/**
|
|
28
|
-
* @param {object} options
|
|
28
|
+
* @param {object} options socket options
|
|
29
29
|
* @param {string} options.id sandbox id
|
|
30
30
|
* @param {string} options.endpoint WebSocket endpoint URL
|
|
31
31
|
* @param {string} options.token authentication token
|
|
@@ -144,7 +144,7 @@ class SandboxSocket {
|
|
|
144
144
|
/**
|
|
145
145
|
* Serialises `frame` and sends it over the socket.
|
|
146
146
|
*
|
|
147
|
-
* @param {object} frame
|
|
147
|
+
* @param {object} frame WebSocket frame to send
|
|
148
148
|
*/
|
|
149
149
|
send (frame) {
|
|
150
150
|
this.socket.send(JSON.stringify(frame))
|
|
@@ -167,7 +167,7 @@ class SandboxSocket {
|
|
|
167
167
|
/**
|
|
168
168
|
* Closes the underlying socket.
|
|
169
169
|
*
|
|
170
|
-
* @param {object} [options]
|
|
170
|
+
* @param {object} [options] close options
|
|
171
171
|
* @param {boolean} [options.intentional] whether pending work should be drained without error
|
|
172
172
|
*/
|
|
173
173
|
close ({ intentional = false } = {}) {
|
|
@@ -180,22 +180,22 @@ class SandboxSocket {
|
|
|
180
180
|
// ------------------------------------------------------------------
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
|
-
* Registers a pending exec, sends the frame, and returns the promises
|
|
183
|
+
* Registers a pending exec, sends the frame, and returns the promises
|
|
184
184
|
* that will be settled by subsequent result or ack frames (for detached)
|
|
185
185
|
*
|
|
186
|
-
* @param {string} execId
|
|
186
|
+
* @param {string} execId execution id for the pending command
|
|
187
187
|
* @param {object} frame the frame to send (must include `type` and `execId`)
|
|
188
|
-
* @param {{ detached: boolean, onOutput?: Function }} options
|
|
189
|
-
* @returns {{ ackPromise: Promise, waitPromise: Promise|null }}
|
|
188
|
+
* @param {{ detached: boolean, onOutput?: Function }} options pending exec options
|
|
189
|
+
* @returns {{ ackPromise: Promise, waitPromise: Promise|null }} promises for ack and optional completion
|
|
190
190
|
*/
|
|
191
191
|
sendExec (execId, frame, { detached, onOutput }) {
|
|
192
192
|
let waitResolve, waitReject, waitPromise
|
|
193
193
|
if (detached) {
|
|
194
|
-
waitPromise = new Promise((
|
|
194
|
+
waitPromise = new Promise((resolve, reject) => { waitResolve = resolve; waitReject = reject })
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
let resolve, reject
|
|
198
|
-
const ackPromise = new Promise((
|
|
198
|
+
const ackPromise = new Promise((_resolve, _reject) => { resolve = _resolve; reject = _reject })
|
|
199
199
|
|
|
200
200
|
this.pendingExecs.set(execId, {
|
|
201
201
|
resolve,
|
|
@@ -225,8 +225,8 @@ class SandboxSocket {
|
|
|
225
225
|
/**
|
|
226
226
|
* Stores a timer handle on a pending exec entry so it can be cleared on completion.
|
|
227
227
|
*
|
|
228
|
-
* @param {string} execId
|
|
229
|
-
* @param {ReturnType<setTimeout>} handle
|
|
228
|
+
* @param {string} execId execution id for the pending command
|
|
229
|
+
* @param {ReturnType<setTimeout>} handle timeout handle to store
|
|
230
230
|
*/
|
|
231
231
|
setExecTimeout (execId, handle) {
|
|
232
232
|
const entry = this.pendingExecs.get(execId)
|
|
@@ -236,8 +236,8 @@ class SandboxSocket {
|
|
|
236
236
|
/**
|
|
237
237
|
* Rejects and removes a pending exec, clearing its timeout.
|
|
238
238
|
*
|
|
239
|
-
* @param {string} execId
|
|
240
|
-
* @param {Error} error
|
|
239
|
+
* @param {string} execId execution id for the pending command
|
|
240
|
+
* @param {Error} error error used to reject the command
|
|
241
241
|
*/
|
|
242
242
|
rejectExec (execId, error) {
|
|
243
243
|
const pending = this.pendingExecs.get(execId)
|
|
@@ -256,8 +256,8 @@ class SandboxSocket {
|
|
|
256
256
|
/**
|
|
257
257
|
* Rejects and removes a pending file operation.
|
|
258
258
|
*
|
|
259
|
-
* @param {string} execId
|
|
260
|
-
* @param {Error} error
|
|
259
|
+
* @param {string} execId file operation id
|
|
260
|
+
* @param {Error} error error used to reject the file operation
|
|
261
261
|
*/
|
|
262
262
|
rejectFileOp (execId, error) {
|
|
263
263
|
const pending = this.pendingFileOps.get(execId)
|
|
@@ -269,7 +269,7 @@ class SandboxSocket {
|
|
|
269
269
|
/**
|
|
270
270
|
* Resolves and removes a pending exec during an intentional sandbox shutdown.
|
|
271
271
|
*
|
|
272
|
-
* @param {string} execId
|
|
272
|
+
* @param {string} execId execution id for the pending command
|
|
273
273
|
*/
|
|
274
274
|
resolveExecOnIntentionalClose (execId) {
|
|
275
275
|
const pending = this.pendingExecs.get(execId)
|
|
@@ -469,7 +469,7 @@ class SandboxSocket {
|
|
|
469
469
|
/**
|
|
470
470
|
* Handles exec.info (response to exec.get) and error frames routed from handleMessage.
|
|
471
471
|
*
|
|
472
|
-
* @param {object} frame
|
|
472
|
+
* @param {object} frame exec.get response or error frame
|
|
473
473
|
*/
|
|
474
474
|
handleGetFrame (frame) {
|
|
475
475
|
const pending = this.pendingGetOps.get(frame.execId)
|
|
@@ -517,12 +517,12 @@ class SandboxSocket {
|
|
|
517
517
|
*
|
|
518
518
|
* @param {object} frame exec.info frame
|
|
519
519
|
* @param {object} pending entry from pendingGetOps
|
|
520
|
-
* @returns {Promise}
|
|
520
|
+
* @returns {Promise} wait promise for the running command
|
|
521
521
|
*/
|
|
522
522
|
resolveExecEntry (frame, pending) {
|
|
523
523
|
const existingExec = this.pendingExecs.get(frame.execId)
|
|
524
524
|
if (existingExec) {
|
|
525
|
-
this.mergeOnOutputCallback(existingExec, pending.onOutput)
|
|
525
|
+
existingExec.onOutput = this.mergeOnOutputCallback(existingExec.onOutput, pending.onOutput)
|
|
526
526
|
return existingExec._waitPromise
|
|
527
527
|
}
|
|
528
528
|
return this.registerReattachedExec(frame, pending.onOutput)
|
|
@@ -531,13 +531,13 @@ class SandboxSocket {
|
|
|
531
531
|
/**
|
|
532
532
|
* Appends `onOutput` to an existing exec entry's callback chain, preserving the previous handler.
|
|
533
533
|
*
|
|
534
|
-
* @param {
|
|
534
|
+
* @param {Function|null|undefined} prev existing callback
|
|
535
535
|
* @param {Function|undefined} onOutput new callback to add
|
|
536
|
+
* @returns {Function|null|undefined} merged callback
|
|
536
537
|
*/
|
|
537
|
-
mergeOnOutputCallback (
|
|
538
|
-
if (!onOutput) return
|
|
539
|
-
|
|
540
|
-
existingExec.onOutput = (data, stream) => {
|
|
538
|
+
mergeOnOutputCallback (prev, onOutput) {
|
|
539
|
+
if (!onOutput) return prev
|
|
540
|
+
return (data, stream) => {
|
|
541
541
|
if (prev) prev(data, stream)
|
|
542
542
|
onOutput(data, stream)
|
|
543
543
|
}
|
|
@@ -549,11 +549,11 @@ class SandboxSocket {
|
|
|
549
549
|
*
|
|
550
550
|
* @param {object} frame exec.info frame
|
|
551
551
|
* @param {Function|undefined} onOutput output callback
|
|
552
|
-
* @returns {Promise}
|
|
552
|
+
* @returns {Promise} wait promise for the reattached command
|
|
553
553
|
*/
|
|
554
554
|
registerReattachedExec (frame, onOutput) {
|
|
555
555
|
let waitResolve, waitReject
|
|
556
|
-
const waitPromise = new Promise((
|
|
556
|
+
const waitPromise = new Promise((resolve, reject) => { waitResolve = resolve; waitReject = reject })
|
|
557
557
|
this.pendingExecs.set(frame.execId, {
|
|
558
558
|
resolve: () => {},
|
|
559
559
|
reject: () => {},
|
|
@@ -576,7 +576,7 @@ class SandboxSocket {
|
|
|
576
576
|
* @param {object} frame exec.info frame
|
|
577
577
|
* @param {Promise} waitPromise resolves when the process exits
|
|
578
578
|
* @param {object} sandbox Sandbox instance for delegating control operations
|
|
579
|
-
* @returns {object}
|
|
579
|
+
* @returns {object} command handle with wait and control helpers
|
|
580
580
|
*/
|
|
581
581
|
buildCommandObject (frame, waitPromise, sandbox) {
|
|
582
582
|
const { execId } = frame
|
package/test/Sandbox.test.js
CHANGED
|
@@ -67,7 +67,7 @@ class FakeWebSocket extends EventEmitter {
|
|
|
67
67
|
|
|
68
68
|
const BASE_OPTIONS = {
|
|
69
69
|
id: 'sb-test',
|
|
70
|
-
endpoint: 'wss://runtime.example.net/
|
|
70
|
+
endpoint: 'wss://runtime.example.net/api/v1/namespaces/ns/sandboxes/sb-test/exec',
|
|
71
71
|
status: 'ready',
|
|
72
72
|
namespace: 'ns',
|
|
73
73
|
apiHost: 'https://runtime.example.net',
|
|
@@ -80,6 +80,9 @@ const BASE_OPTIONS = {
|
|
|
80
80
|
|
|
81
81
|
let sockets
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
*
|
|
85
|
+
*/
|
|
83
86
|
function setupWebSocket () {
|
|
84
87
|
sockets = []
|
|
85
88
|
WebSocket.OPEN = 1
|
|
@@ -90,6 +93,12 @@ function setupWebSocket () {
|
|
|
90
93
|
})
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Builds a connected sandbox backed by the fake WebSocket.
|
|
98
|
+
*
|
|
99
|
+
* @param {object} opts sandbox option overrides
|
|
100
|
+
* @returns {Promise<Sandbox>} connected sandbox instance
|
|
101
|
+
*/
|
|
93
102
|
async function buildConnectedSandbox (opts = {}) {
|
|
94
103
|
const sandbox = new Sandbox({ ...BASE_OPTIONS, ...opts })
|
|
95
104
|
const connectPromise = sandbox.connect()
|
|
@@ -200,7 +209,7 @@ describe('Sandbox', () => {
|
|
|
200
209
|
ok: true,
|
|
201
210
|
json: () => Promise.resolve({
|
|
202
211
|
sandboxId: 'sb-new',
|
|
203
|
-
wsEndpoint: 'wss://runtime.example.net/
|
|
212
|
+
wsEndpoint: 'wss://runtime.example.net/api/v1/namespaces/ns/sandboxes/sb-new/exec',
|
|
204
213
|
status: 'ready',
|
|
205
214
|
token: 'tok-new',
|
|
206
215
|
maxLifetime: 3600,
|
|
@@ -231,7 +240,7 @@ describe('Sandbox', () => {
|
|
|
231
240
|
[3000, 'https://sb-new-3000.preview.example.net']
|
|
232
241
|
]))
|
|
233
242
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
234
|
-
'https://runtime.example.net/api/v1/namespaces/ns/
|
|
243
|
+
'https://runtime.example.net/api/v1/namespaces/ns/sandboxes',
|
|
235
244
|
expect.objectContaining({ method: 'POST' })
|
|
236
245
|
)
|
|
237
246
|
})
|
|
@@ -241,7 +250,7 @@ describe('Sandbox', () => {
|
|
|
241
250
|
ok: true,
|
|
242
251
|
json: () => Promise.resolve({
|
|
243
252
|
sandboxId: 'sb-pol',
|
|
244
|
-
wsEndpoint: 'wss://runtime.example.net/
|
|
253
|
+
wsEndpoint: 'wss://runtime.example.net/api/v1/namespaces/ns/sandboxes/sb-pol/exec',
|
|
245
254
|
status: 'ready',
|
|
246
255
|
token: 'tok-pol',
|
|
247
256
|
maxLifetime: 3600
|
|
@@ -272,7 +281,7 @@ describe('Sandbox', () => {
|
|
|
272
281
|
ok: true,
|
|
273
282
|
json: () => Promise.resolve({
|
|
274
283
|
sandboxId: 'sb-ports',
|
|
275
|
-
wsEndpoint: 'wss://runtime.example.net/
|
|
284
|
+
wsEndpoint: 'wss://runtime.example.net/api/v1/namespaces/ns/sandboxes/sb-ports/exec',
|
|
276
285
|
status: 'ready',
|
|
277
286
|
token: 'tok-ports',
|
|
278
287
|
maxLifetime: 3600,
|
|
@@ -312,7 +321,7 @@ describe('Sandbox', () => {
|
|
|
312
321
|
ok: true,
|
|
313
322
|
json: () => Promise.resolve({
|
|
314
323
|
sandboxId: 'sb-env',
|
|
315
|
-
wsEndpoint: 'wss://runtime.example.net/
|
|
324
|
+
wsEndpoint: 'wss://runtime.example.net/api/v1/namespaces/ns/sandboxes/sb-env/exec',
|
|
316
325
|
status: 'ready',
|
|
317
326
|
token: 'tok-env',
|
|
318
327
|
maxLifetime: 3600
|
|
@@ -355,7 +364,7 @@ describe('Sandbox', () => {
|
|
|
355
364
|
await new Promise(resolve => setImmediate(resolve))
|
|
356
365
|
sockets[0].open()
|
|
357
366
|
sockets[0].message({ type: 'auth.ok', sandboxId: 'sb-noep' })
|
|
358
|
-
|
|
367
|
+
await createPromise
|
|
359
368
|
|
|
360
369
|
expect(sockets[0].url).toContain('wss://')
|
|
361
370
|
expect(sockets[0].url).toContain('sb-noep')
|
|
@@ -408,6 +417,38 @@ describe('Sandbox', () => {
|
|
|
408
417
|
Sandbox.get('sb-x', { apiHost: 'https://runtime.example.net', namespace: 'ns', auth: 'bad' })
|
|
409
418
|
).rejects.toThrow(SandboxUnauthorizedError)
|
|
410
419
|
})
|
|
420
|
+
|
|
421
|
+
test('throws SandboxTimeoutError on 504', async () => {
|
|
422
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
423
|
+
ok: false,
|
|
424
|
+
status: 504,
|
|
425
|
+
text: () => Promise.resolve('gateway timeout')
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
await expect(
|
|
429
|
+
Sandbox.get('sb-slow', { apiHost: 'https://runtime.example.net', namespace: 'ns', auth: 'key' })
|
|
430
|
+
).rejects.toThrow(SandboxTimeoutError)
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
test('throws SandboxClientError on unexpected API status', async () => {
|
|
434
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
435
|
+
ok: false,
|
|
436
|
+
status: 500,
|
|
437
|
+
text: () => Promise.resolve('server error')
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
await expect(
|
|
441
|
+
Sandbox.get('sb-error', { apiHost: 'https://runtime.example.net', namespace: 'ns', auth: 'key' })
|
|
442
|
+
).rejects.toThrow(SandboxClientError)
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
test('wraps fetch failures in SandboxClientError', async () => {
|
|
446
|
+
global.fetch = jest.fn().mockRejectedValue(new Error('network unavailable'))
|
|
447
|
+
|
|
448
|
+
await expect(
|
|
449
|
+
Sandbox.get('sb-network', { apiHost: 'https://runtime.example.net', namespace: 'ns', auth: 'key' })
|
|
450
|
+
).rejects.toThrow(SandboxClientError)
|
|
451
|
+
})
|
|
411
452
|
})
|
|
412
453
|
|
|
413
454
|
// -------------------------------------------------------------------------
|
|
@@ -844,7 +885,7 @@ describe('Sandbox', () => {
|
|
|
844
885
|
expect(result.status).toBe('destroyed')
|
|
845
886
|
expect(sandbox.status).toBe('destroyed')
|
|
846
887
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
847
|
-
expect.stringContaining('/
|
|
888
|
+
expect.stringContaining('/sandboxes/sb-test'),
|
|
848
889
|
expect.objectContaining({ method: 'DELETE' })
|
|
849
890
|
)
|
|
850
891
|
})
|