@askrjs/node 0.0.8 → 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
@@ -28,7 +28,21 @@ createServer(createNodeHandler(app, { baseUrl: "http://localhost:3000" })).liste
28
28
 
29
29
  `createNodeHandler` also works as Connect middleware because it accepts an optional `next`
30
30
  callback. It preserves streaming bodies, repeated `Set-Cookie` headers, aborts, backpressure,
31
- status text, and HEAD responses.
31
+ status text, and HEAD responses. `next` receives adapter failures; application responses, including
32
+ `404`, remain owned by the `ServerApp` and do not fall through.
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
+
41
+ Every handler must have a trusted URL boundary. Pass `baseUrl` when the external origin is fixed,
42
+ or `allowedHosts` when the request `Host` determines the origin. Host names are canonicalized and
43
+ compared case-insensitively; entries without a port allow that host on any port, while entries with
44
+ a port require that exact authority. Absolute-form request targets must retain the trusted origin,
45
+ and ambiguous network-path targets are rejected.
32
46
 
33
47
  ## Listen directly
34
48
 
@@ -48,8 +62,12 @@ Pass an `AbortSignal` to integrate shutdown with your process lifecycle.
48
62
 
49
63
  Enable the built-in `ws` transport with `websocket: true`. It defaults to a
50
64
  1 MiB maximum message payload with compression disabled; pass
51
- `websocket: { maxPayload, maxRejectionBodyBytes, perMessageDeflate }` to override
52
- those settings. Rejected upgrade bodies are capped at 64 KiB by default.
65
+ `websocket: { closeTimeout, maxPayload, maxRejectionBodyBytes, perMessageDeflate }` to override
66
+ those settings. Rejected upgrade bodies are capped at 64 KiB by default. Shutdown gives clients five
67
+ seconds to complete the close handshake by default, then terminates any remaining connections.
68
+ Upgrades require an `Origin`. The request origin is allowed by default; set
69
+ `websocket.allowedOrigins` to a canonical allowlist when trusted browser origins differ from the
70
+ application origin.
53
71
 
54
72
  ```ts
55
73
  router.ws("/echo", (socket) => {
@@ -77,8 +95,26 @@ await running.close();
77
95
  ```
78
96
 
79
97
  `serve` handles static assets and closes both the HTTP server and the application during shutdown.
98
+ When an asset root is configured, extension-bearing `GET` and `HEAD` paths are reserved for static
99
+ files: missing files return `404` without falling through to application routing, source maps are not
100
+ served, and resolved files must remain inside the configured root. Fingerprinted files under
101
+ `/assets/` receive immutable caching; other files receive `no-cache`.
80
102
  Both `listen` and `serve` bind to `127.0.0.1` by default. A non-loopback
81
- `host` also requires `allowPublicBind: true` so public exposure is explicit.
103
+ `host` also requires `allowPublicBind: true` so public exposure is explicit. Public listeners should
104
+ declare every external host name through `allowedHosts`; the bind host and `localhost` remain allowed
105
+ automatically:
106
+
107
+ ```ts
108
+ await serve(app, {
109
+ host: "0.0.0.0",
110
+ allowPublicBind: true,
111
+ allowedHosts: ["app.example.com"],
112
+ });
113
+ ```
114
+
115
+ `listen` and `serve` fail before binding when their `AbortSignal` is already aborted. Later aborts
116
+ start shutdown. `serve().close()` is idempotent, waits for HTTP and WebSocket closure, and then closes
117
+ the application exactly once.
82
118
 
83
119
  ## MCP over stdio
84
120
 
@@ -90,4 +126,6 @@ await connection.closed;
90
126
  ```
91
127
 
92
128
  Protocol messages use stdin/stdout; diagnostics remain isolated on stderr. Authentication may be
93
- provided directly or resolved from the process environment for each message.
129
+ provided directly or resolved from the process environment for each message. Closing stdin, calling
130
+ `connection.close()`, or aborting its signal detaches the transport, cancels active requests,
131
+ terminates the MCP session, and prevents late protocol output.
package/dist/index.d.ts CHANGED
@@ -1,55 +1,139 @@
1
+ import { AddressInfo } from "node:net";
1
2
  import { IncomingMessage, Server, ServerResponse } from "node:http";
2
3
  import { PerMessageDeflateOptions } from "ws";
3
4
  import { ServerApp } from "@askrjs/server";
4
- import { AddressInfo } from "node:net";
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. */
18
+ readonly closeTimeout?: number;
19
+ /** Maximum allowed size, in bytes, of a single WebSocket message. */
7
20
  readonly maxPayload?: number;
21
+ /** Maximum number of body bytes read from a rejected upgrade request before the connection is destroyed. */
8
22
  readonly maxRejectionBodyBytes?: number;
23
+ /** Enables or configures the permessage-deflate WebSocket extension. */
9
24
  readonly perMessageDeflate?: boolean | PerMessageDeflateOptions;
25
+ /** Origins allowed to open a WebSocket connection; when omitted, all origins are allowed. */
10
26
  readonly allowedOrigins?: readonly string[];
11
27
  }
28
+ /** Options shared by anything that turns Node HTTP requests into `@askrjs/server` fetch calls. */
12
29
  interface NodeHandlerOptions {
13
- baseUrl?: string;
14
- allowedHosts?: readonly string[];
30
+ /** Base URL used to resolve request paths into absolute URLs. */
31
+ readonly baseUrl?: string;
32
+ /** Hosts allowed in the request's `Host` header; requests for other hosts are rejected. */
33
+ readonly allowedHosts?: readonly string[];
15
34
  }
16
- interface ListenOptions {
35
+ /** Options for {@link listen}, controlling how the Node HTTP server binds and behaves. */
36
+ interface ListenOptions extends NodeHandlerOptions {
37
+ /** Port to listen on; defaults to an ephemeral port when omitted. */
17
38
  port?: number;
39
+ /** Host/address to bind to. */
18
40
  host?: string;
41
+ /** Allows binding to a non-loopback host without the usual safety check. */
19
42
  allowPublicBind?: boolean;
43
+ /** Maximum length of the queue of pending connections. */
20
44
  backlog?: number;
45
+ /** Aborting this signal stops the server. */
21
46
  signal?: AbortSignal;
47
+ /** Node HTTP server `requestTimeout`, in milliseconds. */
22
48
  requestTimeout?: number;
49
+ /** Node HTTP server `headersTimeout`, in milliseconds. */
23
50
  headersTimeout?: number;
51
+ /** Node HTTP server `keepAliveTimeout`, in milliseconds. */
24
52
  keepAliveTimeout?: number;
53
+ /** Enables WebSocket support, optionally with detailed options. */
25
54
  websocket?: boolean | NodeWebSocketOptions;
26
55
  }
56
+ /** Options for {@link serve}, extending {@link ListenOptions} with static asset serving and shutdown behavior. */
27
57
  interface ServeOptions extends ListenOptions {
58
+ /** Serves static files from this directory before falling back to the application. */
28
59
  readonly assets?: {
29
60
  readonly root: string;
30
61
  };
62
+ /** OS signals that trigger a graceful shutdown; pass `false` to disable automatic shutdown handling. */
31
63
  readonly signals?: false | readonly NodeJS.Signals[];
32
64
  }
65
+ /** A running application returned by {@link serve}. */
33
66
  interface ServedApplication {
67
+ /** The underlying Node HTTP server. */
34
68
  readonly server: import("node:http").Server;
69
+ /** The base URL the server is listening on. */
35
70
  readonly url: string;
71
+ /** Gracefully shuts down the server, any WebSocket connections, and the application. */
36
72
  close(): Promise<void>;
37
73
  }
74
+ /** Connect/Express-style `next` callback used to hand off unhandled requests. */
38
75
  type ConnectNext = (error?: unknown) => void;
76
+ /** A Node-style request handler compatible with `http.Server` and Connect-style middleware chains. */
39
77
  type NodeHandler = (request: IncomingMessage, response: ServerResponse, next?: ConnectNext) => void;
40
78
  //#endregion
41
79
  //#region src/handler.d.ts
42
- declare function createNodeHandler(app: ServerApp, options?: NodeHandlerOptions): NodeHandler;
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
+ */
92
+ declare function createNodeHandler(app: ServerApp, options: NodeHandlerOptions): NodeHandler;
43
93
  //#endregion
44
94
  //#region src/listen.d.ts
95
+ /** A Node HTTP server that is guaranteed to be listening for connections. */
45
96
  type ListeningServer = Server & {
46
97
  address(): AddressInfo | string | null;
47
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
+ */
48
112
  declare function listen(app: ServerApp, options?: ListenOptions): Promise<ListeningServer>;
49
113
  //#endregion
50
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
+ */
51
135
  declare function serve(app: ServerApp & {
52
136
  close?: () => void | Promise<void>;
53
137
  }, options?: ServeOptions): Promise<ServedApplication>;
54
138
  //#endregion
55
- 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 };