@earendil-works/gondolin 0.0.1 → 0.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.
@@ -1,96 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.FrameReader = exports.MAX_FRAME = void 0;
7
- exports.normalize = normalize;
8
- exports.decodeMessage = decodeMessage;
9
- exports.buildExecRequest = buildExecRequest;
10
- exports.buildStdinData = buildStdinData;
11
- exports.encodeFrame = encodeFrame;
12
- const cbor_1 = __importDefault(require("cbor"));
13
- exports.MAX_FRAME = 4 * 1024 * 1024;
14
- class FrameReader {
15
- constructor() {
16
- this.buffer = Buffer.alloc(0);
17
- this.expectedLength = null;
18
- }
19
- push(chunk, onFrame) {
20
- this.buffer = Buffer.concat([this.buffer, chunk]);
21
- while (true) {
22
- if (this.expectedLength === null) {
23
- if (this.buffer.length < 4)
24
- return;
25
- this.expectedLength = this.buffer.readUInt32BE(0);
26
- this.buffer = this.buffer.slice(4);
27
- if (this.expectedLength > exports.MAX_FRAME) {
28
- throw new Error(`Frame too large: ${this.expectedLength}`);
29
- }
30
- }
31
- if (this.buffer.length < this.expectedLength)
32
- return;
33
- const frame = this.buffer.slice(0, this.expectedLength);
34
- this.buffer = this.buffer.slice(this.expectedLength);
35
- this.expectedLength = null;
36
- onFrame(frame);
37
- }
38
- }
39
- }
40
- exports.FrameReader = FrameReader;
41
- function normalize(value) {
42
- if (value instanceof Map) {
43
- const obj = {};
44
- for (const [key, entry] of value.entries()) {
45
- obj[String(key)] = normalize(entry);
46
- }
47
- return obj;
48
- }
49
- if (Array.isArray(value)) {
50
- return value.map((entry) => normalize(entry));
51
- }
52
- if (value instanceof Uint8Array && !Buffer.isBuffer(value)) {
53
- return Buffer.from(value);
54
- }
55
- return value;
56
- }
57
- function decodeMessage(frame) {
58
- const raw = cbor_1.default.decodeFirstSync(frame);
59
- return normalize(raw);
60
- }
61
- function buildExecRequest(id, payload) {
62
- const cleaned = { cmd: payload.cmd };
63
- if (payload.argv !== undefined)
64
- cleaned.argv = payload.argv;
65
- if (payload.env !== undefined)
66
- cleaned.env = payload.env;
67
- if (payload.cwd !== undefined)
68
- cleaned.cwd = payload.cwd;
69
- if (payload.stdin !== undefined)
70
- cleaned.stdin = payload.stdin;
71
- if (payload.pty !== undefined)
72
- cleaned.pty = payload.pty;
73
- return {
74
- v: 1,
75
- t: "exec_request",
76
- id,
77
- p: cleaned,
78
- };
79
- }
80
- function buildStdinData(id, data, eof) {
81
- return {
82
- v: 1,
83
- t: "stdin_data",
84
- id,
85
- p: {
86
- data,
87
- ...(eof ? { eof } : {}),
88
- },
89
- };
90
- }
91
- function encodeFrame(message) {
92
- const payload = cbor_1.default.encode(message);
93
- const header = Buffer.alloc(4);
94
- header.writeUInt32BE(payload.length, 0);
95
- return Buffer.concat([header, payload]);
96
- }
package/dist/vm.js DELETED
@@ -1,523 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VM = void 0;
4
- const ws_1 = require("ws");
5
- const stream_1 = require("stream");
6
- const ws_protocol_1 = require("./ws-protocol");
7
- const sandbox_ws_server_1 = require("./sandbox-ws-server");
8
- const MAX_REQUEST_ID = 0xffffffff;
9
- const DEFAULT_STDIN_CHUNK = 32 * 1024;
10
- class VM {
11
- constructor(options = {}) {
12
- this.ws = null;
13
- this.connectPromise = null;
14
- this.startPromise = null;
15
- this.stopPromise = null;
16
- this.statusPromise = null;
17
- this.statusResolve = null;
18
- this.statusReject = null;
19
- this.state = "unknown";
20
- this.stateWaiters = [];
21
- this.sessions = new Map();
22
- this.nextId = 1;
23
- if (options.url && options.server) {
24
- throw new Error("VM cannot specify both url and server options");
25
- }
26
- this.token =
27
- options.token ?? process.env.ELWING_TOKEN ?? process.env.SANDBOX_WS_TOKEN;
28
- this.autoStart = options.autoStart ?? true;
29
- this.policy = options.policy ?? null;
30
- if (options.url) {
31
- this.url = options.url;
32
- this.server = null;
33
- return;
34
- }
35
- const serverOptions = { ...options.server };
36
- if (serverOptions.host === undefined)
37
- serverOptions.host = "127.0.0.1";
38
- if (serverOptions.port === undefined)
39
- serverOptions.port = 0;
40
- if (this.policy && serverOptions.policy === undefined) {
41
- serverOptions.policy = this.policy;
42
- }
43
- this.server = new sandbox_ws_server_1.SandboxWsServer(serverOptions);
44
- this.url = null;
45
- }
46
- getState() {
47
- return this.state;
48
- }
49
- getUrl() {
50
- return this.url;
51
- }
52
- getPolicy() {
53
- return this.policy;
54
- }
55
- setPolicy(policy) {
56
- this.policy = policy;
57
- if (this.server) {
58
- this.server.setPolicy(policy);
59
- }
60
- if (this.ws && this.ws.readyState === ws_1.WebSocket.OPEN) {
61
- this.sendJson({ type: "policy", policy });
62
- }
63
- }
64
- async start() {
65
- if (this.startPromise)
66
- return this.startPromise;
67
- this.startPromise = this.startInternal().finally(() => {
68
- this.startPromise = null;
69
- });
70
- return this.startPromise;
71
- }
72
- async stop() {
73
- if (this.stopPromise)
74
- return this.stopPromise;
75
- this.stopPromise = this.stopInternal().finally(() => {
76
- this.stopPromise = null;
77
- });
78
- return this.stopPromise;
79
- }
80
- async exec(command, options = {}) {
81
- const stream = await this.execStream(command, { ...options, buffer: true });
82
- return stream.result;
83
- }
84
- async execStream(command, options = {}) {
85
- await this.start();
86
- const { cmd, argv } = normalizeCommand(command, options);
87
- const id = this.allocateId();
88
- const stdinSetting = options.stdin;
89
- const stdinEnabled = stdinSetting !== undefined && stdinSetting !== false;
90
- const session = this.createSession(id, {
91
- bufferOutput: options.buffer ?? false,
92
- stdinEnabled,
93
- stdout: options.stdout,
94
- stderr: options.stderr,
95
- signal: options.signal,
96
- });
97
- this.sessions.set(id, session);
98
- const message = {
99
- type: "exec",
100
- id,
101
- cmd,
102
- argv: argv.length ? argv : undefined,
103
- env: options.env && options.env.length ? options.env : undefined,
104
- cwd: options.cwd,
105
- stdin: stdinEnabled ? true : undefined,
106
- pty: options.pty ? true : undefined,
107
- };
108
- try {
109
- this.sendJson(message);
110
- }
111
- catch (err) {
112
- const error = err instanceof Error ? err : new Error(String(err));
113
- this.rejectSession(session, error);
114
- throw error;
115
- }
116
- if (stdinEnabled && stdinSetting !== true) {
117
- void this.pipeStdin(id, stdinSetting ?? "", session);
118
- }
119
- return {
120
- id,
121
- stdout: session.stdout,
122
- stderr: session.stderr,
123
- sendStdin: async (data) => {
124
- this.ensureStdinAllowed(id);
125
- this.sendStdinData(id, data);
126
- },
127
- endStdin: async () => {
128
- this.ensureStdinAllowed(id);
129
- this.sendStdinEof(id);
130
- },
131
- result: session.result,
132
- };
133
- }
134
- async startInternal() {
135
- if (this.server) {
136
- const address = await this.server.start();
137
- this.url = address.url;
138
- }
139
- await this.ensureConnection();
140
- await this.ensureRunning();
141
- }
142
- async stopInternal() {
143
- if (this.server) {
144
- await this.server.stop();
145
- this.url = null;
146
- }
147
- await this.disconnect();
148
- }
149
- allocateId() {
150
- for (let i = 0; i <= MAX_REQUEST_ID; i += 1) {
151
- const id = this.nextId;
152
- this.nextId = this.nextId + 1;
153
- if (this.nextId > MAX_REQUEST_ID)
154
- this.nextId = 1;
155
- if (!this.sessions.has(id))
156
- return id;
157
- }
158
- throw new Error("no available request ids");
159
- }
160
- createSession(id, options) {
161
- let resolve;
162
- let reject;
163
- const result = new Promise((res, rej) => {
164
- resolve = res;
165
- reject = rej;
166
- });
167
- const session = {
168
- id,
169
- stdout: new stream_1.PassThrough(),
170
- stderr: new stream_1.PassThrough(),
171
- bufferOutput: options.bufferOutput,
172
- stdoutChunks: [],
173
- stderrChunks: [],
174
- resolve,
175
- reject,
176
- result,
177
- stdinEnabled: options.stdinEnabled,
178
- stdoutCallback: options.stdout,
179
- stderrCallback: options.stderr,
180
- signal: options.signal,
181
- };
182
- if (options.signal) {
183
- const onAbort = () => {
184
- this.rejectSession(session, new Error("exec aborted"));
185
- };
186
- options.signal.addEventListener("abort", onAbort, { once: true });
187
- session.signalListener = onAbort;
188
- }
189
- return session;
190
- }
191
- ensureStdinAllowed(id) {
192
- const session = this.sessions.get(id);
193
- if (!session) {
194
- throw new Error(`stdin is not available for request ${id}`);
195
- }
196
- if (!session.stdinEnabled) {
197
- throw new Error(`stdin was not enabled for request ${id}`);
198
- }
199
- }
200
- async pipeStdin(id, input, session) {
201
- if (!session.stdinEnabled)
202
- return;
203
- try {
204
- if (typeof input === "string" || Buffer.isBuffer(input)) {
205
- this.sendStdinData(id, input);
206
- }
207
- else if (typeof input === "boolean") {
208
- // no-op
209
- }
210
- else {
211
- for await (const chunk of toAsyncIterable(input)) {
212
- if (!this.sessions.has(id))
213
- return;
214
- this.sendStdinData(id, chunk);
215
- }
216
- }
217
- if (this.sessions.has(id)) {
218
- this.sendStdinEof(id);
219
- }
220
- }
221
- catch (err) {
222
- const error = err instanceof Error ? err : new Error(String(err));
223
- this.rejectSession(session, error);
224
- }
225
- }
226
- sendStdinData(id, data) {
227
- const payload = typeof data === "string" ? Buffer.from(data) : Buffer.from(data);
228
- for (let offset = 0; offset < payload.length; offset += DEFAULT_STDIN_CHUNK) {
229
- const slice = payload.subarray(offset, offset + DEFAULT_STDIN_CHUNK);
230
- this.sendJson({
231
- type: "stdin",
232
- id,
233
- data: slice.toString("base64"),
234
- });
235
- }
236
- }
237
- sendStdinEof(id) {
238
- this.sendJson({
239
- type: "stdin",
240
- id,
241
- eof: true,
242
- });
243
- }
244
- async ensureConnection() {
245
- if (this.ws && this.ws.readyState === ws_1.WebSocket.OPEN)
246
- return;
247
- if (this.connectPromise)
248
- return this.connectPromise;
249
- if (!this.url) {
250
- throw new Error("WebSocket URL is not available");
251
- }
252
- this.resetConnectionState();
253
- this.connectPromise = new Promise((resolve, reject) => {
254
- const headers = {};
255
- if (this.token)
256
- headers.Authorization = `Bearer ${this.token}`;
257
- const ws = new ws_1.WebSocket(this.url, { headers });
258
- this.ws = ws;
259
- let opened = false;
260
- ws.on("open", () => {
261
- opened = true;
262
- this.flushPolicy();
263
- resolve();
264
- });
265
- ws.on("message", (data, isBinary) => {
266
- this.handleMessage(data, isBinary);
267
- });
268
- ws.on("close", () => {
269
- const error = new Error("WebSocket closed");
270
- if (!opened) {
271
- reject(error);
272
- }
273
- this.handleDisconnect(error);
274
- });
275
- ws.on("error", (err) => {
276
- if (!opened) {
277
- reject(err);
278
- return;
279
- }
280
- const error = err instanceof Error ? err : new Error(String(err));
281
- this.handleDisconnect(error);
282
- });
283
- }).finally(() => {
284
- this.connectPromise = null;
285
- });
286
- return this.connectPromise;
287
- }
288
- resetConnectionState() {
289
- this.state = "unknown";
290
- this.initStatusPromise();
291
- }
292
- initStatusPromise() {
293
- this.statusPromise = new Promise((resolve, reject) => {
294
- this.statusResolve = resolve;
295
- this.statusReject = reject;
296
- });
297
- }
298
- flushPolicy() {
299
- if (this.policy) {
300
- this.sendJson({ type: "policy", policy: this.policy });
301
- }
302
- }
303
- async ensureRunning() {
304
- const state = await this.waitForStatus();
305
- if (state === "running")
306
- return;
307
- if (state === "stopped") {
308
- if (!this.autoStart) {
309
- throw new Error("sandbox is stopped");
310
- }
311
- this.sendJson({ type: "lifecycle", action: "restart" });
312
- }
313
- await this.waitForState("running");
314
- }
315
- async waitForStatus() {
316
- if (this.state !== "unknown")
317
- return this.state;
318
- if (!this.statusPromise) {
319
- this.initStatusPromise();
320
- }
321
- return this.statusPromise;
322
- }
323
- waitForState(state) {
324
- if (this.state === state)
325
- return Promise.resolve();
326
- return new Promise((resolve, reject) => {
327
- this.stateWaiters.push({ state, resolve, reject });
328
- });
329
- }
330
- handleMessage(data, isBinary) {
331
- if (isBinary) {
332
- const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
333
- const frame = (0, ws_protocol_1.decodeOutputFrame)(buffer);
334
- const session = this.sessions.get(frame.id);
335
- if (!session)
336
- return;
337
- if (frame.stream === "stdout") {
338
- session.stdout.write(frame.data);
339
- if (session.bufferOutput)
340
- session.stdoutChunks.push(frame.data);
341
- session.stdoutCallback?.(frame.data);
342
- }
343
- else {
344
- session.stderr.write(frame.data);
345
- if (session.bufferOutput)
346
- session.stderrChunks.push(frame.data);
347
- session.stderrCallback?.(frame.data);
348
- }
349
- return;
350
- }
351
- let message;
352
- try {
353
- message = JSON.parse(data.toString());
354
- }
355
- catch {
356
- return;
357
- }
358
- if (message.type === "status") {
359
- this.updateState(message.state);
360
- return;
361
- }
362
- if (message.type === "exec_response") {
363
- this.handleExecResponse(message);
364
- return;
365
- }
366
- if (message.type === "error") {
367
- this.handleError(message);
368
- }
369
- }
370
- updateState(state) {
371
- this.state = state;
372
- if (this.statusResolve) {
373
- this.statusResolve(state);
374
- this.statusResolve = null;
375
- this.statusReject = null;
376
- this.statusPromise = null;
377
- }
378
- if (this.stateWaiters.length > 0) {
379
- const remaining = [];
380
- for (const waiter of this.stateWaiters) {
381
- if (waiter.state === state) {
382
- waiter.resolve();
383
- }
384
- else {
385
- remaining.push(waiter);
386
- }
387
- }
388
- this.stateWaiters = remaining;
389
- }
390
- }
391
- handleExecResponse(message) {
392
- const session = this.sessions.get(message.id);
393
- if (!session)
394
- return;
395
- const result = {
396
- id: message.id,
397
- exitCode: message.exit_code ?? 1,
398
- signal: message.signal,
399
- stdout: session.bufferOutput ? Buffer.concat(session.stdoutChunks) : Buffer.alloc(0),
400
- stderr: session.bufferOutput ? Buffer.concat(session.stderrChunks) : Buffer.alloc(0),
401
- };
402
- this.finishSession(session, result);
403
- }
404
- handleError(message) {
405
- const error = new Error(`error ${message.code}: ${message.message}`);
406
- if (message.id === undefined) {
407
- this.rejectAll(error);
408
- return;
409
- }
410
- const session = this.sessions.get(message.id);
411
- if (session) {
412
- this.rejectSession(session, error);
413
- }
414
- }
415
- finishSession(session, result) {
416
- this.sessions.delete(session.id);
417
- session.stdout.end();
418
- session.stderr.end();
419
- if (session.signal && session.signalListener) {
420
- session.signal.removeEventListener("abort", session.signalListener);
421
- }
422
- session.resolve(result);
423
- }
424
- rejectSession(session, error) {
425
- this.sessions.delete(session.id);
426
- session.stdout.end();
427
- session.stderr.end();
428
- if (session.signal && session.signalListener) {
429
- session.signal.removeEventListener("abort", session.signalListener);
430
- }
431
- session.reject(error);
432
- }
433
- rejectAll(error) {
434
- for (const session of this.sessions.values()) {
435
- this.rejectSession(session, error);
436
- }
437
- this.sessions.clear();
438
- }
439
- handleDisconnect(error) {
440
- this.ws = null;
441
- if (this.statusReject) {
442
- this.statusReject(error ?? new Error("WebSocket disconnected"));
443
- this.statusReject = null;
444
- this.statusResolve = null;
445
- this.statusPromise = null;
446
- }
447
- if (this.stateWaiters.length > 0) {
448
- for (const waiter of this.stateWaiters) {
449
- waiter.reject(error ?? new Error("WebSocket disconnected"));
450
- }
451
- this.stateWaiters = [];
452
- }
453
- this.rejectAll(error ?? new Error("WebSocket disconnected"));
454
- }
455
- async disconnect() {
456
- if (!this.ws)
457
- return;
458
- const ws = this.ws;
459
- this.ws = null;
460
- if (ws.readyState === ws_1.WebSocket.CLOSED)
461
- return;
462
- await new Promise((resolve) => {
463
- let finished = false;
464
- const finish = () => {
465
- if (finished)
466
- return;
467
- finished = true;
468
- clearTimeout(timeout);
469
- resolve();
470
- };
471
- const timeout = setTimeout(() => {
472
- ws.terminate();
473
- finish();
474
- }, 1000);
475
- ws.once("close", finish);
476
- ws.once("error", finish);
477
- if (ws.readyState === ws_1.WebSocket.CLOSING) {
478
- return;
479
- }
480
- ws.close();
481
- });
482
- }
483
- sendJson(message) {
484
- if (!this.ws || this.ws.readyState !== ws_1.WebSocket.OPEN) {
485
- throw new Error("WebSocket is not connected");
486
- }
487
- this.ws.send(JSON.stringify(message));
488
- }
489
- }
490
- exports.VM = VM;
491
- function normalizeCommand(command, options) {
492
- if (Array.isArray(command)) {
493
- if (command.length === 0) {
494
- throw new Error("command array must include the executable");
495
- }
496
- return { cmd: command[0], argv: command.slice(1) };
497
- }
498
- return { cmd: command, argv: options.argv ?? [] };
499
- }
500
- function isAsyncIterable(value) {
501
- return (typeof value === "object" &&
502
- value !== null &&
503
- Symbol.asyncIterator in value &&
504
- typeof value[Symbol.asyncIterator] === "function");
505
- }
506
- async function* toAsyncIterable(value) {
507
- if (typeof value === "string" || Buffer.isBuffer(value) || typeof value === "boolean") {
508
- return;
509
- }
510
- if (isAsyncIterable(value)) {
511
- for await (const chunk of value) {
512
- yield Buffer.from(chunk);
513
- }
514
- return;
515
- }
516
- if (value instanceof stream_1.Readable) {
517
- for await (const chunk of value) {
518
- yield Buffer.from(chunk);
519
- }
520
- return;
521
- }
522
- throw new Error("unsupported stdin type");
523
- }
@@ -1,28 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OUTPUT_HEADER_BYTES = void 0;
4
- exports.encodeOutputFrame = encodeOutputFrame;
5
- exports.decodeOutputFrame = decodeOutputFrame;
6
- exports.OUTPUT_HEADER_BYTES = 5;
7
- function encodeOutputFrame(id, stream, data) {
8
- if (!Number.isInteger(id) || id < 0 || id > 0xffffffff) {
9
- throw new RangeError("id must be a uint32");
10
- }
11
- const header = Buffer.alloc(exports.OUTPUT_HEADER_BYTES);
12
- header.writeUInt8(stream === "stdout" ? 1 : 2, 0);
13
- header.writeUInt32BE(id, 1);
14
- return Buffer.concat([header, data]);
15
- }
16
- function decodeOutputFrame(frame) {
17
- if (frame.length < exports.OUTPUT_HEADER_BYTES) {
18
- throw new Error("output frame too short");
19
- }
20
- const streamFlag = frame.readUInt8(0);
21
- const stream = streamFlag === 1 ? "stdout" : "stderr";
22
- const id = frame.readUInt32BE(1);
23
- return {
24
- id,
25
- stream,
26
- data: frame.slice(exports.OUTPUT_HEADER_BYTES),
27
- };
28
- }