@namzu/sandbox 1.1.0
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/CHANGELOG.md +474 -0
- package/LICENSE.md +110 -0
- package/README.md +148 -0
- package/dist/backends/aci-standby-pool/index.d.ts +104 -0
- package/dist/backends/aci-standby-pool/index.d.ts.map +1 -0
- package/dist/backends/aci-standby-pool/index.js +425 -0
- package/dist/backends/aci-standby-pool/index.js.map +1 -0
- package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.d.ts +40 -0
- package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.d.ts.map +1 -0
- package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.js +157 -0
- package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.js.map +1 -0
- package/dist/backends/docker/index.d.ts +118 -0
- package/dist/backends/docker/index.d.ts.map +1 -0
- package/dist/backends/docker/index.js +645 -0
- package/dist/backends/docker/index.js.map +1 -0
- package/dist/backends/firecracker/__tests__/backend.test.d.ts +13 -0
- package/dist/backends/firecracker/__tests__/backend.test.d.ts.map +1 -0
- package/dist/backends/firecracker/__tests__/backend.test.js +353 -0
- package/dist/backends/firecracker/__tests__/backend.test.js.map +1 -0
- package/dist/backends/firecracker/__tests__/control-plane-mtls.test.d.ts +19 -0
- package/dist/backends/firecracker/__tests__/control-plane-mtls.test.d.ts.map +1 -0
- package/dist/backends/firecracker/__tests__/control-plane-mtls.test.js +201 -0
- package/dist/backends/firecracker/__tests__/control-plane-mtls.test.js.map +1 -0
- package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.d.ts +39 -0
- package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.d.ts.map +1 -0
- package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.js +149 -0
- package/dist/backends/firecracker/__tests__/fixtures/mtls-pki.js.map +1 -0
- package/dist/backends/firecracker/__tests__/protocol.test.d.ts +6 -0
- package/dist/backends/firecracker/__tests__/protocol.test.d.ts.map +1 -0
- package/dist/backends/firecracker/__tests__/protocol.test.js +77 -0
- package/dist/backends/firecracker/__tests__/protocol.test.js.map +1 -0
- package/dist/backends/firecracker/__tests__/transport.test.d.ts +20 -0
- package/dist/backends/firecracker/__tests__/transport.test.d.ts.map +1 -0
- package/dist/backends/firecracker/__tests__/transport.test.js +449 -0
- package/dist/backends/firecracker/__tests__/transport.test.js.map +1 -0
- package/dist/backends/firecracker/index.d.ts +124 -0
- package/dist/backends/firecracker/index.d.ts.map +1 -0
- package/dist/backends/firecracker/index.js +334 -0
- package/dist/backends/firecracker/index.js.map +1 -0
- package/dist/backends/firecracker/protocol.d.ts +132 -0
- package/dist/backends/firecracker/protocol.d.ts.map +1 -0
- package/dist/backends/firecracker/protocol.js +112 -0
- package/dist/backends/firecracker/protocol.js.map +1 -0
- package/dist/backends/firecracker/transport.d.ts +251 -0
- package/dist/backends/firecracker/transport.d.ts.map +1 -0
- package/dist/backends/firecracker/transport.js +524 -0
- package/dist/backends/firecracker/transport.js.map +1 -0
- package/dist/index.d.ts +611 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +376 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +28 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +670 -0
- package/dist/index.test.js.map +1 -0
- package/package.json +54 -0
- package/src/backends/aci-standby-pool/index.ts +602 -0
- package/src/backends/docker/__tests__/leaf-permissions.smoke.test.ts +169 -0
- package/src/backends/docker/index.ts +826 -0
- package/src/backends/firecracker/__tests__/backend.test.ts +418 -0
- package/src/backends/firecracker/__tests__/control-plane-mtls.test.ts +253 -0
- package/src/backends/firecracker/__tests__/fixtures/mtls-pki.ts +166 -0
- package/src/backends/firecracker/__tests__/protocol.test.ts +90 -0
- package/src/backends/firecracker/__tests__/transport.test.ts +526 -0
- package/src/backends/firecracker/index.ts +528 -0
- package/src/backends/firecracker/protocol.ts +191 -0
- package/src/backends/firecracker/transport.ts +667 -0
- package/src/index.test.ts +731 -0
- package/src/index.ts +930 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Ring-0 mTLS test PKI + a loopback relay stand-in.
|
|
3
|
+
*
|
|
4
|
+
* A throwaway P-256 test PKI (CA + server leaf + client leaf + an unrelated
|
|
5
|
+
* rogue client) baked in as PEM constants — no openssl/forge at test time, so
|
|
6
|
+
* the package stays dependency-light. Minted offline:
|
|
7
|
+
* openssl ec/x509 self-signed CA + leaves
|
|
8
|
+
* (server SAN = DNS:sandbox.fc.internal, IP:127.0.0.1), far-future expiry.
|
|
9
|
+
* `ROGUE_*` chains to a DIFFERENT CA to prove `rejectUnauthorized` blocks it.
|
|
10
|
+
*
|
|
11
|
+
* Used by both transport.test.ts (the raw `kind:'mtls'` dialer arm) and
|
|
12
|
+
* backend.test.ts (the cert-INJECTION seam through `buildFirecrackerBackend`),
|
|
13
|
+
* so the PKI + relay harness lives in one place instead of per-test copies.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { connect as netConnect } from 'node:net'
|
|
17
|
+
import { type TLSSocket, type Server as TlsServer, createServer as createTlsServer } from 'node:tls'
|
|
18
|
+
|
|
19
|
+
export const CA_CRT = `-----BEGIN CERTIFICATE-----
|
|
20
|
+
MIIBjjCCATWgAwIBAgIUGXUtG14KQr4spNGW7rpfjZGoBIIwCgYIKoZIzj0EAwIw
|
|
21
|
+
HDEaMBgGA1UEAwwRdmFuZGFsLWZjLXRlc3QtY2EwIBcNMjYwNjE2MDgzMjUyWhgP
|
|
22
|
+
MjEyNjA1MjMwODMyNTJaMBwxGjAYBgNVBAMMEXZhbmRhbC1mYy10ZXN0LWNhMFkw
|
|
23
|
+
EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5l+vjhjm/PX02D55n56EwelgvM1jrqQM
|
|
24
|
+
Z+ezzvMmrZEBGWInvBND3z3kc6nv7snu6fHT2HHOyhwUNoLqmrIg/qNTMFEwHQYD
|
|
25
|
+
VR0OBBYEFP0M8/KZY1ovxyGDci5iuoYXcmZgMB8GA1UdIwQYMBaAFP0M8/KZY1ov
|
|
26
|
+
xyGDci5iuoYXcmZgMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIg
|
|
27
|
+
C0xtFDASnwsOYT0GiZwzYWTOBfgmjDWK7ANXQQFNTh4CICVLQwq5eLy3424drEHk
|
|
28
|
+
5Ol/FNKVZcGRjE96rB0mRUOp
|
|
29
|
+
-----END CERTIFICATE-----
|
|
30
|
+
`
|
|
31
|
+
|
|
32
|
+
export const SERVER_CRT = `-----BEGIN CERTIFICATE-----
|
|
33
|
+
MIIBsDCCAVWgAwIBAgIUYkXroqlz9blXW8llis6e2OxOc1MwCgYIKoZIzj0EAwIw
|
|
34
|
+
HDEaMBgGA1UEAwwRdmFuZGFsLWZjLXRlc3QtY2EwIBcNMjYwNjE2MDgzMjUyWhgP
|
|
35
|
+
MjEyNjA1MjMwODMyNTJaMBIxEDAOBgNVBAMMB2ZjLWhvc3QwWTATBgcqhkjOPQIB
|
|
36
|
+
BggqhkjOPQMBBwNCAAQodCSHY8Sz5Lh/4K6+SzekkrRCeKMBfmonVb7dUhM0ZWpo
|
|
37
|
+
ePCcRrMcJp5aNxLxEgkl2EMMMMwE3LlLSOX5LZz6o30wezAkBgNVHREEHTAbghNz
|
|
38
|
+
YW5kYm94LmZjLmludGVybmFshwR/AAABMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0G
|
|
39
|
+
A1UdDgQWBBRXHU1vlX7WvqzcbaNxRxV4f3gfDDAfBgNVHSMEGDAWgBT9DPPymWNa
|
|
40
|
+
L8chg3IuYrqGF3JmYDAKBggqhkjOPQQDAgNJADBGAiEAlqzKqgBf5hxJqd26QwcY
|
|
41
|
+
n3cvEKi4f2BSLe1Rfzr5oSoCIQD0izUOJXLNvmac9cMgV0HvkftBerDKFOcGeGTN
|
|
42
|
+
g3Eunw==
|
|
43
|
+
-----END CERTIFICATE-----
|
|
44
|
+
`
|
|
45
|
+
|
|
46
|
+
export const SERVER_KEY = `-----BEGIN PRIVATE KEY-----
|
|
47
|
+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg/ROnYXIkMKnipbLt
|
|
48
|
+
JnFEwXAifgeOv9TGLcK2RojE7jmhRANCAAQodCSHY8Sz5Lh/4K6+SzekkrRCeKMB
|
|
49
|
+
fmonVb7dUhM0ZWpoePCcRrMcJp5aNxLxEgkl2EMMMMwE3LlLSOX5LZz6
|
|
50
|
+
-----END PRIVATE KEY-----
|
|
51
|
+
`
|
|
52
|
+
|
|
53
|
+
export const CLIENT_CRT = `-----BEGIN CERTIFICATE-----
|
|
54
|
+
MIIBjjCCATWgAwIBAgIUYkXroqlz9blXW8llis6e2OxOc1QwCgYIKoZIzj0EAwIw
|
|
55
|
+
HDEaMBgGA1UEAwwRdmFuZGFsLWZjLXRlc3QtY2EwIBcNMjYwNjE2MDgzMjUyWhgP
|
|
56
|
+
MjEyNjA1MjMwODMyNTJaMBgxFjAUBgNVBAMMDWNhLXZhbmRhbC1hcHAwWTATBgcq
|
|
57
|
+
hkjOPQIBBggqhkjOPQMBBwNCAASlCMCwjtrQUicWcWsO29R5S7fzjMbbxXvDh8/K
|
|
58
|
+
w57x/PN/uQwLHWCz1Tsyk0FnbrwGP+nPtwrUPIxrLN//euXko1cwVTATBgNVHSUE
|
|
59
|
+
DDAKBggrBgEFBQcDAjAdBgNVHQ4EFgQU2cxDCNrlVkk4ODs6ZPrkC6GzHjkwHwYD
|
|
60
|
+
VR0jBBgwFoAU/Qzz8pljWi/HIYNyLmK6hhdyZmAwCgYIKoZIzj0EAwIDRwAwRAIg
|
|
61
|
+
RghqArNm3bXnWnRi+jkEEJa8mRb2z0Wj4G7SIhtMurUCIBQSllVtICgZB9hvcM0D
|
|
62
|
+
d24fcW4nlfvaZJMJ6dRC1Wle
|
|
63
|
+
-----END CERTIFICATE-----
|
|
64
|
+
`
|
|
65
|
+
|
|
66
|
+
export const CLIENT_KEY = `-----BEGIN PRIVATE KEY-----
|
|
67
|
+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgaamj/ycrEuPqpiC2
|
|
68
|
+
IhAYx/M5iN4t4P+B17V4GO54Nb2hRANCAASlCMCwjtrQUicWcWsO29R5S7fzjMbb
|
|
69
|
+
xXvDh8/Kw57x/PN/uQwLHWCz1Tsyk0FnbrwGP+nPtwrUPIxrLN//euXk
|
|
70
|
+
-----END PRIVATE KEY-----
|
|
71
|
+
`
|
|
72
|
+
|
|
73
|
+
export const ROGUE_CLIENT_CRT = `-----BEGIN CERTIFICATE-----
|
|
74
|
+
MIIBhjCCASugAwIBAgIUbAYOlW4PQGyXjjRsjCPy62EHUmIwCgYIKoZIzj0EAwIw
|
|
75
|
+
EzERMA8GA1UEAwwIcm9ndWUtY2EwIBcNMjYwNjE2MDgzMjUyWhgPMjEyNjA1MjMw
|
|
76
|
+
ODMyNTJaMBcxFTATBgNVBAMMDHJvZ3VlLWNsaWVudDBZMBMGByqGSM49AgEGCCqG
|
|
77
|
+
SM49AwEHA0IABOPSzdzQTQz3M5CDHLVPguvHd10ncaDV9t4zKUy/PCE+U7GTJwN2
|
|
78
|
+
TtypGnmYEahbcl45j94hwu495P5VbR5ohb6jVzBVMBMGA1UdJQQMMAoGCCsGAQUF
|
|
79
|
+
BwMCMB0GA1UdDgQWBBTf0woauniche3/ps77+Kd0dsbbSDAfBgNVHSMEGDAWgBSX
|
|
80
|
+
Yu0IJlPxi79Gyi5hR1IfS/sqyzAKBggqhkjOPQQDAgNJADBGAiEAxD4N4XWtaHPJ
|
|
81
|
+
yaCOzQP0e5RRyNst3QrhH0NSMyPw89wCIQC+bezLX97TuV1sId53Bo1l9j1TSOMa
|
|
82
|
+
EZotN/Wsq6L+qw==
|
|
83
|
+
-----END CERTIFICATE-----
|
|
84
|
+
`
|
|
85
|
+
|
|
86
|
+
export const ROGUE_CLIENT_KEY = `-----BEGIN PRIVATE KEY-----
|
|
87
|
+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgR6fGq27O9eT/hi0Q
|
|
88
|
+
c+L5+lSm5fbDEghVJCo6k7HrG62hRANCAATj0s3c0E0M9zOQgxy1T4Lrx3ddJ3Gg
|
|
89
|
+
1fbeMylMvzwhPlOxkycDdk7cqRp5mBGoW3JeOY/eIcLuPeT+VW0eaIW+
|
|
90
|
+
-----END PRIVATE KEY-----
|
|
91
|
+
`
|
|
92
|
+
|
|
93
|
+
export interface RelayHandle {
|
|
94
|
+
readonly server: TlsServer
|
|
95
|
+
readonly port: number
|
|
96
|
+
/** sandboxId from the first connection's `SANDBOX <id>` preamble. */
|
|
97
|
+
preamble(): Promise<string>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A loopback stand-in for the per-FC-host mTLS relay.
|
|
102
|
+
*
|
|
103
|
+
* Terminates mTLS (requestCert + rejectUnauthorized + CA pin), reads the
|
|
104
|
+
* `SANDBOX <id>\n` routing preamble line, then bridges the rest of the TLS
|
|
105
|
+
* stream verbatim to a FRESH `net.connect` to `agentSockPath` (the
|
|
106
|
+
* unix-socket-backed real agent) — exactly the dumb byte-pump the production
|
|
107
|
+
* relay is. It does NOT speak framing itself and does NOT send an ack line.
|
|
108
|
+
*/
|
|
109
|
+
export function startMtlsRelay(agentSockPath: string, listenPort = 0): Promise<RelayHandle> {
|
|
110
|
+
let resolvePreamble: (id: string) => void
|
|
111
|
+
const preamble = new Promise<string>((r) => {
|
|
112
|
+
resolvePreamble = r
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
return new Promise<RelayHandle>((resolve, reject) => {
|
|
116
|
+
const server = createTlsServer(
|
|
117
|
+
{
|
|
118
|
+
requestCert: true,
|
|
119
|
+
rejectUnauthorized: true,
|
|
120
|
+
ca: CA_CRT,
|
|
121
|
+
cert: SERVER_CRT,
|
|
122
|
+
key: SERVER_KEY,
|
|
123
|
+
minVersion: 'TLSv1.3',
|
|
124
|
+
},
|
|
125
|
+
(tlsSock: TLSSocket) => {
|
|
126
|
+
let pre = Buffer.alloc(0)
|
|
127
|
+
const onPreambleData = (chunk: Buffer) => {
|
|
128
|
+
pre = Buffer.concat([pre, chunk])
|
|
129
|
+
const nl = pre.indexOf(0x0a)
|
|
130
|
+
if (nl < 0) return
|
|
131
|
+
tlsSock.removeListener('data', onPreambleData)
|
|
132
|
+
tlsSock.pause()
|
|
133
|
+
const line = pre.subarray(0, nl).toString('utf8')
|
|
134
|
+
const rest = pre.subarray(nl + 1)
|
|
135
|
+
if (rest.length > 0) tlsSock.unshift(rest)
|
|
136
|
+
const m = /^SANDBOX (.+)$/.exec(line)
|
|
137
|
+
const id = m?.[1]
|
|
138
|
+
if (id === undefined) {
|
|
139
|
+
tlsSock.destroy()
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
resolvePreamble(id)
|
|
143
|
+
// 1 inbound mTLS conn -> 1 FRESH local agent connect (the
|
|
144
|
+
// resume-survival invariant the relay must preserve).
|
|
145
|
+
const upstream = netConnect({ path: agentSockPath })
|
|
146
|
+
upstream.on('error', () => tlsSock.destroy())
|
|
147
|
+
tlsSock.on('error', () => upstream.destroy())
|
|
148
|
+
upstream.once('connect', () => {
|
|
149
|
+
tlsSock.pipe(upstream)
|
|
150
|
+
upstream.pipe(tlsSock)
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
tlsSock.on('data', onPreambleData)
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
server.on('error', reject)
|
|
157
|
+
server.listen(listenPort, '127.0.0.1', () => {
|
|
158
|
+
const addr = server.address()
|
|
159
|
+
if (addr === null || typeof addr === 'string') {
|
|
160
|
+
reject(new Error('relay: no TCP port'))
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
resolve({ server, port: addr.port, preamble: () => preamble })
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit-pins the shared NDJSON codec the vsock + HTTP transports both
|
|
3
|
+
* speak, and the `pickBackend` routing for `microvm:self-hosted`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
|
|
8
|
+
import { SandboxBackendNotImplementedError, createSandboxProvider } from '../../../index.js'
|
|
9
|
+
import { ExecResultAccumulator, parseExecLine } from '../protocol.js'
|
|
10
|
+
|
|
11
|
+
describe('ExecResultAccumulator', () => {
|
|
12
|
+
it('accumulates stdout/stderr deltas and captures the terminal result', () => {
|
|
13
|
+
const acc = new ExecResultAccumulator(0)
|
|
14
|
+
expect(acc.push({ type: 'stdout_delta', data: 'a' })).toBe(false)
|
|
15
|
+
expect(acc.push({ type: 'stdout_delta', data: 'b' })).toBe(false)
|
|
16
|
+
expect(acc.push({ type: 'stderr_delta', data: 'E' })).toBe(false)
|
|
17
|
+
expect(acc.done).toBe(false)
|
|
18
|
+
expect(acc.push({ type: 'result', exitCode: 0, timedOut: false })).toBe(true)
|
|
19
|
+
expect(acc.done).toBe(true)
|
|
20
|
+
const r = acc.finish()
|
|
21
|
+
expect(r.stdout).toBe('ab')
|
|
22
|
+
expect(r.stderr).toBe('E')
|
|
23
|
+
expect(r.exitCode).toBe(0)
|
|
24
|
+
expect(r.timedOut).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('throws on an error event (docker loop parity)', () => {
|
|
28
|
+
const acc = new ExecResultAccumulator(0)
|
|
29
|
+
expect(() => acc.push({ type: 'error', error: 'boom' })).toThrow('boom')
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('parseExecLine', () => {
|
|
34
|
+
it('parses a valid NDJSON line', () => {
|
|
35
|
+
expect(parseExecLine('{"type":"stdout_delta","data":"x"}')).toEqual({
|
|
36
|
+
type: 'stdout_delta',
|
|
37
|
+
data: 'x',
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
it('returns undefined for blank or malformed lines (SyntaxError swallow)', () => {
|
|
41
|
+
expect(parseExecLine('')).toBeUndefined()
|
|
42
|
+
expect(parseExecLine(' ')).toBeUndefined()
|
|
43
|
+
expect(parseExecLine('{not json')).toBeUndefined()
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('pickBackend — microvm:self-hosted', () => {
|
|
48
|
+
it('builds the firecracker backend when orchestratorEndpoint + getToken are present', () => {
|
|
49
|
+
const provider = createSandboxProvider({
|
|
50
|
+
backend: {
|
|
51
|
+
tier: 'microvm',
|
|
52
|
+
service: 'self-hosted',
|
|
53
|
+
firecrackerBinary: '/usr/bin/firecracker',
|
|
54
|
+
kernelImage: '/golden/vmlinux',
|
|
55
|
+
rootfsImage: '/golden/rootfs.ext4',
|
|
56
|
+
orchestratorEndpoint: 'https://orchestrator.test',
|
|
57
|
+
getToken: async () => 'tok',
|
|
58
|
+
template: 'golden-rev-1',
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
expect(provider.id).toContain('microvm')
|
|
62
|
+
expect(provider.id).toContain('firecracker')
|
|
63
|
+
expect(provider.name).toContain('microvm:self-hosted')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('still throws for the legacy local-containerd self-hosted shape (no orchestrator)', () => {
|
|
67
|
+
expect(() =>
|
|
68
|
+
createSandboxProvider({
|
|
69
|
+
backend: {
|
|
70
|
+
tier: 'microvm',
|
|
71
|
+
service: 'self-hosted',
|
|
72
|
+
firecrackerBinary: '/usr/bin/firecracker',
|
|
73
|
+
kernelImage: '/k',
|
|
74
|
+
rootfsImage: '/r',
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
).toThrow(SandboxBackendNotImplementedError)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('still throws for microvm:e2b and microvm:fly-machines', () => {
|
|
81
|
+
expect(() =>
|
|
82
|
+
createSandboxProvider({ backend: { tier: 'microvm', service: 'e2b', apiKey: 'k' } }),
|
|
83
|
+
).toThrow(SandboxBackendNotImplementedError)
|
|
84
|
+
expect(() =>
|
|
85
|
+
createSandboxProvider({
|
|
86
|
+
backend: { tier: 'microvm', service: 'fly-machines', apiToken: 't', app: 'a', image: 'i' },
|
|
87
|
+
}),
|
|
88
|
+
).toThrow(SandboxBackendNotImplementedError)
|
|
89
|
+
})
|
|
90
|
+
})
|