@askrjs/node 0.0.5 → 0.0.6
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/README.md +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +33 -9
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +31 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ const app = createServerApp({
|
|
|
23
23
|
routes: [{ path: "/health", handler: () => json({ status: "ok" }) }],
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
createServer(createNodeHandler(app)).listen(3000);
|
|
26
|
+
createServer(createNodeHandler(app, { baseUrl: "http://localhost:3000" })).listen(3000);
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
`createNodeHandler` also works as Connect middleware because it accepts an optional `next`
|
package/dist/index.d.ts
CHANGED
|
@@ -6,9 +6,11 @@ import { AddressInfo } from "node:net";
|
|
|
6
6
|
interface NodeWebSocketOptions {
|
|
7
7
|
readonly maxPayload?: number;
|
|
8
8
|
readonly perMessageDeflate?: boolean | PerMessageDeflateOptions;
|
|
9
|
+
readonly allowedOrigins?: readonly string[];
|
|
9
10
|
}
|
|
10
11
|
interface NodeHandlerOptions {
|
|
11
12
|
baseUrl?: string;
|
|
13
|
+
allowedHosts?: readonly string[];
|
|
12
14
|
}
|
|
13
15
|
interface ListenOptions {
|
|
14
16
|
port?: number;
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createReadStream } from "node:fs";
|
|
|
5
5
|
import { stat } from "node:fs/promises";
|
|
6
6
|
import { extname, resolve, sep } from "node:path";
|
|
7
7
|
//#region src/request.ts
|
|
8
|
+
var NodeRequestError = class extends TypeError {};
|
|
8
9
|
function requestHeaders(request) {
|
|
9
10
|
const headers = new Headers();
|
|
10
11
|
for (const [key, value] of Object.entries(request.headers)) {
|
|
@@ -14,10 +15,17 @@ function requestHeaders(request) {
|
|
|
14
15
|
return headers;
|
|
15
16
|
}
|
|
16
17
|
function requestFromNode(request, options, signal) {
|
|
17
|
-
const
|
|
18
|
+
const host = request.headers.host;
|
|
19
|
+
if (!options.baseUrl && !options.allowedHosts?.length) throw new NodeRequestError("Node handling requires baseUrl or an allowedHosts allowlist.");
|
|
20
|
+
if (!host) throw new NodeRequestError("Request Host header is required.");
|
|
21
|
+
const hostName = host.startsWith("[") ? host.slice(1, host.indexOf("]")) : host.split(":", 1)[0];
|
|
22
|
+
if (options.allowedHosts?.length && !options.allowedHosts.some((allowed) => allowed === host || allowed === hostName)) throw new NodeRequestError("Request Host header is not allowed.");
|
|
23
|
+
const base = options.baseUrl ?? `http://${host}`;
|
|
24
|
+
const target = new URL(request.url ?? "/", base);
|
|
25
|
+
if (/^https?:\/\//i.test(request.url ?? "") && target.origin !== new URL(base).origin) throw new NodeRequestError("Absolute-form request target origin is not allowed.");
|
|
18
26
|
const method = request.method ?? "GET";
|
|
19
27
|
const body = method === "GET" || method === "HEAD" ? void 0 : Readable.toWeb(request);
|
|
20
|
-
return new Request(
|
|
28
|
+
return new Request(target, {
|
|
21
29
|
method,
|
|
22
30
|
headers: requestHeaders(request),
|
|
23
31
|
body,
|
|
@@ -101,10 +109,11 @@ function minimalError(response, error) {
|
|
|
101
109
|
response.destroy(error instanceof Error ? error : void 0);
|
|
102
110
|
return;
|
|
103
111
|
}
|
|
104
|
-
|
|
105
|
-
response.
|
|
112
|
+
const clientError = error instanceof NodeRequestError;
|
|
113
|
+
response.statusCode = clientError ? 400 : 500;
|
|
114
|
+
response.statusMessage = clientError ? "Bad Request" : "Internal Server Error";
|
|
106
115
|
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
107
|
-
response.end("Internal Server Error");
|
|
116
|
+
response.end(clientError ? "Bad Request" : "Internal Server Error");
|
|
108
117
|
}
|
|
109
118
|
function attachAbort(request, response, controller) {
|
|
110
119
|
const abort = () => controller.abort();
|
|
@@ -172,6 +181,19 @@ function installWebSockets(server, app, options = {}, handlerOptions = {}) {
|
|
|
172
181
|
const controller = new AbortController();
|
|
173
182
|
let accepted;
|
|
174
183
|
Promise.resolve().then(() => requestFromNode(request, handlerOptions, controller.signal)).then(async (webRequest) => {
|
|
184
|
+
const originHeader = request.headers.origin;
|
|
185
|
+
const origin = Array.isArray(originHeader) ? void 0 : originHeader;
|
|
186
|
+
const allowedOrigins = options.allowedOrigins ?? [new URL(webRequest.url).origin];
|
|
187
|
+
let normalizedOrigin;
|
|
188
|
+
try {
|
|
189
|
+
normalizedOrigin = origin ? new URL(origin).origin : void 0;
|
|
190
|
+
} catch {
|
|
191
|
+
normalizedOrigin = void 0;
|
|
192
|
+
}
|
|
193
|
+
if (!normalizedOrigin || !allowedOrigins.includes(normalizedOrigin)) {
|
|
194
|
+
await rejectUpgrade(socket, new Response("Forbidden", { status: 403 }));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
175
197
|
let marker;
|
|
176
198
|
const response = await app.fetch(webRequest, { websocket: { upgrade: (_request, handler, context) => {
|
|
177
199
|
marker = new Response(null, { status: 200 });
|
|
@@ -200,11 +222,12 @@ function installWebSockets(server, app, options = {}, handlerOptions = {}) {
|
|
|
200
222
|
//#endregion
|
|
201
223
|
//#region src/listen.ts
|
|
202
224
|
function listen(app, options = {}) {
|
|
203
|
-
const
|
|
225
|
+
const handlerOptions = { allowedHosts: [options.host ?? "127.0.0.1", "localhost"] };
|
|
226
|
+
const server = createServer(createNodeHandler(app, handlerOptions));
|
|
204
227
|
if (options.requestTimeout !== void 0) server.requestTimeout = options.requestTimeout;
|
|
205
228
|
if (options.headersTimeout !== void 0) server.headersTimeout = options.headersTimeout;
|
|
206
229
|
if (options.keepAliveTimeout !== void 0) server.keepAliveTimeout = options.keepAliveTimeout;
|
|
207
|
-
const webSockets = options.websocket ? installWebSockets(server, app, options.websocket === true ? {} : options.websocket) : void 0;
|
|
230
|
+
const webSockets = options.websocket ? installWebSockets(server, app, options.websocket === true ? {} : options.websocket, handlerOptions) : void 0;
|
|
208
231
|
if (webSockets) {
|
|
209
232
|
const nativeClose = server.close.bind(server);
|
|
210
233
|
server.close = ((callback) => {
|
|
@@ -249,6 +272,7 @@ function isAssetPath(pathname) {
|
|
|
249
272
|
}
|
|
250
273
|
async function serve(app, options = {}) {
|
|
251
274
|
const root = options.assets ? resolve(options.assets.root) : void 0;
|
|
275
|
+
const handlerOptions = { allowedHosts: [options.host ?? "127.0.0.1", "localhost"] };
|
|
252
276
|
const applicationHandler = createNodeHandler({ async fetch(request, dispatchOptions) {
|
|
253
277
|
const result = await app.fetch(request, dispatchOptions);
|
|
254
278
|
if (!result.headers.has("cache-control") && result.headers.get("content-type")?.includes("text/html")) {
|
|
@@ -261,7 +285,7 @@ async function serve(app, options = {}) {
|
|
|
261
285
|
});
|
|
262
286
|
}
|
|
263
287
|
return result;
|
|
264
|
-
} });
|
|
288
|
+
} }, handlerOptions);
|
|
265
289
|
const server = createServer(async (request, response) => {
|
|
266
290
|
let pathname;
|
|
267
291
|
try {
|
|
@@ -303,7 +327,7 @@ async function serve(app, options = {}) {
|
|
|
303
327
|
if (options.requestTimeout !== void 0) server.requestTimeout = options.requestTimeout;
|
|
304
328
|
if (options.headersTimeout !== void 0) server.headersTimeout = options.headersTimeout;
|
|
305
329
|
if (options.keepAliveTimeout !== void 0) server.keepAliveTimeout = options.keepAliveTimeout;
|
|
306
|
-
const webSockets = options.websocket ? installWebSockets(server, app, options.websocket === true ? {} : options.websocket) : void 0;
|
|
330
|
+
const webSockets = options.websocket ? installWebSockets(server, app, options.websocket === true ? {} : options.websocket, handlerOptions) : void 0;
|
|
307
331
|
let closing;
|
|
308
332
|
const signals = options.signals === false ? [] : options.signals ?? ["SIGINT", "SIGTERM"];
|
|
309
333
|
const removeListeners = () => {
|
package/dist/mcp.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ interface McpStdioOptions<Dependencies = undefined> {
|
|
|
10
10
|
signal?: AbortSignal;
|
|
11
11
|
auth?: AuthContext | ((environment: NodeJS.ProcessEnv) => AuthContext | Promise<AuthContext>);
|
|
12
12
|
environment?: NodeJS.ProcessEnv;
|
|
13
|
+
maxLineBytes?: number;
|
|
14
|
+
maxConcurrency?: number;
|
|
13
15
|
}
|
|
14
16
|
interface McpStdioConnection {
|
|
15
17
|
readonly closed: Promise<void>;
|
package/dist/mcp.js
CHANGED
|
@@ -22,6 +22,11 @@ function connectMcpStdio(mcp, options) {
|
|
|
22
22
|
finish = resolve;
|
|
23
23
|
});
|
|
24
24
|
let ended = false;
|
|
25
|
+
const maxLineBytes = options.maxLineBytes ?? 1048576;
|
|
26
|
+
const maxConcurrency = options.maxConcurrency ?? 16;
|
|
27
|
+
if (!Number.isInteger(maxLineBytes) || maxLineBytes <= 0) throw new TypeError("MCP maxLineBytes must be a positive integer.");
|
|
28
|
+
if (!Number.isInteger(maxConcurrency) || maxConcurrency <= 0) throw new TypeError("MCP maxConcurrency must be a positive integer.");
|
|
29
|
+
let active = 0;
|
|
25
30
|
const write = (message) => new Promise((resolve, reject) => {
|
|
26
31
|
output.write(`${JSON.stringify(message)}\n`, (error) => error ? reject(error) : resolve());
|
|
27
32
|
});
|
|
@@ -37,6 +42,29 @@ function connectMcpStdio(mcp, options) {
|
|
|
37
42
|
};
|
|
38
43
|
options.signal?.addEventListener("abort", () => void close(), { once: true });
|
|
39
44
|
lines.on("line", (line) => {
|
|
45
|
+
if (Buffer.byteLength(line) > maxLineBytes) {
|
|
46
|
+
write({
|
|
47
|
+
jsonrpc: "2.0",
|
|
48
|
+
id: null,
|
|
49
|
+
error: {
|
|
50
|
+
code: -32600,
|
|
51
|
+
message: "Request line exceeds the configured limit"
|
|
52
|
+
}
|
|
53
|
+
}).catch(() => void close());
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (active >= maxConcurrency) {
|
|
57
|
+
write({
|
|
58
|
+
jsonrpc: "2.0",
|
|
59
|
+
id: null,
|
|
60
|
+
error: {
|
|
61
|
+
code: -32600,
|
|
62
|
+
message: "Too many concurrent requests"
|
|
63
|
+
}
|
|
64
|
+
}).catch(() => void close());
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
active += 1;
|
|
40
68
|
(async () => {
|
|
41
69
|
let message;
|
|
42
70
|
try {
|
|
@@ -78,7 +106,9 @@ function connectMcpStdio(mcp, options) {
|
|
|
78
106
|
if (controller && id !== void 0) controllers.delete(id);
|
|
79
107
|
}
|
|
80
108
|
}
|
|
81
|
-
})().catch((error) => diagnostics.write(`MCP stdio error: ${error instanceof Error ? error.message : String(error)}\n`))
|
|
109
|
+
})().catch((error) => diagnostics.write(`MCP stdio error: ${error instanceof Error ? error.message : String(error)}\n`)).finally(() => {
|
|
110
|
+
active -= 1;
|
|
111
|
+
});
|
|
82
112
|
});
|
|
83
113
|
lines.once("close", () => {
|
|
84
114
|
if (!ended) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askrjs/node",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Node http adapter for @askrjs/server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"askr",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"ws": "^8.21.1"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
+
"@askrjs/schema": ">=0.0.2 <0.1.0",
|
|
63
64
|
"@types/node": "^26.0.0",
|
|
64
65
|
"@types/ws": "^8.18.1",
|
|
65
66
|
"publint": "^0.3.21",
|