@procedures/client 0.27.3 → 0.27.4

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.
@@ -396,8 +396,9 @@ export function createProcedureHandler(server: RequestHandler, options: Procedur
396
396
  method,
397
397
  payload,
398
398
  metadata,
399
- // A network request: ProcedureServer examines the token and the request hook (R18, CLI-2a)
400
- credentials: requestCredentials({ headers: req.headers as Record<string, unknown>, url: req.url }),
399
+ // A network request: ProcedureServer examines the token and the request hook (R18, CLI-2a).
400
+ // The credentials have the connection of the socket of the request (CLI-71)
401
+ credentials: requestCredentials({ headers: req.headers as Record<string, unknown>, url: req.url, socket: req.socket }),
401
402
  };
402
403
  };
403
404
 
@@ -387,8 +387,9 @@ export class WebSocketServerTransport implements ServerTransport {
387
387
  };
388
388
  this.trackedConnections.set(connectionId, trackedConnection);
389
389
  // The credentials of the upgrade, for each call of the connection: the bearer token of its
390
- // Authorization header or of its token query, its cookies, its headers and its query (CLI-2a)
391
- const credentials = requestCredentials({ headers: req.headers as Record<string, unknown>, url: req.url }, { tokenQuery: "token" });
390
+ // Authorization header or of its token query, its cookies, its headers and its query (CLI-2a),
391
+ // and the connection of its socket (CLI-71)
392
+ const credentials = requestCredentials({ headers: req.headers as Record<string, unknown>, url: req.url, socket: req.socket }, { tokenQuery: "token" });
392
393
 
393
394
  // The calls of this connection in the two directions (roadmap R24)
394
395
  const isOpen = (): boolean => ws.readyState === WebSocket.OPEN;
package/src/browser.ts CHANGED
@@ -99,6 +99,7 @@ export type {
99
99
  ServerResponse,
100
100
  RequestHandler,
101
101
  Credentials,
102
+ RequestConnection,
102
103
  Principal,
103
104
  PrincipalOf,
104
105
  RequestIdentity,
@@ -51,6 +51,7 @@ export {
51
51
  type ServerResponse,
52
52
  type RequestHandler,
53
53
  type Credentials,
54
+ type RequestConnection,
54
55
  type Principal,
55
56
  type PrincipalOf,
56
57
  type RequestIdentity,
@@ -162,12 +162,12 @@ export interface ProcedureServerOptions extends ServerOptions {
162
162
 
163
163
  /**
164
164
  * The request hook of the server: it reads the credentials of each network request (a cookie, a
165
- * header, the query) and gives a bearer token, a principal and a narrowing. It runs before the
166
- * token check: its bearer token replaces the bearer token of the credentials. Its principal
167
- * replaces the principal of the token check. `authenticate` runs after it and can read
168
- * `request.principal`. Undefined or an error refuses the request with `UNAUTHORIZED`. A local
169
- * request (`LOCAL_CREDENTIALS`) does not go through the hook. A server with a hook refuses a
170
- * request without credentials, and the hook does not run for it (CLI-1).
165
+ * header, the query, the connection of the socket) and gives a bearer token, a principal and a
166
+ * narrowing. It runs before the token check: its bearer token replaces the bearer token of the
167
+ * credentials. Its principal replaces the principal of the token check. `authenticate` runs
168
+ * after it and can read `request.principal`. Undefined or an error refuses the request with
169
+ * `UNAUTHORIZED`. A local request (`LOCAL_CREDENTIALS`) does not go through the hook. A server
170
+ * with a hook refuses a request without credentials, and the hook does not run for it (CLI-1).
171
171
  *
172
172
  * The hook can run more than one time for one request or one task: `admits()` and `identify()`
173
173
  * run it too (CLI-69).
@@ -6,7 +6,7 @@
6
6
  * This module imports no Node code: a server in a browser or a worker can use it too.
7
7
  */
8
8
 
9
- import { LOCAL_MARKER, type Credentials } from "./types.js";
9
+ import { LOCAL_MARKER, type Credentials, type RequestConnection } from "./types.js";
10
10
 
11
11
  /**
12
12
  * The credentials of a local face (CLI-1): a call from this application, not from the network.
@@ -166,13 +166,36 @@ export interface RequestCredentialsOptions {
166
166
  readonly tokenQuery?: string | false | undefined;
167
167
  }
168
168
 
169
+ /** True for a port number of a socket: an integer from 0 to 65535. */
170
+ function isPort(value: unknown): value is number {
171
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 65535;
172
+ }
173
+
174
+ /**
175
+ * The connection of a network request (CLI-71): a frozen copy of the addresses and the ports of
176
+ * its socket. The copy has only the fields that the socket has. A Unix socket or a named pipe
177
+ * has no ports. No header goes into the copy.
178
+ */
179
+ function connectionOf(socket: RequestConnection): RequestConnection {
180
+ const connection: { remoteAddress?: string; remotePort?: number; localAddress?: string; localPort?: number } = {};
181
+ const { remoteAddress, remotePort, localAddress, localPort } = socket;
182
+ if (typeof remoteAddress === "string" && remoteAddress !== "") connection.remoteAddress = remoteAddress;
183
+ if (isPort(remotePort)) connection.remotePort = remotePort;
184
+ if (typeof localAddress === "string" && localAddress !== "") connection.localAddress = localAddress;
185
+ if (isPort(localPort)) connection.localPort = localPort;
186
+ return Object.freeze(connection);
187
+ }
188
+
169
189
  /**
170
190
  * The credentials of a network request (roadmap R18): the bearer token, the cookies, each header
171
191
  * (the secret ones too) and the first value of each query parameter. The request hook of a server
172
192
  * (`principalOf`) reads them. They are not in the metadata, so a handler does not see them.
193
+ *
194
+ * With `request.socket` (the socket of a request of `node:http`), the credentials also have the
195
+ * connection of the request (CLI-71): the addresses and the ports of that socket, as frozen data.
173
196
  */
174
197
  export function requestCredentials(
175
- request: { headers: Record<string, unknown>; url?: string | undefined },
198
+ request: { headers: Record<string, unknown>; url?: string | undefined; socket?: RequestConnection | undefined },
176
199
  options: RequestCredentialsOptions = {}
177
200
  ): Credentials {
178
201
  const headers = new Map<string, string>();
@@ -189,5 +212,6 @@ export function requestCredentials(
189
212
  cookies: cookiesOf(headers.get("cookie")),
190
213
  headers: Object.fromEntries(headers),
191
214
  query: queryRecord(query),
215
+ ...(request.socket ? { connection: connectionOf(request.socket) } : {}),
192
216
  };
193
217
  }
@@ -97,6 +97,42 @@ export interface Credentials {
97
97
  readonly headers?: Readonly<Record<string, string>> | undefined;
98
98
  /** The first value of each query parameter of the request URL, by name. */
99
99
  readonly query?: Readonly<Record<string, string>> | undefined;
100
+ /**
101
+ * The connection of a network request (CLI-71): the addresses and the ports of its socket. A
102
+ * request hook can find the caller from its TCP peer with it, for example from the process that
103
+ * owns the remote port. Each network face sets it: the HTTP face, the WebSocket face (the upgrade
104
+ * and each call) and the Streamable HTTP endpoint of the MCP face.
105
+ *
106
+ * - A local request (a call in this process, MCP over stdio, a message port) has no connection.
107
+ * - An SDK transport of the application (`sdkTransport`, for example the legacy SSE transport of
108
+ * the MCP SDK) gives no connection. The SDK gives only the headers and the URL of each request.
109
+ * - The data comes only from the socket. It does not come from a header, for example
110
+ * `X-Forwarded-For` or `Forwarded`. Behind a proxy, the peer is the proxy. Thus a request
111
+ * hook that trusts the connection there sees the proxy, not the client.
112
+ * - The key of the idempotency cache has the principal (audit s21). When the principal comes
113
+ * from the connection, two callers behind one address can have one principal. Then a call
114
+ * of one caller can get the cached result of a call of the other caller with the same
115
+ * idempotency key.
116
+ */
117
+ readonly connection?: RequestConnection | undefined;
118
+ }
119
+
120
+ /**
121
+ * The connection of a network request (CLI-71): the addresses and the ports of its socket, as
122
+ * frozen plain data. The face copies them from the socket when the request comes in.
123
+ *
124
+ * Each field is optional, because a socket does not always have it. A Unix socket and a named
125
+ * pipe have no IP addresses and no ports. A socket that closed before the copy has no addresses.
126
+ */
127
+ export interface RequestConnection {
128
+ /** The IP address of the other end of the socket: the client, or a proxy. */
129
+ readonly remoteAddress?: string | undefined;
130
+ /** The port of the other end of the socket. It is the local port of the socket of the client. */
131
+ readonly remotePort?: number | undefined;
132
+ /** The IP address of this end of the socket: the address on which the listener took the request. */
133
+ readonly localAddress?: string | undefined;
134
+ /** The port of this end of the socket: the port of the listener. */
135
+ readonly localPort?: number | undefined;
100
136
  }
101
137
 
102
138
  /**
@@ -120,8 +156,9 @@ export interface RequestIdentity {
120
156
 
121
157
  /**
122
158
  * The request hook of a server: it reads the credentials of a network request (a cookie, a header,
123
- * the query) and gives the identity of the request. Undefined, or an error, refuses the request
124
- * with `UNAUTHORIZED`. The hook runs before the token check and before `authenticate`.
159
+ * the query, the connection of the socket) and gives the identity of the request. Undefined, or an
160
+ * error, refuses the request with `UNAUTHORIZED`. The hook runs before the token check and before
161
+ * `authenticate`.
125
162
  *
126
163
  * The hook can run more than one time for one request or one task. `admits()` runs it at a
127
164
  * WebSocket upgrade, and then each call over the connection runs it again. Thus a hook with a
@@ -133,7 +170,8 @@ export interface RequestIdentity {
133
170
  * again for them. For `admission()`, `admits()` and `identify()`, the request of the hook has an
134
171
  * empty method (`{ service: "", operation: "" }`), the id `admission`, `connection` or `identify`,
135
172
  * no payload and no metadata. Thus a hook must not branch on the method, the payload, the
136
- * metadata or the transport of the request. It reads only the credentials.
173
+ * metadata or the transport of the request. It reads only the credentials. The credentials of a
174
+ * network request also have the connection of its socket (`credentials.connection`, CLI-71).
137
175
  */
138
176
  export type PrincipalOf = (
139
177
  credentials: Credentials,