@emseepea/server 0.0.1 → 0.0.3

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
@@ -5,8 +5,9 @@ Protocol (MCP) `2026-07-28` servers over Streamable HTTP.
5
5
 
6
6
  It checks data when requests enter and leave the server. It supports public
7
7
  tools, tools that require sign-in, public resources and prompts, request time
8
- limits, and bounded progress updates for local examples. Prompt arguments and
9
- resource fields may also offer suggestions.
8
+ limits, and bounded progress updates. Direct tools,
9
+ resources, and prompts can ask capable clients for more information. Prompt
10
+ arguments and resource fields may also offer suggestions.
10
11
 
11
12
  The package is pre-alpha. See the
12
13
  [repository README](https://github.com/windyroad/emseepea#readme) for what the
@@ -50,6 +51,221 @@ const app = createEmseepea({ name: "coffee-guide", version: "1.0.0", tools: [get
50
51
  await serveEmseepea(app);
51
52
  ```
52
53
 
54
+ ## Measure Requests
55
+
56
+ Available in this checkout; not yet published to npm.
57
+
58
+ Set `telemetry: true` in `createEmseepea` to record request traces, counts, and
59
+ response times through OpenTelemetry. Leave it out to disable these measurements.
60
+
61
+ Configure your OpenTelemetry providers and context manager before creating the
62
+ app. Em See Pea does not choose an exporter or send data to a service.
63
+
64
+ ### What Gets Measured
65
+
66
+ Each trace covers one `/mcp` request through response completion or
67
+ disconnection, including progress updates. Measurements report:
68
+
69
+ - the HTTP method and status
70
+ - the known MCP method, or `_OTHER` for unknown names
71
+ - whether the response finished or disconnected
72
+
73
+ An HTTP 200 response can still contain a tool error. These measurements describe
74
+ the response, not whether the tool achieved its task.
75
+
76
+ ### What Stays Out
77
+
78
+ The framework does not attach tool names, request data, tokens, URLs, or error
79
+ messages. It preserves a valid active parent trace identity, but drops baggage
80
+ and other context data. It does not read trace headers itself.
81
+
82
+ ### Current Limits
83
+
84
+ SDK host and origin rejections happen before this measurement starts.
85
+ Ordinary telemetry API and exporter failures do not change tool results.
86
+ An adopter exporter that blocks the process or throws outside the telemetry
87
+ call is not isolated.
88
+
89
+ Structured logs are not included yet.
90
+
91
+ ## Report Dependency Readiness
92
+
93
+ Available in this checkout; not yet published to npm.
94
+
95
+ Pass a `readiness` callback to `createEmseepea` to check the dependencies your
96
+ server needs. The callback receives `{ signal }` and must return `true` only
97
+ when those dependencies are ready. It may return a promise.
98
+
99
+ `GET /readyz` returns:
100
+
101
+ - HTTP 200 and `ready` when the check succeeds
102
+ - HTTP 503 and `not ready` when it fails, throws, or takes too long
103
+
104
+ The reply never includes dependency details or error messages. It is not cached.
105
+ `GET /healthz` continues to report that the process is alive, not that its
106
+ dependencies work. Without a callback, readiness does not check dependencies.
107
+
108
+ Set `readinessTimeoutMs` to limit the check. It defaults to 1,000 milliseconds
109
+ and accepts whole numbers from 1 to 60,000. A timeout or shutdown signals
110
+ cancellation. Only one dependency check can remain unfinished at a time; if it
111
+ ignores cancellation, later probes return not-ready until it settles.
112
+
113
+ Readiness reports health for a load balancer or monitoring system. It does not
114
+ block MCP calls itself. Tool handlers must still handle an unavailable backend.
115
+
116
+ ## Flush Measurements on Shutdown
117
+
118
+ Available in this checkout; not yet published to npm.
119
+
120
+ Pass `flushTelemetry` to `serveEmseepea` to flush your OpenTelemetry providers
121
+ when the returned server's `close()` method runs. The callback receives
122
+ `{ signal }`; it can call your providers' `forceFlush()` methods. Calling the
123
+ Fastify app's `close()` directly does not run this callback.
124
+
125
+ Shutdown uses two time limits:
126
+
127
+ - `shutdownTimeoutMs`: finish existing requests, then close remaining
128
+ connections. The default is 5,000 milliseconds.
129
+ - `telemetryFlushTimeoutMs`: wait for the final request measurements, then flush.
130
+ The default is 1,000 milliseconds; the allowed range is 1 to 60,000.
131
+
132
+ The total waiting budget is the sum of those limits. The second limit applies
133
+ only when you provide `flushTelemetry`. If final measurements are not ready in
134
+ time, flushing is skipped. Repeated `close()` calls share the same work.
135
+
136
+ Flushing is best-effort. Its errors do not change tool responses, and completion
137
+ of `close()` does not prove that an exporter delivered data. Callbacks that block
138
+ JavaScript or ignore cancellation cannot be forcibly stopped by the framework.
139
+
140
+ ## Describe What Clients Can Show
141
+
142
+ Applications can give servers, tools, resources, reusable resource addresses,
143
+ and prompts a human-friendly title, description, and icons. A server can also
144
+ provide its website address. Tools can say whether they only read data and
145
+ whether they contact services outside the application:
146
+
147
+ ```ts
148
+ const getBeanDetails = defineTool({
149
+ name: "get-bean-details",
150
+ title: "Get bean details",
151
+ access: "public",
152
+ description: "Get the recorded details for one coffee.",
153
+ icons: [{ src: "https://coffee.example/icons/bean.png", mimeType: "image/png" }],
154
+ annotations: { readOnlyHint: true, openWorldHint: false },
155
+ inputSchema: z.object({ name: z.string() }),
156
+ outputSchema: beanDetails,
157
+ handler,
158
+ });
159
+ ```
160
+
161
+ Resources may also include their known byte size and hints about their intended
162
+ audience and importance. Use `_meta` for application-specific public details.
163
+
164
+ Em See Pea checks this metadata when the application starts and copies it, so
165
+ later changes to the original objects have no effect. Tool annotations are
166
+ hints for clients. They do not prove that a tool is safe, grant permission, or
167
+ replace sign-in and authorization checks.
168
+
169
+ ## Route with a Tool Argument
170
+
171
+ A proxy can route a request using a checked tool argument without reading the
172
+ JSON body. Mark a string, integer, or boolean property with `x-mcp-header`:
173
+
174
+ ```ts
175
+ const inputSchema = z.object({
176
+ region: z.string().meta({ "x-mcp-header": "Region" }),
177
+ });
178
+ ```
179
+
180
+ Compatible clients send the same value as `Mcp-Param-Region`. The tool still
181
+ receives and checks `region` normally. The server rejects a missing or
182
+ different header before the tool runs.
183
+
184
+ This header is a copy of tool input. It does not identify a person, prove that
185
+ they signed in, grant permission, or protect a secret.
186
+
187
+ ## Bound Large Catalogues
188
+
189
+ Split long tool, resource, resource-address, and prompt lists into pages by
190
+ adding one option to the server:
191
+
192
+ ```ts
193
+ const app = createEmseepea({
194
+ name: "coffee-guide",
195
+ version: "1.0.0",
196
+ tools,
197
+ listPagination: { pageSize: 50, maxPageBytes: 256 * 1024 },
198
+ });
199
+ ```
200
+
201
+ `pageSize` must be between 1 and 100. `maxPageBytes` defaults to one mebibyte and
202
+ stops a page earlier when its public catalogue details would be too large. The
203
+ server stops at startup if one item cannot fit.
204
+
205
+ Clients receive an opaque cursor for the next page. Identical server instances
206
+ accept the same cursor. Changing the catalogue or either limit makes old
207
+ cursors invalid. Cursors select public catalogue pages; they do not identify a
208
+ person or grant permission. Omit `listPagination` to keep one-page lists.
209
+
210
+ ## Ask the Client for More Information
211
+
212
+ Application authors can create a direct tool, resource, resource address
213
+ pattern, or prompt that pauses and asks a capable client for more information.
214
+ The client answers by making a fresh request, and the handler runs again with
215
+ those answers in its context.
216
+
217
+ This example asks the person for a coffee preference:
218
+
219
+ ```ts
220
+ import { acceptedContent, defineTool, inputRequired } from "@emseepea/server";
221
+ import { z } from "zod";
222
+
223
+ const preference = z.object({ roast: z.enum(["light", "medium", "dark"]) });
224
+
225
+ const chooseRoast = defineTool({
226
+ name: "choose-roast",
227
+ access: "public",
228
+ description: "Ask which roast a person prefers for one coffee.",
229
+ inputSchema: z.object({ coffee: z.string() }),
230
+ outputSchema: z.object({ coffee: z.string(), roast: z.string() }),
231
+ handler: ({ coffee }, context) => {
232
+ const answer = acceptedContent(context.inputResponses, "preference", preference);
233
+ if (!answer) {
234
+ return inputRequired({
235
+ inputRequests: {
236
+ preference: inputRequired.elicit({
237
+ message: `Which roast do you prefer for ${coffee}?`,
238
+ requestedSchema: preference,
239
+ }),
240
+ },
241
+ });
242
+ }
243
+ return {
244
+ text: `${answer.roast} roast selected for ${coffee}`,
245
+ data: { coffee, roast: answer.roast },
246
+ };
247
+ },
248
+ });
249
+ ```
250
+
251
+ Use `acceptedContent` with the same schema that described the form. Client
252
+ answers are untrusted even when they have the expected wire shape. Use
253
+ `inputResponse` when the handler must distinguish acceptance, refusal, and
254
+ cancellation.
255
+
256
+ A client answer supplies information. It is not proof of identity or permission
257
+ to change data. Authorize any effect through the application's normal security
258
+ boundary.
259
+
260
+ The client must advertise elicitation for form input or URL-mode elicitation,
261
+ where the client opens a URL. Em See Pea applies the normal result-size limit,
262
+ time limit, cancellation, safe-error handling, and sign-in policy to every
263
+ round.
264
+
265
+ This release supports stateless requests only. It rejects opaque
266
+ `requestState`. Mapped tools and progress-reporting tools return through their
267
+ existing checked paths and cannot request more client input.
268
+
53
269
  ## Mapped Backend Tool
54
270
 
55
271
  `defineMappedTool` validates the public input, mapped backend command, backend
@@ -166,7 +382,8 @@ progress, the official MCP library sends progress events over the same POST
166
382
  request. Otherwise the call returns one JSON response.
167
383
 
168
384
  ```ts
169
- import { defineStreamingTool } from "@emseepea/server";
385
+ import { createEmseepea, defineStreamingTool } from "@emseepea/server";
386
+ import { z } from "zod";
170
387
 
171
388
  const roast = defineStreamingTool({
172
389
  name: "roast-batch",
@@ -190,13 +407,64 @@ change those positive bounds. The reporter closes with the tool call.
190
407
  Heartbeats are disabled. Invalid, oversized, late, or extra updates are
191
408
  rejected.
192
409
 
193
- Streaming tools start only for local examples. They do not yet support:
410
+ ### Use Progress Behind a Proxy
411
+
412
+ In the current source, public tools can also report progress behind a trusted
413
+ HTTPS proxy. This addition is not yet in a published npm release.
414
+
415
+ Using the `roast` tool above, configure the proxy's exact address and the public
416
+ host and origin your clients use:
417
+
418
+ ```ts
419
+ const app = createEmseepea({
420
+ name: "coffee-guide",
421
+ version: "1.0.0",
422
+ tools: [roast],
423
+ deployment: {
424
+ mode: "production-behind-proxy",
425
+ trustedProxyAddresses: ["10.0.0.10"],
426
+ allowedAuthorities: ["mcp.example.com"],
427
+ allowedOrigins: ["https://mcp.example.com"],
428
+ rateLimit: { maxRequests: 100, windowMs: 60_000, maxClients: 1_000 },
429
+ },
430
+ });
431
+ ```
432
+
433
+ Replace these example addresses with yours. The server still listens locally
434
+ by default. Choose a private listening address explicitly when starting it,
435
+ and restrict network access so only your proxy can reach it.
436
+
437
+ Your proxy must replace caller-supplied forwarding headers and send:
438
+
439
+ - `X-Forwarded-Proto: https` for the original HTTPS connection
440
+ - `X-Forwarded-For` containing one client IP address, not a comma-separated chain
441
+ - `Host` matching an entry in `allowedAuthorities`
442
+
443
+ It must also forward streamed responses without buffering them.
444
+ `X-Accel-Buffering: no` asks compatible proxies not to buffer; it does not
445
+ configure your proxy for you.
446
+
447
+ Each call sends progress on its already-open HTTP response. A later call can
448
+ go to another server without pinning that client to the first server. This
449
+ does not share application state or rate limits: the configured limit applies
450
+ to each server separately.
451
+
452
+ ### Limits
453
+
454
+ Progress remains limited by the event count, event size, result size, and
455
+ request deadline. Disconnecting cancels work that observes the cancellation
456
+ signal. These checks do not slow the producer to match a slow reader.
457
+
458
+ Not supported yet:
194
459
 
195
460
  - long-running streams opened with GET
196
461
  - saved sessions, replay, or subscriptions
197
- - proxy servers or recovery after reconnecting
462
+ - recovery after reconnecting
198
463
  - slowing the producer when a client cannot keep up
199
- - more than one server instance
464
+ - progress from tools requiring sign-in outside local development
465
+
466
+ See the [progress coverage and tested proxy setup](../../docs/protocol-coverage.md#progress-updates)
467
+ for the current checks and their limits.
200
468
 
201
469
  ## Public Resources and Prompts
202
470
 
@@ -226,6 +494,7 @@ const guide = defineResource({
226
494
  name: "getting-started",
227
495
  uri: guideUri,
228
496
  mimeType: "text/markdown",
497
+ cacheHint: { ttlMs: 30_000 },
229
498
  handler: () => ({ contents: [{ uri: guideUri, text: "# Brew safely" }] }),
230
499
  });
231
500
 
@@ -257,9 +526,31 @@ const app = createEmseepea({
257
526
  version: "1.0.0",
258
527
  resources: [guide, methodGuide],
259
528
  prompts: [brew],
529
+ cacheHints: {
530
+ "resources/list": { ttlMs: 60_000, cacheScope: "public" },
531
+ "resources/read": { ttlMs: 5_000, cacheScope: "private" },
532
+ },
260
533
  });
261
534
  ```
262
535
 
536
+ ## Tell Clients When They May Reuse Results
537
+
538
+ By default, clients are told not to reuse lists, discovery details, or resource
539
+ content. `cacheHints` sets a time limit in milliseconds and says whether a
540
+ shared cache may store the result.
541
+
542
+ Use `public` only when every caller receives the same non-sensitive result.
543
+ Use `private` for results that only the requesting client may keep. These
544
+ instructions do not grant access and do not make changing or personal data safe
545
+ to share.
546
+
547
+ An individual resource or reusable resource address can override either field.
548
+ In the example, `guide` overrides the time limit while its missing
549
+ `cacheScope` still comes from `cacheHints["resources/read"]`.
550
+
551
+ Cache instructions are supported for discovery, tool lists, resource lists,
552
+ reusable resource-address lists, prompt lists, and resource reads.
553
+
263
554
  Resource templates advertise an address pattern rather than listing every
264
555
  possible address. Clients list the patterns, then read an address that matches
265
556
  one. Each `{variable}` fills one complete path segment. The scheme and host
@@ -272,8 +563,8 @@ Pea checks the returned suggestions and keeps at most the first 100.
272
563
  Suggestions are public even when some tools require sign-in. Return only text
273
564
  that is safe for anyone to discover.
274
565
 
275
- Listing every matching address, large result pages, sign-in for resources, and
276
- custom caching are not included yet.
566
+ Listing every matching address, catalogue pages without the bounds above, and
567
+ sign-in for resources are not included yet.
277
568
 
278
569
  ## Tool That Requires Sign-In
279
570
 
package/dist/http.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ export interface JsonHttpClientOptions {
2
+ readonly origin: string | URL;
3
+ readonly maxResponseBytes?: number;
4
+ }
5
+ export interface JsonHttpGetOptions {
6
+ readonly pathname: string;
7
+ readonly searchParams?: Readonly<Record<string, string | readonly string[]>>;
8
+ readonly signal: AbortSignal;
9
+ readonly deadlineMs: number;
10
+ }
11
+ export interface JsonHttpClient {
12
+ readonly get: (options: JsonHttpGetOptions) => Promise<unknown>;
13
+ }
14
+ export declare function createJsonHttpClient(candidate: JsonHttpClientOptions): JsonHttpClient;
15
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAiDA,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC;IAC9B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;IAC7E,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACjE;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,qBAAqB,GAAG,cAAc,CAiCrF"}
package/dist/http.js ADDED
@@ -0,0 +1,284 @@
1
+ import { lookup } from "node:dns/promises";
2
+ import { request } from "node:https";
3
+ import { BlockList, isIP } from "node:net";
4
+ import { TextDecoder } from "node:util";
5
+ const MAX_RESPONSE_BYTES = 1024 * 1024;
6
+ const MAX_RESPONSE_HEADER_BYTES = 16 * 1024;
7
+ const MAX_REQUEST_TARGET_BYTES = 8 * 1024;
8
+ const MAX_DEADLINE_DELAY_MS = 2_147_483_647;
9
+ const FAILURE_MESSAGE = "Outbound JSON request failed";
10
+ const prohibitedIpv4Addresses = new BlockList();
11
+ for (const [network, prefix] of [
12
+ ["0.0.0.0", 8],
13
+ ["10.0.0.0", 8],
14
+ ["100.64.0.0", 10],
15
+ ["127.0.0.0", 8],
16
+ ["169.254.0.0", 16],
17
+ ["172.16.0.0", 12],
18
+ ["192.0.0.0", 24],
19
+ ["192.0.2.0", 24],
20
+ ["192.88.99.0", 24],
21
+ ["192.168.0.0", 16],
22
+ ["198.18.0.0", 15],
23
+ ["198.51.100.0", 24],
24
+ ["203.0.113.0", 24],
25
+ ["224.0.0.0", 4],
26
+ ["240.0.0.0", 4],
27
+ ])
28
+ prohibitedIpv4Addresses.addSubnet(network, prefix, "ipv4");
29
+ const prohibitedIpv6Addresses = new BlockList();
30
+ const globallyRoutableIpv6Addresses = new BlockList();
31
+ globallyRoutableIpv6Addresses.addSubnet("2000::", 3, "ipv6");
32
+ for (const [network, prefix] of [
33
+ ["::", 128],
34
+ ["::1", 128],
35
+ ["::ffff:0:0", 96],
36
+ ["64:ff9b::", 96],
37
+ ["64:ff9b:1::", 48],
38
+ ["100::", 64],
39
+ ["2001::", 23],
40
+ ["2001:db8::", 32],
41
+ ["2002::", 16],
42
+ ["3fff::", 20],
43
+ ["fc00::", 7],
44
+ ["fe80::", 10],
45
+ ["ff00::", 8],
46
+ ])
47
+ prohibitedIpv6Addresses.addSubnet(network, prefix, "ipv6");
48
+ export function createJsonHttpClient(candidate) {
49
+ assertKeys(candidate, ["origin", "maxResponseBytes"], "HTTP client options");
50
+ const origin = normalizeOrigin(candidate.origin);
51
+ const maxResponseBytes = boundedPositiveInteger(candidate.maxResponseBytes ?? MAX_RESPONSE_BYTES, "maxResponseBytes", MAX_RESPONSE_BYTES);
52
+ return Object.freeze({
53
+ async get(options) {
54
+ try {
55
+ assertKeys(options, ["pathname", "searchParams", "signal", "deadlineMs"], "HTTP GET options");
56
+ const url = requestUrl(origin, options.pathname, options.searchParams);
57
+ const operation = operationSignal(options.signal, options.deadlineMs);
58
+ try {
59
+ operation.signal.throwIfAborted();
60
+ const addresses = await waitFor(lookup(url.hostname, { all: true, order: "verbatim" }), operation.signal);
61
+ operation.signal.throwIfAborted();
62
+ if (addresses.length === 0 || addresses.some(({ address, family }) => !isAllowedAddress(address, family))) {
63
+ throw new Error(FAILURE_MESSAGE);
64
+ }
65
+ const selected = addresses[0];
66
+ if (!selected)
67
+ throw new Error(FAILURE_MESSAGE);
68
+ return await waitFor(readJson(url, selected, maxResponseBytes, operation.signal), operation.signal);
69
+ }
70
+ finally {
71
+ operation.dispose();
72
+ }
73
+ }
74
+ catch {
75
+ throw new Error(FAILURE_MESSAGE);
76
+ }
77
+ },
78
+ });
79
+ }
80
+ function normalizeOrigin(candidate) {
81
+ let url;
82
+ try {
83
+ url = new URL(candidate instanceof URL ? candidate.href : candidate);
84
+ }
85
+ catch {
86
+ throw new TypeError("origin must be a valid URL");
87
+ }
88
+ const hostname = url.hostname.startsWith("[") && url.hostname.endsWith("]")
89
+ ? url.hostname.slice(1, -1)
90
+ : url.hostname;
91
+ if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" ||
92
+ url.search || url.hash || isIP(hostname) !== 0) {
93
+ throw new TypeError("origin must be an HTTPS origin without credentials, path, query, fragment, or IP literal");
94
+ }
95
+ return new URL(url.origin);
96
+ }
97
+ function requestUrl(origin, pathname, searchParams) {
98
+ if (typeof pathname !== "string" || !pathname.startsWith("/") || pathname.startsWith("//") ||
99
+ pathname.includes("\\") || pathname.includes("?") || pathname.includes("#")) {
100
+ throw new TypeError("pathname must be one root-relative URL path");
101
+ }
102
+ const url = new URL(pathname, origin);
103
+ if (url.origin !== origin.origin || url.protocol !== "https:" || url.username || url.password || url.hash) {
104
+ throw new TypeError("pathname escaped the configured origin");
105
+ }
106
+ if (searchParams !== undefined) {
107
+ assertPlainRecord(searchParams, "searchParams");
108
+ for (const [name, value] of Object.entries(searchParams).sort(([left], [right]) => left.localeCompare(right))) {
109
+ if (typeof value === "string") {
110
+ url.searchParams.append(name, value);
111
+ }
112
+ else if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
113
+ for (const entry of value)
114
+ url.searchParams.append(name, entry);
115
+ }
116
+ else {
117
+ throw new TypeError("searchParams values must be strings or string arrays");
118
+ }
119
+ }
120
+ }
121
+ if (Buffer.byteLength(`${url.pathname}${url.search}`, "utf8") > MAX_REQUEST_TARGET_BYTES) {
122
+ throw new TypeError("request target exceeds the configured size limit");
123
+ }
124
+ return url;
125
+ }
126
+ function isAllowedAddress(address, family) {
127
+ if (isIP(address) !== family || (family !== 4 && family !== 6))
128
+ return false;
129
+ return family === 4
130
+ ? !prohibitedIpv4Addresses.check(address, "ipv4")
131
+ : globallyRoutableIpv6Addresses.check(address, "ipv6")
132
+ && !prohibitedIpv6Addresses.check(address, "ipv6");
133
+ }
134
+ function operationSignal(parent, deadlineMs) {
135
+ if (!(parent instanceof AbortSignal) || !Number.isSafeInteger(deadlineMs) ||
136
+ deadlineMs - Date.now() > MAX_DEADLINE_DELAY_MS) {
137
+ throw new TypeError("signal and a safe-integer absolute deadlineMs are required");
138
+ }
139
+ const controller = new AbortController();
140
+ const abort = () => controller.abort();
141
+ if (parent.aborted || deadlineMs <= Date.now())
142
+ controller.abort();
143
+ else
144
+ parent.addEventListener("abort", abort, { once: true });
145
+ const timer = setTimeout(abort, Math.max(0, deadlineMs - Date.now()));
146
+ timer.unref();
147
+ return {
148
+ signal: controller.signal,
149
+ dispose() {
150
+ clearTimeout(timer);
151
+ parent.removeEventListener("abort", abort);
152
+ },
153
+ };
154
+ }
155
+ async function waitFor(promise, signal) {
156
+ if (signal.aborted)
157
+ throw new Error(FAILURE_MESSAGE);
158
+ return await new Promise((resolve, reject) => {
159
+ let settled = false;
160
+ const finish = (callback) => {
161
+ if (settled)
162
+ return;
163
+ settled = true;
164
+ signal.removeEventListener("abort", abort);
165
+ callback();
166
+ };
167
+ const abort = () => finish(() => reject(new Error(FAILURE_MESSAGE)));
168
+ signal.addEventListener("abort", abort, { once: true });
169
+ void promise.then((value) => finish(() => resolve(value)), () => finish(() => reject(new Error(FAILURE_MESSAGE))));
170
+ });
171
+ }
172
+ function readJson(url, selected, maxResponseBytes, signal) {
173
+ return new Promise((resolve, reject) => {
174
+ let settled = false;
175
+ const fail = () => {
176
+ if (settled)
177
+ return;
178
+ settled = true;
179
+ reject(new Error(FAILURE_MESSAGE));
180
+ };
181
+ const requestOptions = {
182
+ agent: false,
183
+ autoSelectFamily: false,
184
+ headers: {
185
+ Accept: "application/json",
186
+ "Accept-Encoding": "identity",
187
+ Host: url.host,
188
+ },
189
+ family: selected.family,
190
+ lookup: (_hostname, _options, callback) => callback(null, selected.address, selected.family),
191
+ maxHeaderSize: MAX_RESPONSE_HEADER_BYTES,
192
+ method: "GET",
193
+ servername: url.hostname,
194
+ signal,
195
+ };
196
+ const outgoing = request(url, requestOptions, (incoming) => {
197
+ if (signal.aborted || settled) {
198
+ incoming.destroy();
199
+ return;
200
+ }
201
+ const status = incoming.statusCode ?? 0;
202
+ const contentType = oneHeader(incoming.headersDistinct?.["content-type"]);
203
+ const contentEncoding = oneHeader(incoming.headersDistinct?.["content-encoding"]);
204
+ const contentLength = oneHeader(incoming.headersDistinct?.["content-length"]);
205
+ if (status < 200 || status > 299 || !isJsonContentType(contentType ?? undefined) ||
206
+ contentEncoding === null ||
207
+ (contentEncoding !== undefined && contentEncoding.toLowerCase() !== "identity") ||
208
+ contentLength === null ||
209
+ (contentLength !== undefined && !validContentLength(contentLength, maxResponseBytes))) {
210
+ incoming.destroy();
211
+ fail();
212
+ return;
213
+ }
214
+ const chunks = [];
215
+ let bytes = 0;
216
+ incoming.on("data", (chunk) => {
217
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
218
+ bytes += buffer.length;
219
+ if (bytes > maxResponseBytes) {
220
+ incoming.destroy();
221
+ fail();
222
+ }
223
+ else {
224
+ chunks.push(buffer);
225
+ }
226
+ });
227
+ incoming.once("aborted", fail);
228
+ incoming.once("error", fail);
229
+ incoming.once("close", () => {
230
+ if (!incoming.complete)
231
+ fail();
232
+ });
233
+ incoming.once("end", () => {
234
+ if (settled)
235
+ return;
236
+ try {
237
+ signal.throwIfAborted();
238
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks));
239
+ const value = JSON.parse(text);
240
+ signal.throwIfAborted();
241
+ settled = true;
242
+ resolve(value);
243
+ }
244
+ catch {
245
+ fail();
246
+ }
247
+ });
248
+ });
249
+ outgoing.once("error", fail);
250
+ outgoing.end();
251
+ });
252
+ }
253
+ function oneHeader(values) {
254
+ if (values === undefined || values.length === 0)
255
+ return undefined;
256
+ return values.length === 1 ? values[0] ?? null : null;
257
+ }
258
+ function isJsonContentType(value) {
259
+ return value !== undefined && /^application\/(?:json|[a-z0-9!#$&^_.+-]+\+json)(?:\s*;.*)?$/i.test(value);
260
+ }
261
+ function validContentLength(value, maximum) {
262
+ const length = Number(value);
263
+ return /^(?:0|[1-9]\d*)$/.test(value) && Number.isSafeInteger(length) && length <= maximum;
264
+ }
265
+ function assertKeys(value, allowed, label) {
266
+ assertPlainRecord(value, label);
267
+ const accepted = new Set(allowed);
268
+ if (Reflect.ownKeys(value).some((key) => typeof key !== "string" || !accepted.has(key))) {
269
+ throw new TypeError(`${label} contains an unsupported field`);
270
+ }
271
+ }
272
+ function assertPlainRecord(value, label) {
273
+ if (typeof value !== "object" || value === null || Array.isArray(value) ||
274
+ (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
275
+ throw new TypeError(`${label} must be a plain object`);
276
+ }
277
+ }
278
+ function boundedPositiveInteger(value, label, maximum) {
279
+ if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) {
280
+ throw new TypeError(`${label} must be a positive integer no greater than ${maximum}`);
281
+ }
282
+ return value;
283
+ }
284
+ //# sourceMappingURL=http.js.map