@aitty/server 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.
@@ -0,0 +1,465 @@
1
+ import { isSessionTokenAuthorized } from "../cli-token.js";
2
+ import { isHostAllowed, isOriginAllowed } from "../network-policy.js";
3
+ import { createStructuredLogger } from "../logging.js";
4
+ import os from "node:os";
5
+ import { createExitControlFrame, createHelloControlFrame, createPingControlFrame, createPongControlFrame, createThemeControlFrame, isAittyResizeControlFrame, parseAittyControlFrame, subscribeThemeSource } from "@aitty/protocol";
6
+ //#region src/runtime/websocket-transport.ts
7
+ const DEFAULT_MAX_BUFFERED_AMOUNT_BYTES = 256 * 1024;
8
+ const DEFAULT_MAX_QUEUED_BYTES = 256 * 1024;
9
+ const DEFAULT_DRAIN_WAIT_MS = 250;
10
+ const DEFAULT_PRIORITY_OUTPUT_RECOVERY_MS = 1e3;
11
+ const DEFAULT_KEEPALIVE_INITIAL_DELAY_MS = 100;
12
+ const DEFAULT_KEEPALIVE_INTERVAL_MS = 3e4;
13
+ const SOCKET_OPEN = 1;
14
+ const SOCKET_CLOSED = 3;
15
+ const NORMAL_CLOSE_CODE = 1e3;
16
+ const SUPERSEDED_CLOSE_CODE = 4001;
17
+ const SUPERSEDED_CLOSE_REASON = "superseded";
18
+ const BACKPRESSURE_CLOSE_CODE = 4002;
19
+ const BACKPRESSURE_CLOSE_REASON = "backpressure";
20
+ function createWebSocketTransport(options) {
21
+ const server = options.server;
22
+ const websocketServer = new options.WebSocketServer({ noServer: true });
23
+ const logger = createStructuredLogger({
24
+ token: options.token,
25
+ verbose: options.verbose,
26
+ writer: options.stderr
27
+ });
28
+ const clients = /* @__PURE__ */ new Map();
29
+ let inputWriteChain = Promise.resolve();
30
+ const maxBufferedAmountBytes = options.maxBufferedAmountBytes ?? DEFAULT_MAX_BUFFERED_AMOUNT_BYTES;
31
+ const maxQueuedBytes = options.maxQueuedBytes ?? DEFAULT_MAX_QUEUED_BYTES;
32
+ const drainWaitMs = options.drainWaitMs ?? DEFAULT_DRAIN_WAIT_MS;
33
+ const keepaliveInitialDelayMs = Math.max(0, options.keepaliveInitialDelayMs ?? DEFAULT_KEEPALIVE_INITIAL_DELAY_MS);
34
+ const keepaliveIntervalMs = Math.max(0, options.keepaliveIntervalMs ?? DEFAULT_KEEPALIVE_INTERVAL_MS);
35
+ let closed = false;
36
+ let exitNotified = false;
37
+ let latestThemeFrame = null;
38
+ const themeSubscription = subscribeThemeSource(options.themeSource, (theme) => {
39
+ latestThemeFrame = createThemeControlFrame(theme);
40
+ for (const [websocket, clientState] of clients) {
41
+ if (!clientState.active || websocket.readyState !== SOCKET_OPEN) continue;
42
+ enqueueClientFrame(websocket, clientState, latestThemeFrame, false, {
43
+ drainWaitMs,
44
+ maxBufferedAmountBytes,
45
+ maxQueuedBytes,
46
+ logger
47
+ });
48
+ }
49
+ });
50
+ const sessionSubscription = options.session.onData((chunk) => {
51
+ const normalizedChunk = Buffer.from(chunk);
52
+ const activeClient = getActiveClient(clients);
53
+ if (!activeClient) return;
54
+ const [websocket, clientState] = activeClient;
55
+ if (websocket.readyState !== SOCKET_OPEN) return;
56
+ enqueueClientFrame(websocket, clientState, normalizedChunk, true, {
57
+ drainWaitMs,
58
+ maxBufferedAmountBytes,
59
+ maxQueuedBytes,
60
+ logger
61
+ });
62
+ });
63
+ const onUpgrade = (request, socket, head) => {
64
+ const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
65
+ if (requestUrl.pathname !== "/ws") {
66
+ rejectUpgrade(socket, 404, "Not Found");
67
+ return;
68
+ }
69
+ if (options.networkPolicy && !isHostAllowed(request.headers.host, options.networkPolicy)) {
70
+ rejectUpgrade(socket, 403, "Forbidden");
71
+ return;
72
+ }
73
+ if (!isSessionTokenAuthorized(options.token, requestUrl.searchParams.get("t"))) {
74
+ rejectUpgrade(socket, 403, "Forbidden");
75
+ return;
76
+ }
77
+ if (options.networkPolicy && !isOriginAllowed(request.headers.origin, options.networkPolicy)) {
78
+ rejectUpgrade(socket, 403, "Forbidden");
79
+ return;
80
+ }
81
+ websocketServer.handleUpgrade(request, socket, head, (websocket) => {
82
+ websocketServer.emit("connection", websocket, request);
83
+ });
84
+ };
85
+ server.on("upgrade", onUpgrade);
86
+ websocketServer.on("connection", (websocket) => {
87
+ if (closed) {
88
+ websocket.close();
89
+ return;
90
+ }
91
+ logger.info("WebSocket client connected");
92
+ const clientState = {
93
+ active: true,
94
+ keepaliveTimer: null,
95
+ oversizedAllowanceBytes: 0,
96
+ pendingBytes: 0,
97
+ pendingFrames: [],
98
+ priorityModeUntil: 0,
99
+ sending: false
100
+ };
101
+ const previousClient = getActiveClient(clients);
102
+ if (previousClient) previousClient[1].active = false;
103
+ clients.set(websocket, clientState);
104
+ if (previousClient) supersedeClient(previousClient[0]);
105
+ const dispose = once(() => {
106
+ clientState.active = false;
107
+ clearKeepaliveTimer(clientState);
108
+ clientState.oversizedAllowanceBytes = 0;
109
+ clientState.pendingBytes = 0;
110
+ clientState.pendingFrames.length = 0;
111
+ clients.delete(websocket);
112
+ });
113
+ const replay = options.session.getBufferedOutput();
114
+ enqueueClientFrame(websocket, clientState, createHelloFrame(options.session, replay), false, {
115
+ allowOversizedFrame: true,
116
+ drainWaitMs,
117
+ maxBufferedAmountBytes,
118
+ maxQueuedBytes,
119
+ logger
120
+ });
121
+ if (latestThemeFrame) enqueueClientFrame(websocket, clientState, latestThemeFrame, false, {
122
+ drainWaitMs,
123
+ maxBufferedAmountBytes,
124
+ maxQueuedBytes,
125
+ logger
126
+ });
127
+ scheduleKeepaliveFrame(websocket, clientState, {
128
+ drainWaitMs,
129
+ keepaliveIntervalMs,
130
+ maxBufferedAmountBytes,
131
+ maxQueuedBytes,
132
+ logger
133
+ }, keepaliveInitialDelayMs);
134
+ websocket.on("close", (code, reason) => {
135
+ const closeReason = Buffer.from(reason).toString("utf8");
136
+ logger.info(`WebSocket client disconnected code=${code}${closeReason ? ` reason=${closeReason}` : ""}`);
137
+ dispose();
138
+ });
139
+ websocket.on("error", (error) => {
140
+ const message = error instanceof Error ? error.message : String(error);
141
+ logger.warn(`WebSocket client error: ${message}`);
142
+ dispose();
143
+ });
144
+ websocket.on("message", (data, isBinary) => {
145
+ if (!clients.get(websocket)?.active) return;
146
+ if (isBinary) {
147
+ const payload = toBuffer(data);
148
+ scheduleKeepaliveFrame(websocket, clientState, {
149
+ drainWaitMs,
150
+ keepaliveIntervalMs,
151
+ maxBufferedAmountBytes,
152
+ maxQueuedBytes,
153
+ logger
154
+ }, keepaliveIntervalMs);
155
+ if (containsPriorityControlByte(payload)) prioritizeClientOutput(clientState);
156
+ inputWriteChain = inputWriteChain.catch(() => void 0).then(() => options.session.write(payload));
157
+ return;
158
+ }
159
+ handleControlFrame(websocket, data, options.session, logger);
160
+ scheduleKeepaliveFrame(websocket, clientState, {
161
+ drainWaitMs,
162
+ keepaliveIntervalMs,
163
+ maxBufferedAmountBytes,
164
+ maxQueuedBytes,
165
+ logger
166
+ }, keepaliveIntervalMs);
167
+ });
168
+ });
169
+ return {
170
+ async close() {
171
+ if (closed) return;
172
+ closed = true;
173
+ server.removeListener("upgrade", onUpgrade);
174
+ sessionSubscription.dispose();
175
+ themeSubscription.dispose();
176
+ await Promise.all(Array.from(clients.keys(), (client) => shutdownClient(client)));
177
+ await closeWebSocketServer(websocketServer);
178
+ },
179
+ async notifyExit(event) {
180
+ if (closed || exitNotified) return;
181
+ exitNotified = true;
182
+ const payload = createExitControlFrame({
183
+ code: event.signal ? null : event.exitCode,
184
+ signal: resolveSignalName(event.signal)
185
+ });
186
+ await Promise.all(Array.from(clients.entries(), ([client, state]) => {
187
+ if (!state.active) return Promise.resolve();
188
+ return sendExitAndClose(client, payload);
189
+ }));
190
+ }
191
+ };
192
+ }
193
+ function handleControlFrame(websocket, data, session, logger) {
194
+ const controlFrame = parseAittyControlFrame(toBuffer(data).toString("utf8"));
195
+ if (!controlFrame) {
196
+ logger.warn("Ignoring malformed WebSocket control frame");
197
+ return;
198
+ }
199
+ if (controlFrame.type === "ping") {
200
+ sendPongFrame(websocket);
201
+ return;
202
+ }
203
+ if (controlFrame.type === "pong") return;
204
+ if (controlFrame.type === "resize") {
205
+ handleResizeControlFrame(controlFrame, session, logger);
206
+ return;
207
+ }
208
+ logger.warn(`Ignoring unknown WebSocket control frame type: ${controlFrame.type}`);
209
+ }
210
+ async function sendPongFrame(websocket) {
211
+ if (websocket.readyState !== SOCKET_OPEN) return;
212
+ try {
213
+ await sendFrame(websocket, createPongControlFrame(), false);
214
+ } catch {}
215
+ }
216
+ function handleResizeControlFrame(frame, session, logger) {
217
+ if (!isAittyResizeControlFrame(frame)) {
218
+ logger.warn("Ignoring invalid WebSocket resize frame");
219
+ return;
220
+ }
221
+ if (frame.cols === session.cols && frame.rows === session.rows) return;
222
+ session.resize(frame.cols, frame.rows);
223
+ }
224
+ function rejectUpgrade(socket, statusCode, statusText) {
225
+ const body = statusText;
226
+ socket.write(`HTTP/1.1 ${statusCode} ${statusText}\r\nConnection: close\r
227
+ Content-Type: text/plain; charset=utf-8\r
228
+ Content-Length: ${Buffer.byteLength(body)}\r\n\r
229
+ ` + body);
230
+ socket.destroy();
231
+ }
232
+ function toBuffer(data) {
233
+ if (typeof data === "string") return Buffer.from(data, "utf8");
234
+ if (Buffer.isBuffer(data)) return data;
235
+ if (Array.isArray(data)) return Buffer.concat(data.map((chunk) => toBuffer(chunk)));
236
+ return Buffer.from(data);
237
+ }
238
+ function createHelloFrame(session, replay) {
239
+ return createHelloControlFrame({
240
+ cols: session.cols,
241
+ replay: replay ? replay.toString("base64") : "",
242
+ rows: session.rows
243
+ });
244
+ }
245
+ function enqueueClientFrame(websocket, clientState, payload, binary, options) {
246
+ if (!clientState.active || websocket.readyState !== SOCKET_OPEN) return;
247
+ const frame = createOutboundFrame(payload, binary, options.allowOversizedFrame ?? false);
248
+ if (frame.allowOversized) clientState.oversizedAllowanceBytes = Math.max(clientState.oversizedAllowanceBytes, frame.byteLength);
249
+ const queuedLimit = Math.max(options.maxQueuedBytes, clientState.oversizedAllowanceBytes);
250
+ if (clientState.pendingBytes + frame.byteLength > queuedLimit) {
251
+ if (frame.allowOversized && clientState.pendingBytes === 0 && clientState.pendingFrames.length === 0) {
252
+ clientState.pendingFrames.push(frame);
253
+ clientState.pendingBytes += frame.byteLength;
254
+ pumpClientQueue(websocket, clientState, options.maxBufferedAmountBytes, options.drainWaitMs, options.logger);
255
+ return;
256
+ }
257
+ closeClientForBackpressure(websocket, clientState, options.logger);
258
+ return;
259
+ }
260
+ clientState.pendingFrames.push(frame);
261
+ clientState.pendingBytes += frame.byteLength;
262
+ pumpClientQueue(websocket, clientState, options.maxBufferedAmountBytes, options.drainWaitMs, options.logger);
263
+ }
264
+ function createOutboundFrame(payload, binary, allowOversized) {
265
+ return {
266
+ allowOversized,
267
+ binary,
268
+ byteLength: typeof payload === "string" ? Buffer.byteLength(payload) : payload.length,
269
+ payload
270
+ };
271
+ }
272
+ async function pumpClientQueue(websocket, clientState, maxBufferedAmountBytes, drainWaitMs, logger) {
273
+ if (clientState.sending) return;
274
+ clientState.sending = true;
275
+ try {
276
+ while (clientState.active && websocket.readyState === SOCKET_OPEN) {
277
+ const frame = clientState.pendingFrames[0];
278
+ if (!frame) {
279
+ if (websocket.bufferedAmount <= maxBufferedAmountBytes) clientState.oversizedAllowanceBytes = 0;
280
+ return;
281
+ }
282
+ const bufferedLimit = Math.max(maxBufferedAmountBytes, clientState.oversizedAllowanceBytes);
283
+ if (websocket.bufferedAmount > bufferedLimit) {
284
+ await waitForSocketWritable(websocket, clientState.priorityModeUntil > Date.now() ? Math.max(drainWaitMs, DEFAULT_PRIORITY_OUTPUT_RECOVERY_MS) : drainWaitMs);
285
+ if (!clientState.active || websocket.readyState !== SOCKET_OPEN) return;
286
+ if (websocket.bufferedAmount > bufferedLimit) {
287
+ if (clientState.priorityModeUntil > Date.now()) continue;
288
+ await closeClientForBackpressure(websocket, clientState, logger);
289
+ return;
290
+ }
291
+ }
292
+ clientState.pendingFrames.shift();
293
+ clientState.pendingBytes -= frame.byteLength;
294
+ try {
295
+ await sendFrame(websocket, frame.payload, frame.binary);
296
+ if (clientState.pendingFrames.every((pendingFrame) => !pendingFrame.allowOversized) && websocket.bufferedAmount <= maxBufferedAmountBytes) clientState.oversizedAllowanceBytes = 0;
297
+ } catch {
298
+ return;
299
+ }
300
+ }
301
+ } finally {
302
+ clientState.sending = false;
303
+ if (clientState.active && websocket.readyState === SOCKET_OPEN && clientState.pendingFrames.length > 0) pumpClientQueue(websocket, clientState, maxBufferedAmountBytes, drainWaitMs, logger);
304
+ }
305
+ }
306
+ function resolveSignalName(signal) {
307
+ if (!signal) return null;
308
+ for (const [name, value] of Object.entries(os.constants.signals)) if (value === signal) return name;
309
+ return `SIG${signal}`;
310
+ }
311
+ async function sendExitAndClose(websocket, payload) {
312
+ if (websocket.readyState === SOCKET_CLOSED) return;
313
+ if (websocket.readyState === SOCKET_OPEN) {
314
+ try {
315
+ await sendFrame(websocket, payload, false);
316
+ } catch {}
317
+ websocket.close(NORMAL_CLOSE_CODE);
318
+ }
319
+ if (!await waitForSocketClose(websocket)) {
320
+ websocket.terminate();
321
+ await waitForSocketClose(websocket);
322
+ }
323
+ }
324
+ async function supersedeClient(websocket) {
325
+ if (websocket.readyState === SOCKET_CLOSED) return;
326
+ if (websocket.readyState === SOCKET_OPEN) websocket.close(SUPERSEDED_CLOSE_CODE, SUPERSEDED_CLOSE_REASON);
327
+ if (!await waitForSocketClose(websocket)) {
328
+ websocket.terminate();
329
+ await waitForSocketClose(websocket);
330
+ }
331
+ }
332
+ async function closeClientForBackpressure(websocket, clientState, logger) {
333
+ if (!clientState.active) return;
334
+ clientState.active = false;
335
+ clientState.pendingBytes = 0;
336
+ clientState.pendingFrames.length = 0;
337
+ logger.warn("Closing slow WebSocket client after backpressure limit exceeded");
338
+ if (websocket.readyState === SOCKET_CLOSED) return;
339
+ if (websocket.readyState === SOCKET_OPEN) websocket.close(BACKPRESSURE_CLOSE_CODE, BACKPRESSURE_CLOSE_REASON);
340
+ else {
341
+ websocket.terminate();
342
+ await waitForSocketClose(websocket);
343
+ return;
344
+ }
345
+ if (!await waitForSocketClose(websocket)) {
346
+ websocket.terminate();
347
+ await waitForSocketClose(websocket);
348
+ }
349
+ }
350
+ async function shutdownClient(websocket) {
351
+ if (websocket.readyState === SOCKET_CLOSED) return;
352
+ websocket.terminate();
353
+ await waitForSocketClose(websocket);
354
+ }
355
+ function closeWebSocketServer(websocketServer) {
356
+ return new Promise((resolve, reject) => {
357
+ websocketServer.close((error) => {
358
+ if (error) {
359
+ reject(error);
360
+ return;
361
+ }
362
+ resolve();
363
+ });
364
+ });
365
+ }
366
+ function sendFrame(websocket, payload, binary = Buffer.isBuffer(payload)) {
367
+ return new Promise((resolve, reject) => {
368
+ websocket.send(payload, { binary }, (error) => {
369
+ if (error) {
370
+ reject(error);
371
+ return;
372
+ }
373
+ resolve();
374
+ });
375
+ });
376
+ }
377
+ function waitForSocketWritable(websocket, timeout) {
378
+ const socket = getUnderlyingSocket(websocket);
379
+ if (!socket || websocket.bufferedAmount === 0 && !socket.writableNeedDrain) return Promise.resolve();
380
+ return new Promise((resolve) => {
381
+ const finish = once(() => {
382
+ clearTimeout(timer);
383
+ socket.removeListener("drain", finish);
384
+ websocket.removeListener("close", finish);
385
+ websocket.removeListener("error", finish);
386
+ resolve();
387
+ });
388
+ const timer = setTimeout(() => {
389
+ finish();
390
+ }, timeout);
391
+ socket.once("drain", finish);
392
+ websocket.once("close", finish);
393
+ websocket.once("error", finish);
394
+ });
395
+ }
396
+ function waitForSocketClose(websocket, timeout = 250) {
397
+ if (websocket.readyState === SOCKET_CLOSED) return Promise.resolve(true);
398
+ return new Promise((resolve) => {
399
+ const finish = once((closed) => {
400
+ clearTimeout(timer);
401
+ websocket.removeListener("close", onClose);
402
+ websocket.removeListener("error", onError);
403
+ resolve(closed);
404
+ });
405
+ const timer = setTimeout(() => {
406
+ finish(websocket.readyState === SOCKET_CLOSED);
407
+ }, timeout);
408
+ const onClose = () => {
409
+ finish(true);
410
+ };
411
+ const onError = () => {
412
+ finish(websocket.readyState === SOCKET_CLOSED);
413
+ };
414
+ websocket.once("close", onClose);
415
+ websocket.once("error", onError);
416
+ });
417
+ }
418
+ function getUnderlyingSocket(websocket) {
419
+ return websocket._socket ?? null;
420
+ }
421
+ function once(callback) {
422
+ let called = false;
423
+ return (...args) => {
424
+ if (called) return;
425
+ called = true;
426
+ callback(...args);
427
+ };
428
+ }
429
+ function clearKeepaliveTimer(clientState) {
430
+ if (clientState.keepaliveTimer === null) return;
431
+ clearTimeout(clientState.keepaliveTimer);
432
+ clientState.keepaliveTimer = null;
433
+ }
434
+ function scheduleKeepaliveFrame(websocket, clientState, options, delayMs) {
435
+ clearKeepaliveTimer(clientState);
436
+ if (options.keepaliveIntervalMs <= 0) return;
437
+ clientState.keepaliveTimer = setTimeout(() => {
438
+ clientState.keepaliveTimer = null;
439
+ if (!clientState.active || websocket.readyState !== SOCKET_OPEN) return;
440
+ enqueueClientFrame(websocket, clientState, createPingControlFrame(), false, {
441
+ drainWaitMs: options.drainWaitMs,
442
+ maxBufferedAmountBytes: options.maxBufferedAmountBytes,
443
+ maxQueuedBytes: options.maxQueuedBytes,
444
+ logger: options.logger
445
+ });
446
+ scheduleKeepaliveFrame(websocket, clientState, options, options.keepaliveIntervalMs);
447
+ }, Math.max(0, delayMs));
448
+ }
449
+ function getActiveClient(clients) {
450
+ for (const entry of clients.entries()) if (entry[1].active) return entry;
451
+ return null;
452
+ }
453
+ function containsPriorityControlByte(payload) {
454
+ return payload.includes(3) || payload.includes(12) || isStandaloneEscapeByte(payload);
455
+ }
456
+ function isStandaloneEscapeByte(payload) {
457
+ return payload.length === 1 && payload[0] === 27;
458
+ }
459
+ function prioritizeClientOutput(clientState) {
460
+ clientState.pendingBytes = 0;
461
+ clientState.pendingFrames.length = 0;
462
+ clientState.priorityModeUntil = Date.now() + DEFAULT_PRIORITY_OUTPUT_RECOVERY_MS;
463
+ }
464
+ //#endregion
465
+ export { createWebSocketTransport };
@@ -0,0 +1,51 @@
1
+ import { BrowserRuntimeKind, BrowserShellOptions } from "./frontend/browser-shell.js";
2
+ import { PtyModule, PtySession } from "./runtime/pty-session.js";
3
+ import { AittyThemeSource as AittyThemeSource$1 } from "./theme-source.js";
4
+ import { AittyPortlessIdentity, AittyPortlessIdentity as AittyPortlessIdentity$1, AittyPortlessOptions, AittyPortlessOptions as AittyPortlessOptions$1, AittyTheme } from "@aitty/protocol";
5
+ import * as _$ws from "ws";
6
+
7
+ //#region src/server.d.ts
8
+ interface AittyRuntimeDependencies {
9
+ WebSocketServer?: typeof _$ws.WebSocketServer;
10
+ nodePty: PtyModule;
11
+ }
12
+ interface AittyServerOptions {
13
+ args?: string[];
14
+ bufferSize?: number;
15
+ command: string;
16
+ cwd: string;
17
+ env?: NodeJS.ProcessEnv;
18
+ host?: string;
19
+ port?: number;
20
+ portless?: AittyPortlessOptions$1;
21
+ publicHost?: string;
22
+ publicOrigin?: string;
23
+ runtimeKind?: BrowserRuntimeKind;
24
+ shell?: BrowserShellOptions;
25
+ theme?: AittyTheme;
26
+ themeSource?: AittyThemeSource$1;
27
+ verbose?: boolean;
28
+ }
29
+ interface AittyRunningServer {
30
+ childPid: number;
31
+ close(signal?: NodeJS.Signals): Promise<number>;
32
+ closed: Promise<number>;
33
+ host: string;
34
+ port: number;
35
+ portless: AittyPortlessIdentity$1;
36
+ session: PtySession;
37
+ token: string;
38
+ url: string;
39
+ }
40
+ interface Writer {
41
+ write(chunk: string): boolean;
42
+ }
43
+ interface AittyServerIo {
44
+ registerSignalHandlers?: boolean;
45
+ stderr?: Writer;
46
+ }
47
+ declare function createAittyServer(options: AittyServerOptions, dependencies: AittyRuntimeDependencies, io?: AittyServerIo): Promise<AittyRunningServer>;
48
+ declare function buildAittyUrl(origin: string, token: string, pathname: string): string;
49
+ declare function buildAittyOrigin(host: string, port: number): string;
50
+ //#endregion
51
+ export { type AittyPortlessIdentity, type AittyPortlessOptions, AittyRunningServer, AittyRuntimeDependencies, AittyServerIo, AittyServerOptions, Writer, buildAittyOrigin, buildAittyUrl, createAittyServer };