@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 +43 -5
- package/dist/index.d.ts +90 -6
- package/dist/index.js +442 -97
- package/dist/mcp.d.ts +26 -0
- package/dist/mcp.js +129 -60
- package/package.json +14 -12
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|