@deepseek-ai/dsh-web-fetch-http 0.1.2-alpha.5 → 0.1.3-alpha.2
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/lib/index.js +61 -9
- package/lib/types/network.d.ts +38 -5
- package/package.json +7 -5
package/lib/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
2
|
import { WebError } from "@deepseek-ai/dsh-web";
|
|
3
3
|
import { deadline, timeoutOf } from "@deepseek-ai/dsh-timeout";
|
|
4
|
+
import { proxyRouteFor } from "@deepseek-ai/dsh-http-proxy";
|
|
4
5
|
import { lookup } from "node:dns/promises";
|
|
5
6
|
import { isIP } from "node:net";
|
|
6
7
|
import ipaddr from "ipaddr.js";
|
|
@@ -121,14 +122,33 @@ function embeddedIpv4Address(bytes, prefixLength) {
|
|
|
121
122
|
return [...bytes.slice(prefixBytes, prefixBytes + beforeReservedOctet), ...bytes.slice(9, 13 - beforeReservedOctet)].join(".");
|
|
122
123
|
}
|
|
123
124
|
/**
|
|
124
|
-
*
|
|
125
|
-
* validated address set. The URL hostname remains intact for HTTP Host and TLS SNI.
|
|
125
|
+
* Whether a hostname is an IP literal that {@link resolvePublicAddresses} would refuse.
|
|
126
126
|
*
|
|
127
|
-
*
|
|
127
|
+
* A proxied hop skips those checks because the proxy resolves the origin, but a literal needs no
|
|
128
|
+
* resolution: the address is already stated, and handing it to a proxy running on this machine
|
|
129
|
+
* would reach exactly the loopback or private service the checks exist to keep out of reach.
|
|
130
|
+
*
|
|
131
|
+
* @param hostname - a URL's hostname, bracketed or not.
|
|
132
|
+
* @returns true when the host is a literal address no request may be sent to.
|
|
133
|
+
*/
|
|
134
|
+
function isNonPublicIpLiteral(hostname) {
|
|
135
|
+
const unbracketed = stripIpv6Brackets(hostname);
|
|
136
|
+
return isIP(unbracketed) !== 0 && !isPublicIpAddress(unbracketed);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Fetch through an agent whose lookup callback returns only the already validated address set. The
|
|
140
|
+
* URL hostname remains intact for HTTP Host and TLS SNI.
|
|
141
|
+
*
|
|
142
|
+
* The agent is this request's own because the address set is: pinning is how this package refuses a
|
|
143
|
+
* DNS answer that changes between validation and connection, and it may not apply process-wide —
|
|
144
|
+
* an operator-configured MCP server or model endpoint on loopback is a supported destination, and
|
|
145
|
+
* only the URLs this tool fetches are the model's to choose.
|
|
146
|
+
*
|
|
147
|
+
* @param url - validated HTTP(S) URL the policy does not route through a proxy.
|
|
128
148
|
* @param addresses - public addresses returned by {@link resolvePublicAddresses}.
|
|
129
149
|
* @param headers - request headers.
|
|
130
150
|
* @param signal - request and body-read cancellation signal.
|
|
131
|
-
* @returns a response plus the
|
|
151
|
+
* @returns a response plus the disposer its consumer must call.
|
|
132
152
|
*/
|
|
133
153
|
async function requestPinned(url, addresses, headers, signal) {
|
|
134
154
|
const { Agent, fetch } = await import("undici");
|
|
@@ -154,10 +174,39 @@ async function requestPinned(url, addresses, headers, signal) {
|
|
|
154
174
|
throw error;
|
|
155
175
|
}
|
|
156
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Fetch through the dispatcher the proxy policy already installed, letting the proxy resolve the
|
|
179
|
+
* origin.
|
|
180
|
+
*
|
|
181
|
+
* No address set is pinned because none exists to pin: the proxy performs the lookup, and a
|
|
182
|
+
* connection pinned to a locally resolved address would reach the origin directly and defeat the
|
|
183
|
+
* proxy. The dispatcher is the process-wide one, so hops share its connection pool and no caller
|
|
184
|
+
* closes it.
|
|
185
|
+
*
|
|
186
|
+
* @param dispatcher - the route's dispatcher, from `proxyRouteFor`.
|
|
187
|
+
* @param url - validated HTTP(S) URL the policy routes through a proxy.
|
|
188
|
+
* @param headers - request headers.
|
|
189
|
+
* @param signal - request and body-read cancellation signal.
|
|
190
|
+
* @returns a response plus a disposer that releases nothing, so both paths close alike.
|
|
191
|
+
*/
|
|
192
|
+
async function requestVia(dispatcher, url, headers, signal) {
|
|
193
|
+
const { fetch } = await import("undici");
|
|
194
|
+
return {
|
|
195
|
+
response: await fetch(url, {
|
|
196
|
+
method: "GET",
|
|
197
|
+
redirect: "manual",
|
|
198
|
+
headers,
|
|
199
|
+
signal,
|
|
200
|
+
dispatcher
|
|
201
|
+
}),
|
|
202
|
+
close: () => Promise.resolve()
|
|
203
|
+
};
|
|
204
|
+
}
|
|
157
205
|
/** Production network operations kept as an object so provider tests can replace resolution only. */
|
|
158
206
|
const publicHttpNetwork = {
|
|
159
207
|
resolve: resolvePublicAddresses,
|
|
160
|
-
request: requestPinned
|
|
208
|
+
request: requestPinned,
|
|
209
|
+
requestVia
|
|
161
210
|
};
|
|
162
211
|
/**
|
|
163
212
|
* Build the connector lookup that serves a fixed validated answer set.
|
|
@@ -444,12 +493,15 @@ var HttpFetchProvider = class {
|
|
|
444
493
|
}
|
|
445
494
|
}
|
|
446
495
|
async requestOnce(url, signal) {
|
|
496
|
+
const headers = {
|
|
497
|
+
"user-agent": this.limits.userAgent,
|
|
498
|
+
"accept": "text/html,application/xhtml+xml,text/*;q=0.9,application/json;q=0.8"
|
|
499
|
+
};
|
|
447
500
|
try {
|
|
501
|
+
const route = proxyRouteFor(url);
|
|
502
|
+
if (route.proxied && !isNonPublicIpLiteral(url.hostname)) return await publicHttpNetwork.requestVia(route.dispatcher, url, headers, signal);
|
|
448
503
|
const addresses = await this.resolveAddresses(url.hostname, signal);
|
|
449
|
-
return await publicHttpNetwork.request(url, addresses,
|
|
450
|
-
"user-agent": this.limits.userAgent,
|
|
451
|
-
"accept": "text/html,application/xhtml+xml,text/*;q=0.9,application/json;q=0.8"
|
|
452
|
-
}, signal);
|
|
504
|
+
return await publicHttpNetwork.request(url, addresses, headers, signal);
|
|
453
505
|
} catch (error) {
|
|
454
506
|
if (error instanceof WebError) throw error;
|
|
455
507
|
throw translateAbortOrNetwork(error, signal);
|
package/lib/types/network.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @module @deepseek-ai/dsh-web-fetch-http/network
|
|
7
7
|
*/
|
|
8
8
|
import type { LookupAddress, LookupOptions } from 'node:dns';
|
|
9
|
-
import type { Response } from 'undici';
|
|
9
|
+
import type { Dispatcher, Response } from 'undici';
|
|
10
10
|
/** One address resolved and retained for the subsequent pinned connection. */
|
|
11
11
|
export interface PublicAddress {
|
|
12
12
|
/** Canonical textual IPv4 or IPv6 address. */
|
|
@@ -46,20 +46,53 @@ export declare function isPublicIpAddress(input: string): boolean;
|
|
|
46
46
|
*/
|
|
47
47
|
export declare function resolvePublicAddresses(hostname: string, signal: AbortSignal, resolver?: AddressResolver): Promise<PublicAddress[]>;
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
50
|
-
* validated address set. The URL hostname remains intact for HTTP Host and TLS SNI.
|
|
49
|
+
* Whether a hostname is an IP literal that {@link resolvePublicAddresses} would refuse.
|
|
51
50
|
*
|
|
52
|
-
*
|
|
51
|
+
* A proxied hop skips those checks because the proxy resolves the origin, but a literal needs no
|
|
52
|
+
* resolution: the address is already stated, and handing it to a proxy running on this machine
|
|
53
|
+
* would reach exactly the loopback or private service the checks exist to keep out of reach.
|
|
54
|
+
*
|
|
55
|
+
* @param hostname - a URL's hostname, bracketed or not.
|
|
56
|
+
* @returns true when the host is a literal address no request may be sent to.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isNonPublicIpLiteral(hostname: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Fetch through an agent whose lookup callback returns only the already validated address set. The
|
|
61
|
+
* URL hostname remains intact for HTTP Host and TLS SNI.
|
|
62
|
+
*
|
|
63
|
+
* The agent is this request's own because the address set is: pinning is how this package refuses a
|
|
64
|
+
* DNS answer that changes between validation and connection, and it may not apply process-wide —
|
|
65
|
+
* an operator-configured MCP server or model endpoint on loopback is a supported destination, and
|
|
66
|
+
* only the URLs this tool fetches are the model's to choose.
|
|
67
|
+
*
|
|
68
|
+
* @param url - validated HTTP(S) URL the policy does not route through a proxy.
|
|
53
69
|
* @param addresses - public addresses returned by {@link resolvePublicAddresses}.
|
|
54
70
|
* @param headers - request headers.
|
|
55
71
|
* @param signal - request and body-read cancellation signal.
|
|
56
|
-
* @returns a response plus the
|
|
72
|
+
* @returns a response plus the disposer its consumer must call.
|
|
57
73
|
*/
|
|
58
74
|
export declare function requestPinned(url: URL, addresses: readonly PublicAddress[], headers: Record<string, string>, signal: AbortSignal): Promise<PinnedResponse>;
|
|
75
|
+
/**
|
|
76
|
+
* Fetch through the dispatcher the proxy policy already installed, letting the proxy resolve the
|
|
77
|
+
* origin.
|
|
78
|
+
*
|
|
79
|
+
* No address set is pinned because none exists to pin: the proxy performs the lookup, and a
|
|
80
|
+
* connection pinned to a locally resolved address would reach the origin directly and defeat the
|
|
81
|
+
* proxy. The dispatcher is the process-wide one, so hops share its connection pool and no caller
|
|
82
|
+
* closes it.
|
|
83
|
+
*
|
|
84
|
+
* @param dispatcher - the route's dispatcher, from `proxyRouteFor`.
|
|
85
|
+
* @param url - validated HTTP(S) URL the policy routes through a proxy.
|
|
86
|
+
* @param headers - request headers.
|
|
87
|
+
* @param signal - request and body-read cancellation signal.
|
|
88
|
+
* @returns a response plus a disposer that releases nothing, so both paths close alike.
|
|
89
|
+
*/
|
|
90
|
+
export declare function requestVia(dispatcher: Dispatcher, url: URL, headers: Record<string, string>, signal: AbortSignal): Promise<PinnedResponse>;
|
|
59
91
|
/** Production network operations kept as an object so provider tests can replace resolution only. */
|
|
60
92
|
export declare const publicHttpNetwork: {
|
|
61
93
|
resolve: typeof resolvePublicAddresses;
|
|
62
94
|
request: typeof requestPinned;
|
|
95
|
+
requestVia: typeof requestVia;
|
|
63
96
|
};
|
|
64
97
|
type LookupCallback = (error: NodeJS.ErrnoException | null, address: string | LookupAddress[], family?: number) => void;
|
|
65
98
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-web-fetch-http",
|
|
3
3
|
"description": "Anonymous public HTTP(S) fetch provider for the DeepSeek Harness web capability seam (ctx.web)",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
31
|
-
"@deepseek-ai/dsh-timeout": "^0.1.
|
|
32
|
-
"@deepseek-ai/dsh-
|
|
31
|
+
"@deepseek-ai/dsh-timeout": "^0.1.3-alpha.2",
|
|
32
|
+
"@deepseek-ai/dsh-http-proxy": "^0.1.3-alpha.2",
|
|
33
|
+
"@deepseek-ai/dsh-web": "^0.1.3-alpha.2"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
36
|
"ipaddr.js": "^2.5.0",
|
|
@@ -38,7 +39,8 @@
|
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
41
|
-
"@deepseek-ai/dsh-timeout": "^0.1.
|
|
42
|
-
"@deepseek-ai/dsh-web": "^0.1.
|
|
42
|
+
"@deepseek-ai/dsh-timeout": "^0.1.3-alpha.2",
|
|
43
|
+
"@deepseek-ai/dsh-web": "^0.1.3-alpha.2",
|
|
44
|
+
"@deepseek-ai/dsh-http-proxy": "^0.1.3-alpha.2"
|
|
43
45
|
}
|
|
44
46
|
}
|