@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 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 base = options.baseUrl ?? `http://${request.headers.host ?? "localhost"}`;
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(new URL(request.url ?? "/", base), {
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
- response.statusCode = 500;
105
- response.statusMessage = "Internal Server Error";
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 server = createServer(createNodeHandler(app));
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.5",
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",