@try-works/dsh-model-as-tool 0.1.3 → 0.4.0
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/bin/dsh-tool-mode-credentials.mjs +20 -0
- package/bin/dsh-tool-mode.mjs +202 -0
- package/catalog/desert-ant-models.json +693 -693
- package/client/client.model-tools.js +614 -0
- package/client/index.js +1717 -0
- package/lib/adapters/index.d.ts.map +1 -1
- package/lib/adapters/index.js +4 -4
- package/lib/bindings.d.ts.map +1 -1
- package/lib/bindings.js +26 -25
- package/lib/capability.d.ts.map +1 -1
- package/lib/capability.js +17 -0
- package/lib/capture-path.d.ts +97 -0
- package/lib/capture-path.d.ts.map +1 -0
- package/lib/capture-path.js +152 -0
- package/lib/diagnostics.d.ts +166 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +135 -0
- package/lib/extension-dependencies.d.ts +325 -0
- package/lib/extension-dependencies.d.ts.map +1 -0
- package/lib/extension-dependencies.js +858 -0
- package/lib/extension-dispatch.d.ts +117 -0
- package/lib/extension-dispatch.d.ts.map +1 -0
- package/lib/extension-dispatch.js +124 -0
- package/lib/extension-registrar.d.ts +100 -0
- package/lib/extension-registrar.d.ts.map +1 -0
- package/lib/extension-registrar.js +134 -0
- package/lib/extensions-api.d.ts +221 -0
- package/lib/extensions-api.d.ts.map +1 -0
- package/lib/extensions-api.js +17 -0
- package/lib/extensions-remote.d.ts +54 -0
- package/lib/extensions-remote.d.ts.map +1 -0
- package/lib/extensions-remote.js +66 -0
- package/lib/extensions-service.d.ts +367 -0
- package/lib/extensions-service.d.ts.map +1 -0
- package/lib/extensions-service.js +1527 -0
- package/lib/harness-llm.d.ts +138 -0
- package/lib/harness-llm.d.ts.map +1 -0
- package/lib/harness-llm.js +218 -0
- package/lib/harness-serving.d.ts +95 -0
- package/lib/harness-serving.d.ts.map +1 -0
- package/lib/harness-serving.js +157 -0
- package/lib/headless.d.ts +97 -0
- package/lib/headless.d.ts.map +1 -0
- package/lib/headless.js +349 -0
- package/lib/http-acquisition.d.ts +27 -0
- package/lib/http-acquisition.d.ts.map +1 -0
- package/lib/http-acquisition.js +136 -0
- package/lib/index.d.ts +271 -7
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2644 -70
- package/lib/invocation-accounting.d.ts +169 -0
- package/lib/invocation-accounting.d.ts.map +1 -0
- package/lib/invocation-accounting.js +148 -0
- package/lib/legacy-migration.d.ts +95 -0
- package/lib/legacy-migration.d.ts.map +1 -0
- package/lib/legacy-migration.js +429 -0
- package/lib/managed-workers.d.ts +151 -0
- package/lib/managed-workers.d.ts.map +1 -0
- package/lib/managed-workers.js +249 -0
- package/lib/mcp/client-grant.d.ts +74 -0
- package/lib/mcp/client-grant.d.ts.map +1 -0
- package/lib/mcp/client-grant.js +60 -0
- package/lib/mcp/credential-cli.d.ts +38 -0
- package/lib/mcp/credential-cli.d.ts.map +1 -0
- package/lib/mcp/credential-cli.js +148 -0
- package/lib/mcp/credential-store.d.ts +143 -0
- package/lib/mcp/credential-store.d.ts.map +1 -0
- package/lib/mcp/credential-store.js +454 -0
- package/lib/mcp/host-binding.d.ts +25 -0
- package/lib/mcp/host-binding.d.ts.map +1 -1
- package/lib/mcp/host-binding.js +107 -14
- package/lib/mcp/http.d.ts +68 -18
- package/lib/mcp/http.d.ts.map +1 -1
- package/lib/mcp/http.js +390 -62
- package/lib/mcp/oauth.d.ts +149 -0
- package/lib/mcp/oauth.d.ts.map +1 -0
- package/lib/mcp/oauth.js +396 -0
- package/lib/mcp/protocol.d.ts +46 -7
- package/lib/mcp/protocol.d.ts.map +1 -1
- package/lib/mcp/protocol.js +63 -16
- package/lib/mcp/rate-limit.d.ts +68 -0
- package/lib/mcp/rate-limit.d.ts.map +1 -0
- package/lib/mcp/rate-limit.js +114 -0
- package/lib/mcp/sdk-v2.d.ts +17 -0
- package/lib/mcp/sdk-v2.d.ts.map +1 -0
- package/lib/mcp/sdk-v2.js +181 -0
- package/lib/mcp/server.d.ts +11 -1
- package/lib/mcp/server.d.ts.map +1 -1
- package/lib/mcp/server.js +80 -13
- package/lib/mcp/stdio.d.ts.map +1 -1
- package/lib/mcp/stdio.js +9 -1
- package/lib/mcp/subscriptions.d.ts +43 -1
- package/lib/mcp/subscriptions.d.ts.map +1 -1
- package/lib/mcp/subscriptions.js +90 -9
- package/lib/model-qualification.d.ts +192 -0
- package/lib/model-qualification.d.ts.map +1 -0
- package/lib/model-qualification.js +178 -0
- package/lib/planning.d.ts +246 -0
- package/lib/planning.d.ts.map +1 -0
- package/lib/planning.js +457 -0
- package/lib/prepared-assets.d.ts +178 -0
- package/lib/prepared-assets.d.ts.map +1 -0
- package/lib/prepared-assets.js +286 -0
- package/lib/provider-bridge.d.ts +37 -0
- package/lib/provider-bridge.d.ts.map +1 -1
- package/lib/provider-bridge.js +189 -12
- package/lib/ptc-authority.d.ts +140 -0
- package/lib/ptc-authority.d.ts.map +1 -0
- package/lib/ptc-authority.js +216 -0
- package/lib/ptc-runtime.d.ts +156 -0
- package/lib/ptc-runtime.d.ts.map +1 -0
- package/lib/ptc-runtime.js +191 -0
- package/lib/publication.d.ts +2 -1
- package/lib/publication.d.ts.map +1 -1
- package/lib/publication.js +13 -5
- package/lib/receipt.d.ts.map +1 -1
- package/lib/receipt.js +8 -0
- package/lib/title-proposal.d.ts +194 -0
- package/lib/title-proposal.d.ts.map +1 -0
- package/lib/title-proposal.js +244 -0
- package/lib/tool-mode-run.d.ts +235 -0
- package/lib/tool-mode-run.d.ts.map +1 -0
- package/lib/tool-mode-run.js +283 -0
- package/lib/types.d.ts +3 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/update-surface.d.ts +79 -0
- package/lib/update-surface.d.ts.map +1 -0
- package/lib/update-surface.js +224 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts +38 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.js +445 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts +14 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.js +63 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts +14 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.js +76 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts.map +1 -1
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.js +69 -55
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts +4 -1
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts.map +1 -1
- package/node_modules/@try-works/tool-mode-contracts/lib/index.js +26 -2
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +28 -2
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts.map +1 -1
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.js +93 -10
- package/node_modules/@try-works/tool-mode-contracts/package.json +0 -1
- package/node_modules/@try-works/tool-mode-extension-sdk/bin/dsh-tool-mode.mjs +20 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts +80 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.js +533 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts +83 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.js +125 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts +70 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.js +201 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts +57 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.js +83 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts +32 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.js +32 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts +86 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.js +191 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts +126 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.js +297 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts +216 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.js +800 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts +210 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.js +368 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts +63 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.js +149 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts +35 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.js +179 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts +179 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.js +235 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts +367 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.js +807 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.d.ts +44 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.js +243 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts +185 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.js +504 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts +17 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.js +80 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts +52 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.js +102 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts +171 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.js +271 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts +242 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.js +58 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts +118 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.js +113 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts +2 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.js +351 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts +150 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.js +670 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/package.json +53 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/schema/extension-manifest-1.1.0.schema.json +253 -0
- package/node_modules/@try-works/tool-mode-recipes/bin/tool-mode-recipe.mjs +15 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts +427 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.js +592 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts +60 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.js +936 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts +237 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.js +200 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts +85 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.js +176 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts +105 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.js +129 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts +25 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.js +25 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts +325 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.js +1310 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts +51 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.js +310 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts +227 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.js +407 -0
- package/node_modules/@try-works/tool-mode-recipes/package.json +56 -0
- package/node_modules/@try-works/tool-mode-recipes/recipes/describe-image.json +38 -0
- package/node_modules/@try-works/tool-mode-recipes/recipes/text-title.json +45 -0
- package/node_modules/@try-works/tool-mode-recipes/schema/harness-model-recipe.schema.json +273 -0
- package/package.json +43 -15
- package/remote/spec.mjs +462 -0
- package/remote/typert.host.mjs +14 -0
- package/remote/typert.remote-client.mjs +16 -0
- package/lib/adapters/tongue.d.ts +0 -31
- package/lib/adapters/tongue.d.ts.map +0 -1
- package/lib/adapters/tongue.js +0 -93
- package/lib/adapters/voz.d.ts +0 -28
- package/lib/adapters/voz.d.ts.map +0 -1
- package/lib/adapters/voz.js +0 -102
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts +0 -0
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts.map +0 -0
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.js +0 -0
package/lib/mcp/http.js
CHANGED
|
@@ -17,15 +17,21 @@
|
|
|
17
17
|
* @module @try-works/dsh-model-as-tool/mcp/http
|
|
18
18
|
*/
|
|
19
19
|
import { timingSafeEqual } from 'node:crypto';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
|
|
20
|
+
import { toWebRequest } from '@modelcontextprotocol/node';
|
|
21
|
+
import { assertHeadersMatch, decodeHeaderValue, ERROR_CODES, errorResponse, negotiateVersion, parseRequestMetadata, unsupportedRequiredCapabilities, WireError, } from "./protocol.js";
|
|
22
|
+
import { serveSdkV2Request } from "./sdk-v2.js";
|
|
23
|
+
import { oauthChallenge, oauthErrorStatus } from "./oauth.js";
|
|
24
|
+
import { createRateLimiter, DEFAULT_RATE_LIMIT_POLICY, } from "./rate-limit.js";
|
|
25
|
+
import { resolveClientGrant } from "./client-grant.js";
|
|
26
|
+
/** A conservative default: loopback, the illustrative port, a small body cap,
|
|
27
|
+
* and the default per-client rate limit (AT-12). */
|
|
23
28
|
export const DEFAULT_HTTP_CONFIG = {
|
|
24
29
|
path: '/mcp',
|
|
25
30
|
host: '127.0.0.1',
|
|
26
31
|
port: 3765,
|
|
27
32
|
maxBodyBytes: 4 * 1024 * 1024,
|
|
28
33
|
allowedOrigins: [],
|
|
34
|
+
rateLimit: DEFAULT_RATE_LIMIT_POLICY,
|
|
29
35
|
};
|
|
30
36
|
/** Whether an address is loopback. */
|
|
31
37
|
export function isLoopback(address) {
|
|
@@ -60,33 +66,75 @@ function tokenMatches(presented, accepted) {
|
|
|
60
66
|
return false;
|
|
61
67
|
return timingSafeEqual(left, right);
|
|
62
68
|
}
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
/** Validate the Host header when the client supplies one. */
|
|
70
|
+
function hostHeaderAllowed(header, config) {
|
|
71
|
+
if (header === undefined || header.length === 0)
|
|
72
|
+
return true;
|
|
73
|
+
const value = header.toLowerCase();
|
|
74
|
+
const authority = value.startsWith('[')
|
|
75
|
+
? value.slice(1, value.indexOf(']'))
|
|
76
|
+
: (value.split(':')[0] ?? '');
|
|
77
|
+
const configured = config.host.toLowerCase();
|
|
78
|
+
const allowed = new Set([configured]);
|
|
79
|
+
if (isLoopback(configured)) {
|
|
80
|
+
allowed.add('localhost');
|
|
81
|
+
allowed.add('127.0.0.1');
|
|
82
|
+
allowed.add('::1');
|
|
83
|
+
}
|
|
84
|
+
return allowed.has(authority);
|
|
85
|
+
}
|
|
86
|
+
function createRequestGate(config, host) {
|
|
87
|
+
/**
|
|
88
|
+
* THE LIMITER IS PER-HANDLER STATE, in memory and unpersisted (AT-12).
|
|
89
|
+
*
|
|
90
|
+
* One limiter per bound endpoint, keyed by the authenticated client identity.
|
|
91
|
+
* Absent configuration means the DEFAULT policy applies, so the endpoint is
|
|
92
|
+
* bounded even when the operator configured nothing; `false` disables it; a
|
|
93
|
+
* policy overrides it. Construction validates the policy, so a broken
|
|
94
|
+
* configuration fails when the transport starts rather than mid-request.
|
|
95
|
+
*/
|
|
96
|
+
const effectivePolicy = config.rateLimit === false
|
|
97
|
+
? DEFAULT_RATE_LIMIT_POLICY
|
|
98
|
+
: (config.rateLimit ?? DEFAULT_RATE_LIMIT_POLICY);
|
|
99
|
+
const limiter = config.rateLimit === false
|
|
100
|
+
? undefined
|
|
101
|
+
: createRateLimiter(effectivePolicy);
|
|
75
102
|
return async (request) => {
|
|
103
|
+
// 0. DISCOVERY METADATA (RFC 8414/9728), when the OAuth scheme serves it.
|
|
104
|
+
// Metadata endpoints are the ONE thing served unauthenticated: a client must
|
|
105
|
+
// learn the issuer, audience and token endpoint BEFORE it can hold a token.
|
|
106
|
+
if (config.auth.scheme === 'oauth-resource-server') {
|
|
107
|
+
if (request.method === 'GET' && request.path === '/.well-known/oauth-protected-resource') {
|
|
108
|
+
return respond(200, config.auth.server.protectedResourceMetadata());
|
|
109
|
+
}
|
|
110
|
+
if (request.method === 'GET' && request.path === '/.well-known/oauth-authorization-server') {
|
|
111
|
+
return respond(200, config.auth.server.authorizationServerMetadata());
|
|
112
|
+
}
|
|
113
|
+
}
|
|
76
114
|
// 1. Loopback only (MCP-02, SEC-02). Checked against the SOCKET address, not a
|
|
77
115
|
// header: a header is caller-supplied, and trusting one would make the binding
|
|
78
116
|
// decorative.
|
|
79
117
|
if (!isLoopback(request.remoteAddress)) {
|
|
80
118
|
return respond(403, { error: 'this endpoint serves loopback clients only' });
|
|
81
119
|
}
|
|
120
|
+
if (!hostHeaderAllowed(request.headers.host, config)) {
|
|
121
|
+
return respond(403, { error: 'the Host header is not approved for this endpoint' });
|
|
122
|
+
}
|
|
82
123
|
// 2. Size limit (SEC-02), measured on the received bytes.
|
|
83
124
|
const bodyBytes = Buffer.byteLength(request.body, 'utf8');
|
|
84
125
|
if (bodyBytes > config.maxBodyBytes) {
|
|
85
126
|
// 413 rather than 400: the request may be perfectly well formed and merely
|
|
86
127
|
// too large, and a client needs to tell those apart.
|
|
128
|
+
/**
|
|
129
|
+
* NO `id` — the pinned schema neither permits nor requires one here.
|
|
130
|
+
*
|
|
131
|
+
* This wrote `id: null`, which `RequestId` (`type: ["string","integer"]`) REJECTS:
|
|
132
|
+
* `null` is not a string and not an integer. `JSONRPCErrorResponse` requires only
|
|
133
|
+
* `["error","jsonrpc"]`, so the id is optional — and when the body was too large to
|
|
134
|
+
* read there is no id to echo anyway. Omitting it is both valid and honest.
|
|
135
|
+
*/
|
|
87
136
|
return respond(413, {
|
|
88
137
|
jsonrpc: '2.0',
|
|
89
|
-
id: null,
|
|
90
138
|
error: { code: ERROR_CODES.invalidRequest, message: `the request body exceeds ${config.maxBodyBytes} bytes` },
|
|
91
139
|
});
|
|
92
140
|
}
|
|
@@ -115,15 +163,82 @@ export function createHttpHandler(config, host) {
|
|
|
115
163
|
message: 'authentication is required',
|
|
116
164
|
}), { 'www-authenticate': 'Bearer' });
|
|
117
165
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
166
|
+
/** The AUTHENTICATED client identity, resolved once and used for rate limiting. */
|
|
167
|
+
let clientId;
|
|
168
|
+
if (config.auth.scheme === 'static-token') {
|
|
169
|
+
let matched;
|
|
170
|
+
if (tokenMatches(presented, config.auth.token))
|
|
171
|
+
matched = config.auth.clientId;
|
|
172
|
+
// SEC-01: a GENERATED static-token credential authenticates through the
|
|
173
|
+
// durable store, carrying its OWN client identity into grants and receipts.
|
|
174
|
+
if (matched === undefined && config.auth.registry !== undefined) {
|
|
175
|
+
matched = config.auth.registry.bySecret(presented)?.clientId;
|
|
176
|
+
}
|
|
177
|
+
if (matched === undefined) {
|
|
178
|
+
// The message does not distinguish "unknown" from "expired": with a static
|
|
179
|
+
// token there is nothing to expire, and a more specific message would only
|
|
180
|
+
// help an attacker.
|
|
181
|
+
return respond(401, errorResponse(null, {
|
|
182
|
+
code: ERROR_CODES.invalidRequest,
|
|
183
|
+
message: 'the presented credential is not accepted',
|
|
184
|
+
}), { 'www-authenticate': 'Bearer' });
|
|
185
|
+
}
|
|
186
|
+
clientId = matched;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
const verdict = await config.auth.server.verify(presented, config.auth.requiredScope);
|
|
190
|
+
if (!verdict.ok) {
|
|
191
|
+
// An RFC 6750 challenge names WHY: invalid_token vs insufficient_scope are
|
|
192
|
+
// different client instructions, and flattening them would misdirect a
|
|
193
|
+
// client that could otherwise self-correct. The STATUS follows §3.1 the
|
|
194
|
+
// same way: invalid_request 400, invalid_token 401, insufficient_scope
|
|
195
|
+
// 403 (the F4 gap was one flat 401 for all three).
|
|
196
|
+
return respond(oauthErrorStatus(verdict.error), errorResponse(null, {
|
|
197
|
+
code: ERROR_CODES.invalidRequest,
|
|
198
|
+
message: verdict.error.description,
|
|
199
|
+
}), { 'www-authenticate': oauthChallenge(verdict.error) });
|
|
200
|
+
}
|
|
201
|
+
clientId = verdict.token.clientId;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 5.5. Per-client rate limiting (AT-12), AFTER authentication and BEFORE the
|
|
205
|
+
* body is examined.
|
|
206
|
+
*
|
|
207
|
+
* A refused request is answered here without parsing, so a rate-limited caller
|
|
208
|
+
* cannot make the transport pay body-processing cost per attempt, and the
|
|
209
|
+
* counter is keyed on the identity the transport just proved — never the
|
|
210
|
+
* remote address, which every loopback client shares. Unauthenticated and
|
|
211
|
+
* malformed-credential requests were already refused at step 5 WITHOUT moving
|
|
212
|
+
* a counter: counting them would turn the limiter into an authentication
|
|
213
|
+
* oracle, where a 429 after N attempts tells a caller whether a credential
|
|
214
|
+
* exists.
|
|
215
|
+
*
|
|
216
|
+
* The `id` is OMITTED, exactly as the oversize-body refusal omits it: the body
|
|
217
|
+
* was never examined, so there is no id to echo, and the pinned `RequestId`
|
|
218
|
+
* does not admit `null`. The envelope is still a conformant
|
|
219
|
+
* `JSONRPCErrorResponse`, and `Retry-After` names when the window opens.
|
|
220
|
+
*/
|
|
221
|
+
if (limiter !== undefined) {
|
|
222
|
+
const decision = limiter.admit(clientId);
|
|
223
|
+
if (!decision.admitted) {
|
|
224
|
+
return respond(429, errorResponse(null, {
|
|
225
|
+
code: ERROR_CODES.rateLimited,
|
|
226
|
+
message: `rate limit exceeded: at most ${effectivePolicy.maxRequests} `
|
|
227
|
+
+ `requests per client per ${Math.round(effectivePolicy.windowMs / 1000)}s window (AT-12)`,
|
|
228
|
+
data: { retryAfterMs: decision.retryAfterMs },
|
|
229
|
+
}), { 'retry-after': String(Math.ceil(decision.retryAfterMs / 1000)) });
|
|
230
|
+
}
|
|
126
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* 5.5. The per-client external grant (SEC-04), DERIVED here because the
|
|
234
|
+
* authentication verdict just happened: the grant is built from the verified
|
|
235
|
+
* client identity, the operator's grant configuration and the LIVE published
|
|
236
|
+
* surface — never from anything in the request body, which is what BASE-05
|
|
237
|
+
* forbids. The derived identity travels with the dispatch as CALLER identity,
|
|
238
|
+
* so the surface can authorize against the CLIENT's grant instead of the
|
|
239
|
+
* host's own interactive grant.
|
|
240
|
+
*/
|
|
241
|
+
const caller = resolveClientGrant(clientId, config.grants, host.listTools().map(tool => tool.name));
|
|
127
242
|
// 6. Parse, then check headers against the body (P26-05).
|
|
128
243
|
let parsed;
|
|
129
244
|
try {
|
|
@@ -136,6 +251,13 @@ export function createHttpHandler(config, host) {
|
|
|
136
251
|
return respond(400, errorResponse(null, { code: ERROR_CODES.invalidRequest, message: 'the body is not a JSON-RPC object' }));
|
|
137
252
|
}
|
|
138
253
|
const wire = parsed;
|
|
254
|
+
if (wire.jsonrpc !== '2.0' || typeof wire.method !== 'string'
|
|
255
|
+
|| (wire.id !== undefined && typeof wire.id !== 'string'
|
|
256
|
+
&& !(typeof wire.id === 'number' && Number.isInteger(wire.id)))) {
|
|
257
|
+
return respond(400, errorResponse(null, {
|
|
258
|
+
code: ERROR_CODES.invalidRequest, message: 'the body is not a JSON-RPC 2.0 request or notification',
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
139
261
|
// Built conditionally rather than with explicit `undefined` values, because
|
|
140
262
|
// `exactOptionalPropertyTypes` distinguishes "absent" from "present and
|
|
141
263
|
// undefined" — and so does the header check, which treats a missing header
|
|
@@ -155,14 +277,102 @@ export function createHttpHandler(config, host) {
|
|
|
155
277
|
// P26-05 names HTTP 400 and HeaderMismatch together.
|
|
156
278
|
return respond(400, errorResponse(wire.id ?? null, { code: ERROR_CODES.headerMismatch, message }));
|
|
157
279
|
}
|
|
158
|
-
//
|
|
159
|
-
|
|
280
|
+
// Pinned metadata failures are transport rejections (HTTP 400), not SDK
|
|
281
|
+
// dispatch errors under HTTP 200. Keep the shared wire validation, not a
|
|
282
|
+
// second business dispatcher. Optional clientInfo remains optional.
|
|
283
|
+
if (wire.id !== undefined && wire.method !== 'initialize') {
|
|
284
|
+
try {
|
|
285
|
+
const metadata = parseRequestMetadata(wire.params);
|
|
286
|
+
negotiateVersion(metadata);
|
|
287
|
+
const declared = metadata.clientCapabilities.required;
|
|
288
|
+
const required = Array.isArray(declared)
|
|
289
|
+
? declared.filter((name) => typeof name === 'string') : [];
|
|
290
|
+
const unsupported = unsupportedRequiredCapabilities(required);
|
|
291
|
+
if (unsupported.length > 0) {
|
|
292
|
+
throw new WireError(ERROR_CODES.unsupportedClientCapability, `the client requires unsupported capability: ${unsupported.join(', ')}`, { requiredCapabilities: Object.fromEntries(unsupported.map(name => [name, {}])) });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
if (!(error instanceof WireError))
|
|
297
|
+
throw error;
|
|
298
|
+
return respond(400, errorResponse(wire.id, error));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// 7. The OFFICIAL v2 server handles the protocol, not our handwritten dispatch.
|
|
302
|
+
// This finite response-description interface cannot carry open subscriptions.
|
|
303
|
+
// Refuse before creating a subscription rather than swallowing notifications.
|
|
304
|
+
if (wire.id !== undefined && wire.method === 'subscriptions/listen') {
|
|
305
|
+
return respond(404, errorResponse(wire.id ?? null, {
|
|
306
|
+
code: ERROR_CODES.methodNotFound,
|
|
307
|
+
message: 'this HTTP transport does not offer open subscription streams',
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
// Pinned Streamable HTTP defines NO client-to-server notifications;
|
|
311
|
+
// notifications/cancelled is stdio-only. 202/no body is mandatory only IF a
|
|
312
|
+
// notification is accepted. We implement no extension notification handlers,
|
|
313
|
+
// so explicitly refuse rather than silently dropping unsupported messages.
|
|
160
314
|
if (wire.id === undefined) {
|
|
161
|
-
return
|
|
315
|
+
return respond(400, errorResponse(null, {
|
|
316
|
+
code: ERROR_CODES.invalidRequest,
|
|
317
|
+
message: 'this stateless endpoint does not accept standalone notifications',
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
const headers = new Headers();
|
|
321
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
322
|
+
if (value !== undefined)
|
|
323
|
+
headers.set(name, value);
|
|
162
324
|
}
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
325
|
+
// Normalize sentinel values only AFTER shared validation. The pinned
|
|
326
|
+
// transport requires Mcp-Method; do not synthesize a missing header here or
|
|
327
|
+
// bypass assertHeadersMatch's required-header/mismatch rejection.
|
|
328
|
+
if (headerInput.method !== undefined)
|
|
329
|
+
headers.set('mcp-method', decodeHeaderValue(headerInput.method));
|
|
330
|
+
if (headerInput.protocolVersion !== undefined) {
|
|
331
|
+
headers.set('mcp-protocol-version', decodeHeaderValue(headerInput.protocolVersion));
|
|
332
|
+
}
|
|
333
|
+
// The SDK already decodes Mcp-Name correctly (including non-ASCII and
|
|
334
|
+
// literal sentinels), so preserve its original wire encoding here.
|
|
335
|
+
return { caller, token: presented, headers };
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Shared, guarded Fetch/stream core. The Node adapter's toNodeHandler is NOT
|
|
340
|
+
* used directly: its early 413 has a different wire envelope, it does not watch
|
|
341
|
+
* IncomingMessage.aborted, and its close/drain listeners are not removed.
|
|
342
|
+
* Use its official bounded Request conversion only AFTER our unchanged gate.
|
|
343
|
+
*/
|
|
344
|
+
export function createHttpStreamHandler(config, host) {
|
|
345
|
+
const gate = createRequestGate(config, host);
|
|
346
|
+
return async (request, signal) => {
|
|
347
|
+
const verdict = await gate(request);
|
|
348
|
+
if ('status' in verdict) {
|
|
349
|
+
return new Response(verdict.body, { status: verdict.status, headers: verdict.headers });
|
|
350
|
+
}
|
|
351
|
+
signal?.throwIfAborted();
|
|
352
|
+
const webRequest = await toWebRequest({
|
|
353
|
+
method: request.method,
|
|
354
|
+
url: config.path,
|
|
355
|
+
// Host was validated already; never let an untrusted authority affect URL
|
|
356
|
+
// construction. Keep the actual normalized headers on the Request below.
|
|
357
|
+
headers: { host: 'localhost' },
|
|
358
|
+
async *[Symbol.asyncIterator]() { yield Buffer.from(request.body, 'utf8'); },
|
|
359
|
+
}, undefined, {
|
|
360
|
+
maxRequestBodySize: config.maxBodyBytes,
|
|
361
|
+
...signal === undefined ? {} : { signal },
|
|
362
|
+
});
|
|
363
|
+
return serveSdkV2Request(host, verdict.caller, new Request(webRequest, { headers: verdict.headers }), verdict.token);
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/** Finite, materializing test facade; production sockets use the stream core. */
|
|
367
|
+
export function createHttpHandler(config, host) {
|
|
368
|
+
const stream = createHttpStreamHandler(config, host);
|
|
369
|
+
return async (request) => {
|
|
370
|
+
const response = await stream(request);
|
|
371
|
+
return {
|
|
372
|
+
status: response.status,
|
|
373
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
374
|
+
body: await response.text(),
|
|
375
|
+
};
|
|
166
376
|
};
|
|
167
377
|
}
|
|
168
378
|
/** Extract a bearer token from an Authorization header. */
|
|
@@ -175,13 +385,16 @@ function bearerOf(header) {
|
|
|
175
385
|
/**
|
|
176
386
|
* Whether this transport may claim OAuth conformance.
|
|
177
387
|
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
388
|
+
* The static-token scheme may not; the oauth-resource-server scheme may, because
|
|
389
|
+
* it verifies HS256 credentials, audience, expiry and scope against the embedded
|
|
390
|
+
* server and answers RFC 6750 challenges — a conformant subset, not an invention.
|
|
391
|
+
* The claim is per-DEPLOYMENT, which is what a status report needs: a host using
|
|
392
|
+
* static tokens must still report false.
|
|
393
|
+
* @param auth - the live auth configuration.
|
|
394
|
+
* @returns whether the live scheme may claim the OAuth 2.1 resource-server profile.
|
|
182
395
|
*/
|
|
183
|
-
export function claimsOAuthConformance() {
|
|
184
|
-
return
|
|
396
|
+
export function claimsOAuthConformance(auth) {
|
|
397
|
+
return auth.scheme === 'oauth-resource-server';
|
|
185
398
|
}
|
|
186
399
|
/**
|
|
187
400
|
* The authentication profile this transport reports.
|
|
@@ -189,17 +402,79 @@ export function claimsOAuthConformance() {
|
|
|
189
402
|
* A machine-readable statement of what is actually implemented, so a client or an
|
|
190
403
|
* operator can tell custom authentication from the OAuth profile without reading
|
|
191
404
|
* the source.
|
|
405
|
+
* @param auth - the live auth configuration.
|
|
192
406
|
* @returns the auth profile description.
|
|
193
407
|
*/
|
|
194
|
-
export function authProfile() {
|
|
408
|
+
export function authProfile(auth) {
|
|
409
|
+
if (auth.scheme === 'static-token') {
|
|
410
|
+
return {
|
|
411
|
+
scheme: 'static-token',
|
|
412
|
+
// Stated explicitly rather than left to inference.
|
|
413
|
+
oauth21ResourceServer: false,
|
|
414
|
+
perClientRevocable: true,
|
|
415
|
+
note: 'Custom authentication. The OAuth 2.1 resource-server profile is not used and is not claimed.',
|
|
416
|
+
};
|
|
417
|
+
}
|
|
195
418
|
return {
|
|
196
|
-
scheme: '
|
|
197
|
-
|
|
198
|
-
oauth21ResourceServer: false,
|
|
419
|
+
scheme: 'oauth-resource-server',
|
|
420
|
+
oauth21ResourceServer: true,
|
|
199
421
|
perClientRevocable: true,
|
|
200
|
-
|
|
422
|
+
issuer: auth.server.authorizationServerMetadata().issuer,
|
|
423
|
+
audience: auth.server.protectedResourceMetadata().resource,
|
|
424
|
+
note: 'OAuth 2.1 resource-server profile via the embedded local authorization server: '
|
|
425
|
+
+ 'HS256 credentials, RFC 6750 challenges, audience/expiry/scope validation, per-client revocation.',
|
|
201
426
|
};
|
|
202
427
|
}
|
|
428
|
+
/** Wait for capacity without stranding a promise on a disconnected socket. */
|
|
429
|
+
function waitForDrain(outgoing, signal) {
|
|
430
|
+
return new Promise((resolve, reject) => {
|
|
431
|
+
const cleanup = () => {
|
|
432
|
+
outgoing.off('drain', onDrain);
|
|
433
|
+
outgoing.off('error', onError);
|
|
434
|
+
outgoing.off('close', onAbort);
|
|
435
|
+
signal.removeEventListener('abort', onAbort);
|
|
436
|
+
};
|
|
437
|
+
const onDrain = () => { cleanup(); resolve(); };
|
|
438
|
+
const onError = (error) => { cleanup(); reject(error); };
|
|
439
|
+
const onAbort = () => { cleanup(); reject(signal.reason ?? new Error('HTTP response closed')); };
|
|
440
|
+
outgoing.once('drain', onDrain);
|
|
441
|
+
outgoing.once('error', onError);
|
|
442
|
+
outgoing.once('close', onAbort);
|
|
443
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
444
|
+
if (signal.aborted || outgoing.destroyed)
|
|
445
|
+
onAbort();
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
/** Pump bytes, never text-materialize SSE, and cancel even a blocked read. */
|
|
449
|
+
async function writeStreamResponse(response, outgoing, signal) {
|
|
450
|
+
const reader = response.body?.getReader();
|
|
451
|
+
const onAbort = () => { void reader?.cancel(signal.reason).catch(() => { }); };
|
|
452
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
453
|
+
let drained = false;
|
|
454
|
+
try {
|
|
455
|
+
signal.throwIfAborted();
|
|
456
|
+
outgoing.writeHead(response.status, Object.fromEntries(response.headers.entries()));
|
|
457
|
+
outgoing.flushHeaders();
|
|
458
|
+
if (reader !== undefined) {
|
|
459
|
+
while (true) {
|
|
460
|
+
const next = await reader.read();
|
|
461
|
+
signal.throwIfAborted();
|
|
462
|
+
if (next.done)
|
|
463
|
+
break;
|
|
464
|
+
if (!outgoing.write(next.value))
|
|
465
|
+
await waitForDrain(outgoing, signal);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
drained = true;
|
|
469
|
+
outgoing.end();
|
|
470
|
+
}
|
|
471
|
+
finally {
|
|
472
|
+
signal.removeEventListener('abort', onAbort);
|
|
473
|
+
if (!drained)
|
|
474
|
+
await reader?.cancel(signal.reason).catch(() => { });
|
|
475
|
+
reader?.releaseLock();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
203
478
|
/**
|
|
204
479
|
* Bind the endpoint on loopback.
|
|
205
480
|
*
|
|
@@ -220,38 +495,87 @@ export async function startHttpServer(config, host) {
|
|
|
220
495
|
+ 'A routable bind address would expose inference to the network.');
|
|
221
496
|
}
|
|
222
497
|
const { createServer } = await import('node:http');
|
|
223
|
-
const handler =
|
|
498
|
+
const handler = createHttpStreamHandler(config, host);
|
|
499
|
+
const active = new Set();
|
|
224
500
|
const server = createServer((incoming, outgoing) => {
|
|
501
|
+
const abort = new AbortController();
|
|
502
|
+
active.add(abort);
|
|
503
|
+
const onAborted = () => { abort.abort(new Error('HTTP client disconnected')); };
|
|
504
|
+
const onClose = () => { if (!outgoing.writableFinished)
|
|
505
|
+
onAborted(); };
|
|
506
|
+
const cleanup = () => {
|
|
507
|
+
incoming.off('aborted', onAborted);
|
|
508
|
+
incoming.off('error', onAborted);
|
|
509
|
+
outgoing.off('close', onClose);
|
|
510
|
+
outgoing.off('error', onAborted);
|
|
511
|
+
active.delete(abort);
|
|
512
|
+
};
|
|
513
|
+
incoming.once('aborted', onAborted);
|
|
514
|
+
incoming.once('error', onAborted);
|
|
515
|
+
outgoing.once('close', onClose);
|
|
516
|
+
outgoing.once('error', onAborted);
|
|
225
517
|
const chunks = [];
|
|
226
518
|
let received = 0;
|
|
519
|
+
let refused = false;
|
|
227
520
|
incoming.on('data', (chunk) => {
|
|
521
|
+
if (refused || abort.signal.aborted)
|
|
522
|
+
return;
|
|
228
523
|
received += chunk.byteLength;
|
|
229
|
-
//
|
|
230
|
-
// unbounded body in order to measure it defeats the limit.
|
|
524
|
+
// Bound DURING receipt, and free already buffered chunks on refusal.
|
|
231
525
|
if (received > config.maxBodyBytes) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
526
|
+
refused = true;
|
|
527
|
+
chunks.length = 0;
|
|
528
|
+
outgoing.writeHead(413, { 'content-type': 'application/json', connection: 'close' });
|
|
529
|
+
outgoing.end(JSON.stringify({
|
|
530
|
+
jsonrpc: '2.0',
|
|
531
|
+
error: {
|
|
532
|
+
code: ERROR_CODES.invalidRequest,
|
|
533
|
+
message: `the request body exceeds ${config.maxBodyBytes} bytes`,
|
|
534
|
+
},
|
|
535
|
+
}));
|
|
536
|
+
cleanup();
|
|
235
537
|
return;
|
|
236
538
|
}
|
|
237
539
|
chunks.push(chunk);
|
|
238
540
|
});
|
|
239
|
-
incoming.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
541
|
+
incoming.once('end', () => {
|
|
542
|
+
if (refused || abort.signal.aborted) {
|
|
543
|
+
cleanup();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
547
|
+
chunks.length = 0;
|
|
548
|
+
void (async () => {
|
|
549
|
+
try {
|
|
550
|
+
const response = await handler({
|
|
551
|
+
method: incoming.method ?? 'POST',
|
|
552
|
+
path: (incoming.url ?? '/').split('?')[0],
|
|
553
|
+
headers: incoming.headers,
|
|
554
|
+
body,
|
|
555
|
+
remoteAddress: incoming.socket.remoteAddress ?? '',
|
|
556
|
+
}, abort.signal);
|
|
557
|
+
await writeStreamResponse(response, outgoing, abort.signal);
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
if (abort.signal.aborted || outgoing.destroyed)
|
|
561
|
+
return;
|
|
562
|
+
if (outgoing.headersSent) {
|
|
563
|
+
// A partial SSE response cannot be replaced by a JSON error.
|
|
564
|
+
outgoing.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
outgoing.writeHead(500, { 'content-type': 'application/json' });
|
|
568
|
+
outgoing.end(JSON.stringify({ error: error instanceof Error ? error.message : 'internal error' }));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
finally {
|
|
572
|
+
cleanup();
|
|
573
|
+
}
|
|
574
|
+
})();
|
|
254
575
|
});
|
|
576
|
+
// No 'end' event is guaranteed on a truncated upload.
|
|
577
|
+
incoming.once('close', () => { if (!incoming.complete)
|
|
578
|
+
cleanup(); });
|
|
255
579
|
});
|
|
256
580
|
await new Promise((resolve, reject) => {
|
|
257
581
|
server.once('error', reject);
|
|
@@ -265,8 +589,12 @@ export async function startHttpServer(config, host) {
|
|
|
265
589
|
port,
|
|
266
590
|
url: `http://${config.host}:${port}${config.path}`,
|
|
267
591
|
async close() {
|
|
592
|
+
for (const abort of active)
|
|
593
|
+
abort.abort(new Error('HTTP endpoint closed'));
|
|
268
594
|
await new Promise((resolve, reject) => {
|
|
269
595
|
server.close(error => error === undefined ? resolve() : reject(error));
|
|
596
|
+
// server.close alone waits indefinitely for active/keep-alive sockets.
|
|
597
|
+
server.closeAllConnections();
|
|
270
598
|
});
|
|
271
599
|
},
|
|
272
600
|
};
|