@askrjs/node 0.0.9 → 0.0.11

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
@@ -31,6 +31,13 @@ callback. It preserves streaming bodies, repeated `Set-Cookie` headers, aborts,
31
31
  status text, and HEAD responses. `next` receives adapter failures; application responses, including
32
32
  `404`, remain owned by the `ServerApp` and do not fall through.
33
33
 
34
+ Every dispatched request includes `CLIENT_ADDRESS_HEADER` (`x-askr-client-address`) set from the
35
+ TCP socket peer. The adapter overwrites a client-supplied value and does not interpret
36
+ `X-Forwarded-For`, so applications can use this header for direct-listener IP controls without
37
+ trusting attacker-controlled forwarding metadata. Deployments behind a reverse proxy see the
38
+ proxy peer by default. Supporting original client addresses requires an explicit trusted-proxy
39
+ boundary; do not read `X-Forwarded-For` directly in application code.
40
+
34
41
  Every handler must have a trusted URL boundary. Pass `baseUrl` when the external origin is fixed,
35
42
  or `allowedHosts` when the request `Host` determines the origin. Host names are canonicalized and
36
43
  compared case-insensitively; entries without a port allow that host on any port, while entries with
package/dist/index.d.ts CHANGED
@@ -2,55 +2,138 @@ import { AddressInfo } from "node:net";
2
2
  import { IncomingMessage, Server, ServerResponse } from "node:http";
3
3
  import { PerMessageDeflateOptions } from "ws";
4
4
  import { ServerApp } from "@askrjs/server";
5
+ //#region src/client-address.d.ts
6
+ /**
7
+ * Reserved request header containing the TCP peer address authenticated by the Node adapter.
8
+ * Any value supplied by the HTTP client is overwritten before application dispatch.
9
+ */
10
+ declare const CLIENT_ADDRESS_HEADER = "x-askr-client-address";
11
+ /** Normalizes the socket peer address used for the adapter-authenticated request header. */
12
+ declare function normalizeClientAddress(address: string | undefined): string;
13
+ //#endregion
5
14
  //#region src/contracts.d.ts
15
+ /** Options controlling how WebSocket upgrades are handled on a Node server. */
6
16
  interface NodeWebSocketOptions {
17
+ /** Milliseconds to wait for a peer to acknowledge a close handshake before the socket is force-closed. */
7
18
  readonly closeTimeout?: number;
19
+ /** Maximum allowed size, in bytes, of a single WebSocket message. */
8
20
  readonly maxPayload?: number;
21
+ /** Maximum number of body bytes read from a rejected upgrade request before the connection is destroyed. */
9
22
  readonly maxRejectionBodyBytes?: number;
23
+ /** Enables or configures the permessage-deflate WebSocket extension. */
10
24
  readonly perMessageDeflate?: boolean | PerMessageDeflateOptions;
25
+ /** Origins allowed to open a WebSocket connection; when omitted, all origins are allowed. */
11
26
  readonly allowedOrigins?: readonly string[];
12
27
  }
28
+ /** Options shared by anything that turns Node HTTP requests into `@askrjs/server` fetch calls. */
13
29
  interface NodeHandlerOptions {
30
+ /** Base URL used to resolve request paths into absolute URLs. */
14
31
  readonly baseUrl?: string;
32
+ /** Hosts allowed in the request's `Host` header; requests for other hosts are rejected. */
15
33
  readonly allowedHosts?: readonly string[];
16
34
  }
35
+ /** Options for {@link listen}, controlling how the Node HTTP server binds and behaves. */
17
36
  interface ListenOptions extends NodeHandlerOptions {
37
+ /** Port to listen on; defaults to an ephemeral port when omitted. */
18
38
  port?: number;
39
+ /** Host/address to bind to. */
19
40
  host?: string;
41
+ /** Allows binding to a non-loopback host without the usual safety check. */
20
42
  allowPublicBind?: boolean;
43
+ /** Maximum length of the queue of pending connections. */
21
44
  backlog?: number;
45
+ /** Aborting this signal stops the server. */
22
46
  signal?: AbortSignal;
47
+ /** Node HTTP server `requestTimeout`, in milliseconds. */
23
48
  requestTimeout?: number;
49
+ /** Node HTTP server `headersTimeout`, in milliseconds. */
24
50
  headersTimeout?: number;
51
+ /** Node HTTP server `keepAliveTimeout`, in milliseconds. */
25
52
  keepAliveTimeout?: number;
53
+ /** Enables WebSocket support, optionally with detailed options. */
26
54
  websocket?: boolean | NodeWebSocketOptions;
27
55
  }
56
+ /** Options for {@link serve}, extending {@link ListenOptions} with static asset serving and shutdown behavior. */
28
57
  interface ServeOptions extends ListenOptions {
58
+ /** Serves static files from this directory before falling back to the application. */
29
59
  readonly assets?: {
30
60
  readonly root: string;
31
61
  };
62
+ /** OS signals that trigger a graceful shutdown; pass `false` to disable automatic shutdown handling. */
32
63
  readonly signals?: false | readonly NodeJS.Signals[];
33
64
  }
65
+ /** A running application returned by {@link serve}. */
34
66
  interface ServedApplication {
67
+ /** The underlying Node HTTP server. */
35
68
  readonly server: import("node:http").Server;
69
+ /** The base URL the server is listening on. */
36
70
  readonly url: string;
71
+ /** Gracefully shuts down the server, any WebSocket connections, and the application. */
37
72
  close(): Promise<void>;
38
73
  }
74
+ /** Connect/Express-style `next` callback used to hand off unhandled requests. */
39
75
  type ConnectNext = (error?: unknown) => void;
76
+ /** A Node-style request handler compatible with `http.Server` and Connect-style middleware chains. */
40
77
  type NodeHandler = (request: IncomingMessage, response: ServerResponse, next?: ConnectNext) => void;
41
78
  //#endregion
42
79
  //#region src/handler.d.ts
80
+ /**
81
+ * Wraps an `@askrjs/server` application as a Node-style request handler.
82
+ *
83
+ * Converts each incoming `IncomingMessage`/`ServerResponse` pair into a web
84
+ * `Request`, dispatches it through `app.fetch`, and writes the resulting web
85
+ * `Response` back to Node. Errors are reported to `next` when provided,
86
+ * otherwise a minimal 400/500 response is written directly.
87
+ *
88
+ * @param app - The application to dispatch requests to.
89
+ * @param options - Options controlling base URL resolution and host validation.
90
+ * @returns A handler usable with `http.createServer` or Connect-style middleware.
91
+ */
43
92
  declare function createNodeHandler(app: ServerApp, options: NodeHandlerOptions): NodeHandler;
44
93
  //#endregion
45
94
  //#region src/listen.d.ts
95
+ /** A Node HTTP server that is guaranteed to be listening for connections. */
46
96
  type ListeningServer = Server & {
47
97
  address(): AddressInfo | string | null;
48
98
  };
99
+ /**
100
+ * Starts a Node HTTP server for an `@askrjs/server` application and resolves once it is listening.
101
+ *
102
+ * Optionally installs WebSocket support and wires up graceful shutdown on
103
+ * `options.signal`. Unlike {@link serve}, this does not serve static assets
104
+ * or install OS signal handlers.
105
+ *
106
+ * @param app - The application to serve.
107
+ * @param options - Listen options such as port, host, timeouts, and WebSocket support.
108
+ * @returns A promise resolving to the listening server once it has bound successfully.
109
+ * @example
110
+ * const server = await listen(app, { port: 3000 });
111
+ */
49
112
  declare function listen(app: ServerApp, options?: ListenOptions): Promise<ListeningServer>;
50
113
  //#endregion
51
114
  //#region src/serve.d.ts
115
+ /**
116
+ * Serves an `@askrjs/server` application over Node HTTP, with optional static
117
+ * asset serving, WebSocket support, and graceful shutdown on OS signals or an
118
+ * abort signal.
119
+ *
120
+ * Requests for paths with a file extension are first checked against
121
+ * `options.assets.root` (path-traversal safe, following symlinks) and served
122
+ * directly with appropriate `content-type`/`cache-control` headers before
123
+ * falling back to the application handler. HTML responses from the
124
+ * application get a `no-cache` header when they don't already set
125
+ * `cache-control`.
126
+ *
127
+ * @param app - The application to serve; may expose an optional `close()` for cleanup.
128
+ * @param options - Serve options such as port, host, static assets, and shutdown signals.
129
+ * @returns The served application, including its bound `url` and a `close()` for shutdown.
130
+ * @example
131
+ * const app = await serve(myApp, { port: 3000, assets: { root: "./public" } });
132
+ * // ...
133
+ * await app.close();
134
+ */
52
135
  declare function serve(app: ServerApp & {
53
136
  close?: () => void | Promise<void>;
54
137
  }, options?: ServeOptions): Promise<ServedApplication>;
55
138
  //#endregion
56
- export { ConnectNext, ListenOptions, ListeningServer, NodeHandler, NodeHandlerOptions, NodeWebSocketOptions, ServeOptions, ServedApplication, createNodeHandler, listen, serve };
139
+ export { CLIENT_ADDRESS_HEADER, ConnectNext, ListenOptions, ListeningServer, NodeHandler, NodeHandlerOptions, NodeWebSocketOptions, ServeOptions, ServedApplication, createNodeHandler, listen, normalizeClientAddress, serve };
package/dist/index.js CHANGED
@@ -6,6 +6,22 @@ import { createReadStream } from "node:fs";
6
6
  import { realpath, stat } from "node:fs/promises";
7
7
  import { extname, resolve, sep } from "node:path";
8
8
  import { pipeline } from "node:stream";
9
+ //#region src/client-address.ts
10
+ /**
11
+ * Reserved request header containing the TCP peer address authenticated by the Node adapter.
12
+ * Any value supplied by the HTTP client is overwritten before application dispatch.
13
+ */
14
+ const CLIENT_ADDRESS_HEADER = "x-askr-client-address";
15
+ /** Normalizes the socket peer address used for the adapter-authenticated request header. */
16
+ function normalizeClientAddress(address) {
17
+ if (!address) return "unknown";
18
+ if (address.toLowerCase().startsWith("::ffff:")) {
19
+ const mapped = address.slice(7);
20
+ if (isIP(mapped) === 4) return mapped;
21
+ }
22
+ return address;
23
+ }
24
+ //#endregion
9
25
  //#region src/request.ts
10
26
  var NodeRequestError = class extends TypeError {};
11
27
  function explicitPort(authority) {
@@ -69,6 +85,7 @@ function requestHeaders(request) {
69
85
  if (Array.isArray(value)) for (const item of value) headers.append(key, item);
70
86
  else headers.set(key, value);
71
87
  }
88
+ headers.set(CLIENT_ADDRESS_HEADER, normalizeClientAddress(request.socket.remoteAddress));
72
89
  return headers;
73
90
  }
74
91
  function resolveNodeRequestHref(request, options) {
@@ -255,6 +272,18 @@ async function handleNodeRequest(app, preparedOptions, request, response, signal
255
272
  cleanup();
256
273
  }
257
274
  }
275
+ /**
276
+ * Wraps an `@askrjs/server` application as a Node-style request handler.
277
+ *
278
+ * Converts each incoming `IncomingMessage`/`ServerResponse` pair into a web
279
+ * `Request`, dispatches it through `app.fetch`, and writes the resulting web
280
+ * `Response` back to Node. Errors are reported to `next` when provided,
281
+ * otherwise a minimal 400/500 response is written directly.
282
+ *
283
+ * @param app - The application to dispatch requests to.
284
+ * @param options - Options controlling base URL resolution and host validation.
285
+ * @returns A handler usable with `http.createServer` or Connect-style middleware.
286
+ */
258
287
  function createNodeHandler(app, options) {
259
288
  const preparedOptions = prepareNodeHandlerOptions(options);
260
289
  return (request, response, next) => {
@@ -499,6 +528,19 @@ function installWebSockets(server, app, options, handlerOptions) {
499
528
  }
500
529
  //#endregion
501
530
  //#region src/listen.ts
531
+ /**
532
+ * Starts a Node HTTP server for an `@askrjs/server` application and resolves once it is listening.
533
+ *
534
+ * Optionally installs WebSocket support and wires up graceful shutdown on
535
+ * `options.signal`. Unlike {@link serve}, this does not serve static assets
536
+ * or install OS signal handlers.
537
+ *
538
+ * @param app - The application to serve.
539
+ * @param options - Listen options such as port, host, timeouts, and WebSocket support.
540
+ * @returns A promise resolving to the listening server once it has bound successfully.
541
+ * @example
542
+ * const server = await listen(app, { port: 3000 });
543
+ */
502
544
  function listen(app, options = {}) {
503
545
  options.signal?.throwIfAborted();
504
546
  const host = resolveBindHost(options);
@@ -576,6 +618,26 @@ function isWithinRoot(root, candidate) {
576
618
  const prefix = root.endsWith(sep) ? root : `${root}${sep}`;
577
619
  return candidate === root || candidate.startsWith(prefix);
578
620
  }
621
+ /**
622
+ * Serves an `@askrjs/server` application over Node HTTP, with optional static
623
+ * asset serving, WebSocket support, and graceful shutdown on OS signals or an
624
+ * abort signal.
625
+ *
626
+ * Requests for paths with a file extension are first checked against
627
+ * `options.assets.root` (path-traversal safe, following symlinks) and served
628
+ * directly with appropriate `content-type`/`cache-control` headers before
629
+ * falling back to the application handler. HTML responses from the
630
+ * application get a `no-cache` header when they don't already set
631
+ * `cache-control`.
632
+ *
633
+ * @param app - The application to serve; may expose an optional `close()` for cleanup.
634
+ * @param options - Serve options such as port, host, static assets, and shutdown signals.
635
+ * @returns The served application, including its bound `url` and a `close()` for shutdown.
636
+ * @example
637
+ * const app = await serve(myApp, { port: 3000, assets: { root: "./public" } });
638
+ * // ...
639
+ * await app.close();
640
+ */
579
641
  async function serve(app, options = {}) {
580
642
  options.signal?.throwIfAborted();
581
643
  const host = resolveBindHost(options);
@@ -711,4 +773,4 @@ async function serve(app, options = {}) {
711
773
  });
712
774
  }
713
775
  //#endregion
714
- export { createNodeHandler, listen, serve };
776
+ export { CLIENT_ADDRESS_HEADER, createNodeHandler, listen, normalizeClientAddress, serve };
package/dist/mcp.d.ts CHANGED
@@ -2,21 +2,47 @@ import { Readable, Writable } from "node:stream";
2
2
  import { AuthContext } from "@askrjs/auth";
3
3
  import { McpServer } from "@askrjs/server/mcp";
4
4
  //#region src/mcp.d.ts
5
+ /** Options for {@link connectMcpStdio}. */
5
6
  interface McpStdioOptions<Dependencies = undefined> {
7
+ /** Application dependencies passed through to each MCP request. */
6
8
  dependencies: Dependencies;
9
+ /** Stream to read newline-delimited JSON-RPC requests from; defaults to `process.stdin`. */
7
10
  input?: Readable;
11
+ /** Stream to write newline-delimited JSON-RPC responses to; defaults to `process.stdout`. */
8
12
  output?: Writable;
13
+ /** Stream that non-protocol errors are reported to; defaults to `process.stderr`. */
9
14
  diagnostics?: Writable;
15
+ /** Aborting this signal closes the connection. */
10
16
  signal?: AbortSignal;
17
+ /** Auth context to use for requests, or a function that derives one from the environment. */
11
18
  auth?: AuthContext | ((environment: NodeJS.ProcessEnv) => AuthContext | Promise<AuthContext>);
19
+ /** Environment passed to the `auth` function; defaults to `process.env`. */
12
20
  environment?: NodeJS.ProcessEnv;
21
+ /** Maximum size, in bytes, of a single input line before it is rejected; defaults to 1 MiB. */
13
22
  maxLineBytes?: number;
23
+ /** Maximum number of requests handled concurrently; defaults to 16. */
14
24
  maxConcurrency?: number;
15
25
  }
26
+ /** A live MCP stdio connection returned by {@link connectMcpStdio}. */
16
27
  interface McpStdioConnection {
28
+ /** Resolves once the connection has fully closed. */
17
29
  readonly closed: Promise<void>;
30
+ /** Closes the connection, aborting in-flight requests and terminating the MCP session. */
18
31
  close(): Promise<void>;
19
32
  }
33
+ /**
34
+ * Connects an MCP server to newline-delimited JSON-RPC over stdio (or any
35
+ * pair of readable/writable streams).
36
+ *
37
+ * Reads one JSON-RPC message per line, dispatches it to `mcp.handle`, and
38
+ * writes the response back as a line of JSON. Handles request cancellation
39
+ * notifications, enforces `maxConcurrency` and `maxLineBytes`, and cleans up
40
+ * the MCP session when the connection closes.
41
+ *
42
+ * @param mcp - The MCP server to dispatch requests to.
43
+ * @param options - Stdio connection options, including dependencies and stream overrides.
44
+ * @returns A handle exposing `closed` and `close()` for the connection's lifecycle.
45
+ */
20
46
  declare function connectMcpStdio<Dependencies>(mcp: McpServer<Dependencies>, options: McpStdioOptions<Dependencies>): McpStdioConnection;
21
47
  //#endregion
22
48
  export { McpStdioConnection, McpStdioOptions, connectMcpStdio };
package/dist/mcp.js CHANGED
@@ -7,6 +7,19 @@ const anonymous = Object.freeze({
7
7
  session: null,
8
8
  tenant: null
9
9
  });
10
+ /**
11
+ * Connects an MCP server to newline-delimited JSON-RPC over stdio (or any
12
+ * pair of readable/writable streams).
13
+ *
14
+ * Reads one JSON-RPC message per line, dispatches it to `mcp.handle`, and
15
+ * writes the response back as a line of JSON. Handles request cancellation
16
+ * notifications, enforces `maxConcurrency` and `maxLineBytes`, and cleans up
17
+ * the MCP session when the connection closes.
18
+ *
19
+ * @param mcp - The MCP server to dispatch requests to.
20
+ * @param options - Stdio connection options, including dependencies and stream overrides.
21
+ * @returns A handle exposing `closed` and `close()` for the connection's lifecycle.
22
+ */
10
23
  function connectMcpStdio(mcp, options) {
11
24
  const input = options.input ?? process.stdin;
12
25
  const output = options.output ?? process.stdout;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askrjs/node",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "Node http adapter for @askrjs/server",
5
5
  "keywords": [
6
6
  "askr",