geonix 1.23.8 → 1.30.2
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/LICENSE.md +1 -1
- package/README.md +348 -4
- package/exports.js +0 -2
- package/index.d.ts +292 -237
- package/package.json +11 -10
- package/src/Codec.js +20 -7
- package/src/Connection.js +94 -40
- package/src/Crypto.js +103 -0
- package/src/Gateway.js +146 -70
- package/src/Logger.js +90 -9
- package/src/Registry.js +127 -15
- package/src/Remote.js +15 -6
- package/src/Request.js +117 -80
- package/src/RequestOptions.js +11 -8
- package/src/Service.js +128 -92
- package/src/Stream.js +69 -15
- package/src/Util.js +192 -158
- package/src/WebServer.js +18 -10
- package/.claude/settings.local.json +0 -10
- package/PROJECT.md +0 -164
- package/REVIEW.md +0 -372
package/src/Util.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "crypto";
|
|
2
|
-
import { URL
|
|
3
|
-
import {
|
|
2
|
+
import { URL } from "url";
|
|
3
|
+
import { createRequire } from "module";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { Transform } from "node:stream";
|
|
6
6
|
import { networkInterfaces } from "os";
|
|
@@ -24,7 +24,7 @@ export const sleep = delay => new Promise(resolve => setTimeout(resolve, delay))
|
|
|
24
24
|
*
|
|
25
25
|
* @returns
|
|
26
26
|
*/
|
|
27
|
-
export const
|
|
27
|
+
export const yieldToEventLoop = () => new Promise(resolve => setImmediate(resolve));
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Parse nats:// URL
|
|
@@ -47,12 +47,29 @@ export function parseURL(url) {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
const BASE62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
51
|
+
const LOG256_LOG62 = Math.log(256) / Math.log(62); // ≈ 1.3437
|
|
52
|
+
|
|
53
|
+
export function encodeBase62(buffer) {
|
|
54
|
+
if (buffer.length === 0) { return ""; }
|
|
55
|
+
const len = Math.ceil(buffer.length * LOG256_LOG62);
|
|
56
|
+
let n = BigInt("0x" + buffer.toString("hex"));
|
|
57
|
+
const chars = new Array(len);
|
|
58
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
59
|
+
chars[i] = BASE62[Number(n % 62n)];
|
|
60
|
+
n /= 62n;
|
|
61
|
+
}
|
|
62
|
+
return chars.join("");
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
66
|
+
* Generates a cryptographically random Base62-encoded ID string.
|
|
67
|
+
* Exported as `randomID` in the public API.
|
|
68
|
+
*
|
|
69
|
+
* @param {number} [size=16] - Number of random bytes to encode (more bytes → longer, more unique ID).
|
|
70
|
+
* @returns {string} URL-safe, Base62-encoded random string.
|
|
54
71
|
*/
|
|
55
|
-
export const picoid = (size =
|
|
72
|
+
export const picoid = (size = 16) => encodeBase62(randomBytes(size));
|
|
56
73
|
|
|
57
74
|
/**
|
|
58
75
|
* Get SHA256 hash of a string or a buffer
|
|
@@ -81,39 +98,24 @@ export const createServerAtPort = (port, pkg, handler) =>
|
|
|
81
98
|
});
|
|
82
99
|
|
|
83
100
|
/**
|
|
84
|
-
* Create TCP or HTTP server at
|
|
85
|
-
* @param {Object} pkg
|
|
86
|
-
* @param {Function} handler
|
|
87
|
-
* @
|
|
88
|
-
* @param {number} poolSize
|
|
89
|
-
* @returns
|
|
90
|
-
*/
|
|
91
|
-
export const createServerAtFreePort = async (pkg, handler, start = 30000, poolSize = 20000) => {
|
|
92
|
-
for (let port = start; port < start + poolSize; port++) {
|
|
93
|
-
try {
|
|
94
|
-
const result = await createServerAtPort(port, pkg, handler);
|
|
95
|
-
if (result) {
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
} catch {
|
|
99
|
-
// silenty ignore errors
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Create TCP server at random port
|
|
106
|
-
* @param {Function} handler
|
|
107
|
-
* @returns
|
|
101
|
+
* Create TCP or HTTP server at an OS-assigned free port
|
|
102
|
+
* @param {Object} pkg
|
|
103
|
+
* @param {Function} handler
|
|
104
|
+
* @returns
|
|
108
105
|
*/
|
|
109
|
-
export const
|
|
106
|
+
export const createServerAtFreePort = (pkg, handler) =>
|
|
107
|
+
new Promise((resolve, reject) => {
|
|
108
|
+
const server = pkg.createServer(handler);
|
|
109
|
+
server.on("error", reject);
|
|
110
|
+
server.listen(0, () => resolve({ server, port: server.address().port }));
|
|
111
|
+
});
|
|
110
112
|
|
|
111
113
|
/**
|
|
112
|
-
* Create
|
|
113
|
-
* @param {Function} handler
|
|
114
|
-
* @returns
|
|
114
|
+
* Create TCP server at an OS-assigned free port
|
|
115
|
+
* @param {Function} handler
|
|
116
|
+
* @returns
|
|
115
117
|
*/
|
|
116
|
-
export const
|
|
118
|
+
export const createTCPServer = (handler) => createServerAtFreePort(net, handler);
|
|
117
119
|
|
|
118
120
|
/**
|
|
119
121
|
* Return number of seconds passed from the start of the day (0-86399)
|
|
@@ -124,39 +126,6 @@ export const getSecondsSinceMidnight = () => {
|
|
|
124
126
|
return Math.floor((date.getTime() - date.setHours(0, 0, 0, 0)) / 1000);
|
|
125
127
|
};
|
|
126
128
|
|
|
127
|
-
/**
|
|
128
|
-
* Parse function body and return array of param names
|
|
129
|
-
* @param {*} fn
|
|
130
|
-
* @returns string[]
|
|
131
|
-
*/
|
|
132
|
-
export const getFunctionParams = (fn) => {
|
|
133
|
-
const code = fn.toString();
|
|
134
|
-
const endParenthesisPosition = code.indexOf(")");
|
|
135
|
-
let params;
|
|
136
|
-
|
|
137
|
-
if (endParenthesisPosition != -1) {
|
|
138
|
-
params = code.substring(code.indexOf("(") + 1, endParenthesisPosition);
|
|
139
|
-
} else {
|
|
140
|
-
params = code.substring(0, code.indexOf("=>"));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
params = params
|
|
144
|
-
// cleanup spaces
|
|
145
|
-
.replaceAll(" ", "")
|
|
146
|
-
// split into array
|
|
147
|
-
.split(",");
|
|
148
|
-
|
|
149
|
-
// remove potential default values
|
|
150
|
-
for (let index = 0; index < params.length; index++) {
|
|
151
|
-
const defaultValueAssignmentPosition = params[index].indexOf("=");
|
|
152
|
-
if (defaultValueAssignmentPosition != -1) {
|
|
153
|
-
params[index] = params[index].substring(0, defaultValueAssignmentPosition - 1);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return params;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
129
|
export const proxyHttp = (target, req, res) =>
|
|
161
130
|
new Promise((resolve, reject) => {
|
|
162
131
|
const remoteTarget = `${target}${req.originalUrl}`;
|
|
@@ -188,37 +157,40 @@ export const proxyHttp = (target, req, res) =>
|
|
|
188
157
|
*/
|
|
189
158
|
export const OverlayObject = (object, overlay) => new Proxy(object, { get: (t, p) => overlay[p] !== undefined ? overlay[p] : t[p] });
|
|
190
159
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
160
|
+
/**
|
|
161
|
+
* The version string of the currently installed Geonix package, read from `package.json` at
|
|
162
|
+
* module load time. Equals `"N/A"` when the package metadata cannot be found.
|
|
163
|
+
*
|
|
164
|
+
* @type {string}
|
|
165
|
+
*/
|
|
166
|
+
export const GeonixVersion = (() => {
|
|
167
|
+
try {
|
|
168
|
+
return createRequire(import.meta.url)("../package.json").version ?? "N/A";
|
|
169
|
+
} catch {
|
|
170
|
+
return "N/A";
|
|
171
|
+
}
|
|
172
|
+
})();
|
|
204
173
|
|
|
205
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Chunk a stream into smaller chunks
|
|
176
|
+
*
|
|
177
|
+
* @param {*} chunkSize
|
|
178
|
+
* @returns
|
|
179
|
+
*/
|
|
180
|
+
export const StreamChunker = (chunkSize = 65536) => new Transform({
|
|
181
|
+
transform(chunk, _encoding, done) {
|
|
206
182
|
let offset = 0;
|
|
207
183
|
while (offset < chunk.length) {
|
|
208
184
|
const sliceSize = Math.min(chunkSize, chunk.length - offset);
|
|
209
185
|
this.push(chunk.slice(offset, offset + sliceSize));
|
|
210
186
|
offset += sliceSize;
|
|
211
187
|
}
|
|
212
|
-
|
|
213
188
|
done();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
chunker._flush = function (done) {
|
|
189
|
+
},
|
|
190
|
+
flush(done) {
|
|
217
191
|
done();
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return chunker;
|
|
221
|
-
};
|
|
192
|
+
}
|
|
193
|
+
});
|
|
222
194
|
|
|
223
195
|
export async function getFirstItemFromAsyncIterable(asyncIterable) {
|
|
224
196
|
const iterator = asyncIterable[Symbol.asyncIterator]();
|
|
@@ -250,15 +222,24 @@ export function isIterable(obj) {
|
|
|
250
222
|
}
|
|
251
223
|
|
|
252
224
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* @
|
|
256
|
-
*
|
|
257
|
-
*
|
|
225
|
+
* Parses a `multipart/form-data` request body into an array of part objects. Each part
|
|
226
|
+
* exposes parsed headers (e.g. `content-disposition`), a `name`, an optional `filename`,
|
|
227
|
+
* and a `body` {@link import('stream').Readable}.
|
|
228
|
+
*
|
|
229
|
+
* By default parts are streamed through temporary files on disk; set `options.useMemory` to
|
|
230
|
+
* `true` to buffer them in memory instead.
|
|
231
|
+
*
|
|
232
|
+
* @param {import('http').IncomingMessage} req - Incoming HTTP request with a `multipart/form-data` content-type.
|
|
233
|
+
* @param {object} [_options] - Parsing options.
|
|
234
|
+
* @param {boolean} [_options.useMemory=false] - Buffer parts in memory instead of temp files.
|
|
235
|
+
* @param {number} [_options.maxFileSize] - Maximum allowed size in bytes for a single part.
|
|
236
|
+
* @param {number} [_options.maxFiles] - Maximum number of parts allowed.
|
|
237
|
+
* @returns {Promise<Array<{ name: string|null, filename: string|null, headers: object, body: import('stream').Readable, size: number }>>}
|
|
238
|
+
* @throws {Error} If the content-type is not `multipart/form-data` or a size/count limit is exceeded.
|
|
258
239
|
*/
|
|
259
240
|
export async function parseMultipart(req, _options) {
|
|
260
241
|
if (!req.headers["content-type"]?.startsWith("multipart/form-data")) {
|
|
261
|
-
throw
|
|
242
|
+
throw Error("Invalid content type (multipart/form-data expected)");
|
|
262
243
|
}
|
|
263
244
|
|
|
264
245
|
const BUFFER_SIZE = 1024 * 1024;
|
|
@@ -276,15 +257,41 @@ export async function parseMultipart(req, _options) {
|
|
|
276
257
|
options.useMemory = true;
|
|
277
258
|
}
|
|
278
259
|
|
|
279
|
-
const
|
|
260
|
+
const boundaryValue = req.headers["content-type"].match(/boundary=([^;,\s]+)/)?.[1];
|
|
261
|
+
if (!boundaryValue) {
|
|
262
|
+
throw Error("parseMultipart: missing boundary in content-type");
|
|
263
|
+
}
|
|
264
|
+
const boundary = Buffer.from("\r\n--" + boundaryValue);
|
|
280
265
|
|
|
281
|
-
await
|
|
266
|
+
await yieldToEventLoop();
|
|
282
267
|
|
|
283
268
|
let lastChunk = Buffer.from("\r\n");
|
|
284
269
|
let activePart;
|
|
285
|
-
|
|
270
|
+
|
|
271
|
+
const cleanup = async () => {
|
|
272
|
+
for (const part of parts) {
|
|
273
|
+
if (part.bodyFile) {
|
|
274
|
+
try {
|
|
275
|
+
part.body.destroy();
|
|
276
|
+
} catch {
|
|
277
|
+
// ignore errors
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
await unlink(part.bodyFile);
|
|
281
|
+
} catch {
|
|
282
|
+
// ignore errors
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
286
287
|
|
|
287
288
|
const write = (chunk) => {
|
|
289
|
+
if (options.maxFileSize !== undefined) {
|
|
290
|
+
activePart.size += chunk.length;
|
|
291
|
+
if (activePart.size > options.maxFileSize) {
|
|
292
|
+
throw Error(`parseMultipart: part exceeds maxFileSize of ${options.maxFileSize} bytes`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
288
295
|
if (options.useMemory) {
|
|
289
296
|
activePart.body.push(chunk);
|
|
290
297
|
} else {
|
|
@@ -293,75 +300,95 @@ export async function parseMultipart(req, _options) {
|
|
|
293
300
|
};
|
|
294
301
|
|
|
295
302
|
const newPart = () => {
|
|
296
|
-
|
|
303
|
+
if (options.maxFiles !== undefined && parts.length >= options.maxFiles) {
|
|
304
|
+
throw Error(`parseMultipart: exceeded maxFiles limit of ${options.maxFiles}`);
|
|
305
|
+
}
|
|
297
306
|
const bodyFile = tempFilename();
|
|
298
307
|
activePart = {
|
|
299
308
|
headers: {},
|
|
300
309
|
bodyFile: options.useMemory ? undefined : bodyFile,
|
|
301
|
-
body: options.useMemory ? [] : createWriteStream(bodyFile, { flags: "wx" })
|
|
310
|
+
body: options.useMemory ? [] : createWriteStream(bodyFile, { flags: "wx" }),
|
|
311
|
+
size: 0
|
|
302
312
|
};
|
|
303
313
|
parts.push(activePart);
|
|
304
314
|
};
|
|
305
315
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
316
|
+
try {
|
|
317
|
+
while (stream.readable) {
|
|
318
|
+
// next next chunk
|
|
319
|
+
let chunk = stream.read(BUFFER_SIZE);
|
|
309
320
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
321
|
+
if (!chunk) {
|
|
322
|
+
await yieldToEventLoop();
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
314
325
|
|
|
315
|
-
|
|
326
|
+
let combined = Buffer.concat([lastChunk, chunk]);
|
|
327
|
+
let lookbehindSet = false;
|
|
328
|
+
|
|
329
|
+
while (combined.length >= boundary.length + 2) {
|
|
330
|
+
const boundaryIndex = combined.indexOf(boundary);
|
|
331
|
+
|
|
332
|
+
if (boundaryIndex === -1) {
|
|
333
|
+
// Keep only the last boundary.length-1 bytes as lookbehind so a
|
|
334
|
+
// boundary that straddles a read boundary is not split across chunks.
|
|
335
|
+
const safeLength = combined.length - (boundary.length - 1);
|
|
336
|
+
if (activePart && safeLength > 0) {
|
|
337
|
+
write(combined.subarray(0, safeLength));
|
|
338
|
+
}
|
|
339
|
+
lastChunk = combined.subarray(safeLength);
|
|
340
|
+
lookbehindSet = true;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
316
343
|
|
|
317
|
-
|
|
318
|
-
const boundaryIndex = combined.indexOf(boundary);
|
|
319
|
-
const isLastBoundary = combined[boundaryIndex + boundary.length] === 45 && combined[boundaryIndex + boundary.length + 1] === 45;
|
|
344
|
+
const isLastBoundary = combined[boundaryIndex + boundary.length] === 45 && combined[boundaryIndex + boundary.length + 1] === 45;
|
|
320
345
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
346
|
+
if (boundaryIndex > 0) {
|
|
347
|
+
write(combined.subarray(0, boundaryIndex));
|
|
348
|
+
}
|
|
325
349
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
350
|
+
if (isLastBoundary) {
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
329
353
|
|
|
330
|
-
|
|
331
|
-
combined = combined.subarray(boundaryIndex + boundary.length + 2);
|
|
332
|
-
done = true;
|
|
333
|
-
break;
|
|
334
|
-
}
|
|
354
|
+
newPart();
|
|
335
355
|
|
|
336
|
-
|
|
356
|
+
const endOfHeaders = combined.indexOf(END_OF_HEADERS, boundaryIndex);
|
|
337
357
|
|
|
338
|
-
|
|
358
|
+
if (endOfHeaders === -1) {
|
|
359
|
+
throw Error("parseMultipart: malformed part — missing header terminator");
|
|
360
|
+
}
|
|
339
361
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
362
|
+
activePart.headers = combined
|
|
363
|
+
.subarray(boundaryIndex + boundary.length + 2, endOfHeaders).toString()
|
|
364
|
+
.split("\r\n")
|
|
365
|
+
.reduce((acc, val) => {
|
|
366
|
+
const [header, value] = val.split(": ");
|
|
367
|
+
acc[header.toLowerCase()] = value;
|
|
368
|
+
return acc;
|
|
369
|
+
}, Object.create(null));
|
|
348
370
|
|
|
349
|
-
|
|
371
|
+
combined = combined.subarray(endOfHeaders + END_OF_HEADERS.length);
|
|
350
372
|
|
|
351
|
-
|
|
352
|
-
|
|
373
|
+
lastChunk = combined;
|
|
374
|
+
}
|
|
353
375
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
376
|
+
// Carry the unprocessed remainder into the next read so it gets
|
|
377
|
+
// prepended to the next chunk. Skip when the lookbehind was already
|
|
378
|
+
// set inside the boundaryIndex === -1 branch above.
|
|
379
|
+
if (!lookbehindSet) {
|
|
380
|
+
lastChunk = combined;
|
|
381
|
+
}
|
|
358
382
|
}
|
|
383
|
+
} catch (e) {
|
|
384
|
+
await cleanup();
|
|
385
|
+
throw e;
|
|
359
386
|
}
|
|
360
387
|
|
|
361
388
|
for (const part of parts) {
|
|
362
389
|
// extract name and filename from content-disposition header
|
|
363
390
|
if (part.headers["content-disposition"]) {
|
|
364
|
-
const [, name] = part.headers["content-disposition"].match(/name="([^"]+)"/);
|
|
391
|
+
const [, name] = part.headers["content-disposition"].match(/name="([^"]+)"/) || [];
|
|
365
392
|
const [, filename] = part.headers["content-disposition"].match(/filename="([^"]+)"/) || [];
|
|
366
393
|
part.name = name ?? null;
|
|
367
394
|
part.filename = filename ?? null;
|
|
@@ -387,24 +414,13 @@ export async function parseMultipart(req, _options) {
|
|
|
387
414
|
}
|
|
388
415
|
|
|
389
416
|
export function tempFilename() {
|
|
390
|
-
return join(tmpdir(), `${
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
export function randomSafeId(size = 12) {
|
|
394
|
-
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
395
|
-
let result = "";
|
|
396
|
-
|
|
397
|
-
for (let i = 0; i < size; i++) {
|
|
398
|
-
result += charset.charAt(Math.floor(Math.random() * charset.length));
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return result;
|
|
417
|
+
return join(tmpdir(), `${picoid(12)}.gxtmp`);
|
|
402
418
|
}
|
|
403
419
|
|
|
404
420
|
export function deepMerge(target, ...source) {
|
|
405
421
|
for (const src of source) {
|
|
406
|
-
for (const key
|
|
407
|
-
if (src[key] instanceof Object) {
|
|
422
|
+
for (const key of Object.keys(src)) {
|
|
423
|
+
if (src[key] instanceof Object && !Array.isArray(src[key])) {
|
|
408
424
|
if (!target[key]) {
|
|
409
425
|
target[key] = {};
|
|
410
426
|
}
|
|
@@ -427,4 +443,22 @@ export function cleanupWebsocketUrl(url) {
|
|
|
427
443
|
} catch {
|
|
428
444
|
return url;
|
|
429
445
|
}
|
|
430
|
-
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export async function fetchWithTimeout(url, options = {}, timeout = 500) {
|
|
449
|
+
const ac = new AbortController();
|
|
450
|
+
const timer = setTimeout(() => ac.abort(), timeout);
|
|
451
|
+
try {
|
|
452
|
+
return await fetch(url, { ...options, signal: ac.signal });
|
|
453
|
+
} finally {
|
|
454
|
+
clearTimeout(timer);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function withTimeout(promise, timeout) {
|
|
459
|
+
let timeoutId;
|
|
460
|
+
const timer = new Promise((_, reject) => {
|
|
461
|
+
timeoutId = setTimeout(() => reject(Error("Timeout")), timeout);
|
|
462
|
+
});
|
|
463
|
+
return Promise.race([promise, timer]).finally(() => clearTimeout(timeoutId));
|
|
464
|
+
}
|
package/src/WebServer.js
CHANGED
|
@@ -3,13 +3,21 @@ import express, { Router } from "express";
|
|
|
3
3
|
import expressWs from "express-ws";
|
|
4
4
|
import { createServerAtFreePort, createServerAtPort, sleep } from "./Util.js";
|
|
5
5
|
import * as http from "http";
|
|
6
|
-
import { Service } from "./Service.js";
|
|
7
6
|
import * as path from "path";
|
|
8
7
|
import { logger } from "./Logger.js";
|
|
9
8
|
import { activeStreams } from "./Stream.js";
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Creates an Express router that serves static files from `root`. Supports an optional URL
|
|
12
|
+
* prefix strip via `options.root` and falls back to `index.html` for unmatched paths when
|
|
13
|
+
* `options.indexOn404` is `true` (useful for single-page applications).
|
|
14
|
+
*
|
|
15
|
+
* @param {string} root - Filesystem path to the directory containing static assets.
|
|
16
|
+
* @param {object} [options] - Options forwarded to `express.static`, plus:
|
|
17
|
+
* @param {string} [options.root] - URL prefix to strip before serving.
|
|
18
|
+
* @param {boolean} [options.indexOn404] - If `true`, respond with `index.html` for all 404s.
|
|
19
|
+
* @returns {import('express').Router}
|
|
20
|
+
*/
|
|
13
21
|
export const ServeStatic = (root, options = {}) => {
|
|
14
22
|
const router = Router();
|
|
15
23
|
const absoluteRoot = path.resolve(root);
|
|
@@ -34,7 +42,6 @@ export const ServeStatic = (root, options = {}) => {
|
|
|
34
42
|
|
|
35
43
|
if (options.indexOn404) {
|
|
36
44
|
router.get("*", (req, res) => {
|
|
37
|
-
logger.info(path.join(absoluteRoot, "index.html"));
|
|
38
45
|
res.sendFile(path.join(absoluteRoot, "index.html"));
|
|
39
46
|
});
|
|
40
47
|
}
|
|
@@ -58,14 +65,15 @@ class WebServer {
|
|
|
58
65
|
this.#started = true;
|
|
59
66
|
|
|
60
67
|
let srv;
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
const localPort = process.env.GX_LOCAL_PORT || process.env.LOCAL_PORT;
|
|
69
|
+
if (localPort) {
|
|
70
|
+
srv = await createServerAtPort(localPort, http, this.#app);
|
|
63
71
|
} else {
|
|
64
72
|
srv = await createServerAtFreePort(http, this.#app);
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
if (!srv) {
|
|
68
|
-
throw
|
|
76
|
+
throw Error("gx.webserver.start: unable to start");
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
this.#server = srv.server;
|
|
@@ -74,7 +82,7 @@ class WebServer {
|
|
|
74
82
|
expressWs(this.#app, srv.server);
|
|
75
83
|
|
|
76
84
|
// stream endpoint
|
|
77
|
-
this.#app.get("/!!
|
|
85
|
+
this.#app.get("/!!_gx/stream/:id", (req, res) => {
|
|
78
86
|
const id = req.params.id;
|
|
79
87
|
|
|
80
88
|
if (activeStreams[id]) {
|
|
@@ -86,8 +94,8 @@ class WebServer {
|
|
|
86
94
|
}
|
|
87
95
|
});
|
|
88
96
|
|
|
89
|
-
this.#app.get(
|
|
90
|
-
res.send({ status: "healthy"
|
|
97
|
+
this.#app.get("/!!_gx/health", (req, res) => {
|
|
98
|
+
res.send({ status: "healthy" });
|
|
91
99
|
});
|
|
92
100
|
|
|
93
101
|
// middleware to handle dynamic routers
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Skill(code-review:code-review)",
|
|
5
|
-
"Bash(node --test --test-reporter=spec tests/integration/service.test.js tests/integration/stream.test.js)",
|
|
6
|
-
"Bash(node --test --test-reporter=spec tests/integration/gateway.test.js)",
|
|
7
|
-
"Bash(timeout 90 node --test --test-reporter=tap tests/integration/gateway.test.js)"
|
|
8
|
-
]
|
|
9
|
-
}
|
|
10
|
-
}
|