@opengeni/network 0.1.1 → 0.2.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/README.md +10 -9
- package/dist/index.d.ts +34 -28
- package/dist/index.js +151 -20
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +190 -18
package/README.md
CHANGED
|
@@ -26,12 +26,13 @@ calling `pinnedFetch`; the policy resolution and pinned Agent are the final
|
|
|
26
26
|
network boundary. Callers must not preflight with one fetch and then call a
|
|
27
27
|
second global fetch.
|
|
28
28
|
|
|
29
|
-
The
|
|
30
|
-
`undici/index.js` entrypoint
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
The pinned transport uses Undici's `request()` implementation from its explicit
|
|
30
|
+
`undici/index.js` entrypoint and adapts the result to a web `Response`. This
|
|
31
|
+
avoids Bun/Undici `fetch()` body-stream interoperability stalls while preserving
|
|
32
|
+
the vetted per-request `Agent`. The separately exported `undiciFetch` remains a
|
|
33
|
+
fetch-compatible adapter for callers that do not use `pinnedFetch`. Bun's bare
|
|
34
|
+
`undici` specifier resolves to a compatibility shim whose `Agent` does not expose
|
|
35
|
+
the required dispatcher methods, and Bun's native `fetch` does not provide a
|
|
36
|
+
portable guarantee that an Undici `dispatcher` is honored. This package does not
|
|
37
|
+
follow redirects; each caller must make a manual redirect decision and call the
|
|
38
|
+
transport again so every hop is independently resolved and pinned.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,50 +1,58 @@
|
|
|
1
|
-
type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
2
|
-
type DnsAddress = {
|
|
1
|
+
export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
2
|
+
export type DnsAddress = {
|
|
3
3
|
address: string;
|
|
4
4
|
family: 4 | 6;
|
|
5
5
|
};
|
|
6
|
-
type DnsLookup = (hostname: string) => Promise<readonly DnsAddress[]>;
|
|
7
|
-
type OutboundNetworkSettings = {
|
|
6
|
+
export type DnsLookup = (hostname: string) => Promise<readonly DnsAddress[]>;
|
|
7
|
+
export type OutboundNetworkSettings = {
|
|
8
8
|
environment: string;
|
|
9
9
|
integrationsAllowPrivateNetworkTargets: boolean;
|
|
10
10
|
};
|
|
11
|
-
type PinnedDestination = {
|
|
11
|
+
export type PinnedDestination = {
|
|
12
12
|
url: URL;
|
|
13
13
|
hostname: string;
|
|
14
14
|
addresses: readonly DnsAddress[];
|
|
15
15
|
};
|
|
16
|
-
type DispatcherLifecycle = {
|
|
16
|
+
export type DispatcherLifecycle = {
|
|
17
17
|
/** The raw dispatcher handed to fetch; omitted for test-only lifecycle fakes. */
|
|
18
18
|
dispatcher?: unknown;
|
|
19
19
|
close: () => Promise<void> | void;
|
|
20
20
|
destroy: (error?: unknown) => Promise<void> | void;
|
|
21
21
|
};
|
|
22
|
-
type PinnedFetchOptions = {
|
|
22
|
+
export type PinnedFetchOptions = {
|
|
23
23
|
fetchImpl?: FetchLike;
|
|
24
24
|
dnsLookup?: DnsLookup;
|
|
25
25
|
agentFactory?: (addresses: readonly DnsAddress[]) => DispatcherLifecycle;
|
|
26
26
|
label?: string;
|
|
27
27
|
requireHttpsOutsideLocalTest?: boolean;
|
|
28
28
|
};
|
|
29
|
-
declare const OAUTH_MAX_RESPONSE_BYTES: number;
|
|
30
|
-
type HttpUrlValidationOptions = {
|
|
29
|
+
export declare const OAUTH_MAX_RESPONSE_BYTES: number;
|
|
30
|
+
export type HttpUrlValidationOptions = {
|
|
31
31
|
allowLoopbackHttp?: boolean;
|
|
32
32
|
label?: string;
|
|
33
33
|
};
|
|
34
34
|
/** Validate a protocol endpoint before it is persisted, returned, or opened. */
|
|
35
|
-
declare function validateHttpUrl(rawUrl: string, options?: HttpUrlValidationOptions): string;
|
|
35
|
+
export declare function validateHttpUrl(rawUrl: string, options?: HttpUrlValidationOptions): string;
|
|
36
36
|
/**
|
|
37
37
|
* Raised when a response cannot be safely consumed within its caller's byte
|
|
38
38
|
* budget. The error intentionally contains no response bytes or provider
|
|
39
39
|
* message: these readers are used on credential-bearing paths.
|
|
40
40
|
*/
|
|
41
|
-
declare class ResponseBodyLimitError extends Error {
|
|
41
|
+
export declare class ResponseBodyLimitError extends Error {
|
|
42
42
|
readonly label: string;
|
|
43
43
|
readonly actualBytes: number;
|
|
44
44
|
readonly maxBytes: number;
|
|
45
45
|
readonly reason: "declared_length" | "stream_overflow" | "invalid_content_length";
|
|
46
46
|
constructor(label: string, actualBytes: number, maxBytes: number, reason: "declared_length" | "stream_overflow" | "invalid_content_length");
|
|
47
47
|
}
|
|
48
|
+
/** Raised when a caller-supplied absolute request deadline expires. */
|
|
49
|
+
export declare class RequestDeadlineError extends Error {
|
|
50
|
+
readonly label: string;
|
|
51
|
+
constructor(label: string);
|
|
52
|
+
}
|
|
53
|
+
export type BoundedResponseReadOptions = {
|
|
54
|
+
signal?: AbortSignal;
|
|
55
|
+
};
|
|
48
56
|
/**
|
|
49
57
|
* Read a response through its stream with a hard byte ceiling.
|
|
50
58
|
*
|
|
@@ -53,42 +61,40 @@ declare class ResponseBodyLimitError extends Error {
|
|
|
53
61
|
* the stream and rejects. Cancellation is important because pinnedFetch owns a
|
|
54
62
|
* per-response dispatcher which must be closed on every exit path.
|
|
55
63
|
*/
|
|
56
|
-
declare function readResponseBodyBounded(response: Response, maxBytes: number, label: string): Promise<Uint8Array>;
|
|
57
|
-
declare function readResponseTextBounded(response: Response, maxBytes: number, label: string): Promise<string>;
|
|
58
|
-
declare function readResponseJsonBounded<T = unknown>(response: Response, maxBytes: number, label: string): Promise<T>;
|
|
59
|
-
type ResolvePinnedDestinationOptions = {
|
|
64
|
+
export declare function readResponseBodyBounded(response: Response, maxBytes: number, label: string, options?: BoundedResponseReadOptions): Promise<Uint8Array>;
|
|
65
|
+
export declare function readResponseTextBounded(response: Response, maxBytes: number, label: string, options?: BoundedResponseReadOptions): Promise<string>;
|
|
66
|
+
export declare function readResponseJsonBounded<T = unknown>(response: Response, maxBytes: number, label: string, options?: BoundedResponseReadOptions): Promise<T>;
|
|
67
|
+
export type ResolvePinnedDestinationOptions = {
|
|
60
68
|
dnsLookup?: DnsLookup;
|
|
61
69
|
label?: string;
|
|
62
70
|
requireHttpsOutsideLocalTest?: boolean;
|
|
63
71
|
};
|
|
64
|
-
type DestinationPolicyReason = "invalid_url" | "unsupported_protocol" | "https_required" | "dns_failed" | "dns_empty" | "invalid_dns_answer" | "private_or_special_use";
|
|
65
|
-
declare class DestinationPolicyError extends Error {
|
|
72
|
+
export type DestinationPolicyReason = "invalid_url" | "unsupported_protocol" | "https_required" | "dns_failed" | "dns_empty" | "invalid_dns_answer" | "private_or_special_use";
|
|
73
|
+
export declare class DestinationPolicyError extends Error {
|
|
66
74
|
readonly reason: DestinationPolicyReason;
|
|
67
75
|
constructor(reason: DestinationPolicyReason, message: string);
|
|
68
76
|
}
|
|
69
77
|
/** Undici fetch used by the pinned transport; Bun callers should prefer this over native fetch. */
|
|
70
|
-
declare const undiciFetch: FetchLike;
|
|
78
|
+
export declare const undiciFetch: FetchLike;
|
|
71
79
|
/**
|
|
72
80
|
* Resolve and policy-check one destination. The returned address set is the
|
|
73
81
|
* complete DNS answer that the caller is allowed to use; the transport never
|
|
74
82
|
* performs a second resolver call.
|
|
75
83
|
*/
|
|
76
|
-
declare function resolvePinnedDestination(rawUrl: string | URL, settings: OutboundNetworkSettings, options?: ResolvePinnedDestinationOptions): Promise<PinnedDestination>;
|
|
84
|
+
export declare function resolvePinnedDestination(rawUrl: string | URL, settings: OutboundNetworkSettings, options?: ResolvePinnedDestinationOptions): Promise<PinnedDestination>;
|
|
77
85
|
/**
|
|
78
86
|
* Fetch through a dispatcher whose lookup is pinned to the result of exactly
|
|
79
|
-
* one policy resolution. The response body owns the agent until
|
|
80
|
-
* cancellation, or stream failure.
|
|
87
|
+
* one policy resolution. The response body owns the one-shot agent until
|
|
88
|
+
* completion, cancellation, or stream failure.
|
|
81
89
|
*/
|
|
82
|
-
declare function pinnedFetch(input: string | URL | Request, init: RequestInit | undefined, settings: OutboundNetworkSettings, options?: PinnedFetchOptions): Promise<Response>;
|
|
83
|
-
declare function isLocalTestEnvironment(environment: string): boolean;
|
|
90
|
+
export declare function pinnedFetch(input: string | URL | Request, init: RequestInit | undefined, settings: OutboundNetworkSettings, options?: PinnedFetchOptions): Promise<Response>;
|
|
91
|
+
export declare function isLocalTestEnvironment(environment: string): boolean;
|
|
84
92
|
/** Return true for malformed, non-IPv4, and non-IPv6 address strings. */
|
|
85
|
-
declare function isInvalidAddress(address: string): boolean;
|
|
93
|
+
export declare function isInvalidAddress(address: string): boolean;
|
|
86
94
|
/**
|
|
87
95
|
* Classify private, reserved, documentation, benchmark, multicast, and other
|
|
88
96
|
* special-use answers. IPv4-mapped IPv6 addresses are classified through their
|
|
89
97
|
* embedded IPv4 value.
|
|
90
98
|
*/
|
|
91
|
-
declare function isNonPublicAddress(address: string): boolean;
|
|
92
|
-
declare const isPrivateAddress: typeof isNonPublicAddress;
|
|
93
|
-
|
|
94
|
-
export { DestinationPolicyError, type DestinationPolicyReason, type DispatcherLifecycle, type DnsAddress, type DnsLookup, type FetchLike, type HttpUrlValidationOptions, OAUTH_MAX_RESPONSE_BYTES, type OutboundNetworkSettings, type PinnedDestination, type PinnedFetchOptions, type ResolvePinnedDestinationOptions, ResponseBodyLimitError, isInvalidAddress, isLocalTestEnvironment, isNonPublicAddress, isPrivateAddress, pinnedFetch, readResponseBodyBounded, readResponseJsonBounded, readResponseTextBounded, resolvePinnedDestination, undiciFetch, validateHttpUrl };
|
|
99
|
+
export declare function isNonPublicAddress(address: string): boolean;
|
|
100
|
+
export declare const isPrivateAddress: typeof isNonPublicAddress;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { Agent, fetch as undiciFetchImpl } from "undici/index.js";
|
|
2
|
+
import { Agent, fetch as undiciFetchImpl, request as undiciRequestImpl } from "undici/index.js";
|
|
3
3
|
import { lookup as nodeLookup } from "dns/promises";
|
|
4
4
|
import { isIP } from "net";
|
|
5
|
+
var DISPATCHER_CLOSE_TIMEOUT_MS = 100;
|
|
5
6
|
var OAUTH_MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
6
7
|
function validateHttpUrl(rawUrl, options = {}) {
|
|
7
8
|
const label = options.label ?? "HTTP endpoint";
|
|
@@ -38,7 +39,14 @@ var ResponseBodyLimitError = class extends Error {
|
|
|
38
39
|
this.name = "ResponseBodyLimitError";
|
|
39
40
|
}
|
|
40
41
|
};
|
|
41
|
-
|
|
42
|
+
var RequestDeadlineError = class extends Error {
|
|
43
|
+
constructor(label) {
|
|
44
|
+
super(`${label} timed out`);
|
|
45
|
+
this.label = label;
|
|
46
|
+
this.name = "RequestDeadlineError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
async function readResponseBodyBounded(response, maxBytes, label, options = {}) {
|
|
42
50
|
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
|
43
51
|
throw new RangeError("response body limit must be a non-negative safe integer");
|
|
44
52
|
}
|
|
@@ -63,7 +71,7 @@ async function readResponseBodyBounded(response, maxBytes, label) {
|
|
|
63
71
|
let receivedBytes = 0;
|
|
64
72
|
try {
|
|
65
73
|
for (; ; ) {
|
|
66
|
-
const result = await reader.
|
|
74
|
+
const result = await readStreamChunk(reader, options.signal, label);
|
|
67
75
|
if (result.done) {
|
|
68
76
|
break;
|
|
69
77
|
}
|
|
@@ -75,10 +83,13 @@ async function readResponseBodyBounded(response, maxBytes, label) {
|
|
|
75
83
|
chunks.push(result.value);
|
|
76
84
|
}
|
|
77
85
|
} catch (error) {
|
|
78
|
-
|
|
86
|
+
void reader.cancel(error).catch(() => void 0);
|
|
79
87
|
throw error;
|
|
80
88
|
} finally {
|
|
81
|
-
|
|
89
|
+
try {
|
|
90
|
+
reader.releaseLock();
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
82
93
|
}
|
|
83
94
|
const body = new Uint8Array(receivedBytes);
|
|
84
95
|
let offset = 0;
|
|
@@ -88,11 +99,36 @@ async function readResponseBodyBounded(response, maxBytes, label) {
|
|
|
88
99
|
}
|
|
89
100
|
return body;
|
|
90
101
|
}
|
|
91
|
-
async function readResponseTextBounded(response, maxBytes, label) {
|
|
92
|
-
return new TextDecoder().decode(
|
|
102
|
+
async function readResponseTextBounded(response, maxBytes, label, options = {}) {
|
|
103
|
+
return new TextDecoder().decode(
|
|
104
|
+
await readResponseBodyBounded(response, maxBytes, label, options)
|
|
105
|
+
);
|
|
93
106
|
}
|
|
94
|
-
async function readResponseJsonBounded(response, maxBytes, label) {
|
|
95
|
-
return JSON.parse(await readResponseTextBounded(response, maxBytes, label));
|
|
107
|
+
async function readResponseJsonBounded(response, maxBytes, label, options = {}) {
|
|
108
|
+
return JSON.parse(await readResponseTextBounded(response, maxBytes, label, options));
|
|
109
|
+
}
|
|
110
|
+
async function readStreamChunk(reader, signal, label) {
|
|
111
|
+
if (!signal) return await reader.read();
|
|
112
|
+
if (signal.aborted) throw new RequestDeadlineError(label);
|
|
113
|
+
return await new Promise((resolve, reject) => {
|
|
114
|
+
let settled = false;
|
|
115
|
+
const finish = (callback) => {
|
|
116
|
+
if (settled) return;
|
|
117
|
+
settled = true;
|
|
118
|
+
signal.removeEventListener("abort", onAbort);
|
|
119
|
+
callback();
|
|
120
|
+
};
|
|
121
|
+
const onAbort = () => {
|
|
122
|
+
const error = new RequestDeadlineError(label);
|
|
123
|
+
void reader.cancel(error).catch(() => void 0);
|
|
124
|
+
finish(() => reject(error));
|
|
125
|
+
};
|
|
126
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
127
|
+
void reader.read().then(
|
|
128
|
+
(result) => finish(() => resolve(result)),
|
|
129
|
+
(error) => finish(() => reject(error))
|
|
130
|
+
);
|
|
131
|
+
});
|
|
96
132
|
}
|
|
97
133
|
var DestinationPolicyError = class extends Error {
|
|
98
134
|
constructor(reason, message) {
|
|
@@ -106,6 +142,79 @@ var defaultDnsLookup = async (hostname) => {
|
|
|
106
142
|
return answers.map((entry) => normalizeDnsAnswer(entry));
|
|
107
143
|
};
|
|
108
144
|
var defaultFetch = (input, init) => undiciFetchImpl(input, init);
|
|
145
|
+
var pinnedRequestFetch = async (input, init) => {
|
|
146
|
+
const source = input instanceof Request ? input : null;
|
|
147
|
+
const url = source?.url ?? input.toString();
|
|
148
|
+
const method = init?.method ?? source?.method ?? "GET";
|
|
149
|
+
const headers = new Headers(source?.headers);
|
|
150
|
+
if (init?.headers) {
|
|
151
|
+
new Headers(init.headers).forEach((value, name) => headers.set(name, value));
|
|
152
|
+
}
|
|
153
|
+
if (!headers.has("accept-encoding")) headers.set("accept-encoding", "identity");
|
|
154
|
+
const rawBody = init?.body ?? (method !== "GET" && method !== "HEAD" ? source?.body : void 0);
|
|
155
|
+
const body = await normalizeUndiciRequestBody(rawBody, headers);
|
|
156
|
+
const dispatcher = init?.dispatcher;
|
|
157
|
+
const result = await undiciRequestImpl(url, {
|
|
158
|
+
method,
|
|
159
|
+
headers,
|
|
160
|
+
...body !== void 0 && body !== null ? { body } : {},
|
|
161
|
+
...init?.signal ? { signal: init.signal } : source?.signal ? { signal: source.signal } : {},
|
|
162
|
+
...dispatcher ? { dispatcher } : {},
|
|
163
|
+
maxRedirections: 0
|
|
164
|
+
});
|
|
165
|
+
const iterator = result.body[Symbol.asyncIterator]();
|
|
166
|
+
const responseBody = new ReadableStream({
|
|
167
|
+
async pull(controller) {
|
|
168
|
+
try {
|
|
169
|
+
const chunk = await iterator.next();
|
|
170
|
+
if (chunk.done) {
|
|
171
|
+
controller.close();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
controller.enqueue(
|
|
175
|
+
chunk.value instanceof Uint8Array ? chunk.value : new Uint8Array(chunk.value)
|
|
176
|
+
);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
controller.error(error);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
async cancel(reason) {
|
|
182
|
+
result.body.destroy(reason instanceof Error ? reason : void 0);
|
|
183
|
+
await iterator.return?.();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
const responseHeaders = new Headers();
|
|
187
|
+
for (const [name, value] of Object.entries(result.headers)) {
|
|
188
|
+
if (Array.isArray(value)) {
|
|
189
|
+
for (const entry of value) responseHeaders.append(name, entry);
|
|
190
|
+
} else if (value !== void 0) {
|
|
191
|
+
responseHeaders.set(name, value);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const response = new Response(responseBody, {
|
|
195
|
+
status: result.statusCode,
|
|
196
|
+
headers: responseHeaders
|
|
197
|
+
});
|
|
198
|
+
Object.defineProperty(response, "url", { value: url });
|
|
199
|
+
return response;
|
|
200
|
+
};
|
|
201
|
+
async function normalizeUndiciRequestBody(body, headers) {
|
|
202
|
+
if (body instanceof URLSearchParams) {
|
|
203
|
+
if (!headers.has("content-type")) {
|
|
204
|
+
headers.set("content-type", "application/x-www-form-urlencoded;charset=UTF-8");
|
|
205
|
+
}
|
|
206
|
+
return body.toString();
|
|
207
|
+
}
|
|
208
|
+
if (body instanceof Blob) {
|
|
209
|
+
if (body.type && !headers.has("content-type")) headers.set("content-type", body.type);
|
|
210
|
+
return new Uint8Array(await body.arrayBuffer());
|
|
211
|
+
}
|
|
212
|
+
if (body instanceof ArrayBuffer) return new Uint8Array(body);
|
|
213
|
+
if (ArrayBuffer.isView(body)) {
|
|
214
|
+
return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
215
|
+
}
|
|
216
|
+
return body;
|
|
217
|
+
}
|
|
109
218
|
var undiciFetch = defaultFetch;
|
|
110
219
|
async function resolvePinnedDestination(rawUrl, settings, options = {}) {
|
|
111
220
|
const label = options.label ?? "Outbound request";
|
|
@@ -165,7 +274,7 @@ async function pinnedFetch(input, init, settings, options = {}) {
|
|
|
165
274
|
const rawUrl = input instanceof Request ? input.url : input;
|
|
166
275
|
const destination = await resolvePinnedDestination(rawUrl, settings, options);
|
|
167
276
|
const dispatcher = options.agentFactory?.(destination.addresses) ?? createPinnedAgent(destination.addresses);
|
|
168
|
-
const fetchImpl = options.fetchImpl ??
|
|
277
|
+
const fetchImpl = options.fetchImpl ?? pinnedRequestFetch;
|
|
169
278
|
const fetchInit = {
|
|
170
279
|
...init,
|
|
171
280
|
redirect: "manual",
|
|
@@ -179,10 +288,10 @@ async function pinnedFetch(input, init, settings, options = {}) {
|
|
|
179
288
|
throw error;
|
|
180
289
|
}
|
|
181
290
|
if (!response.body) {
|
|
182
|
-
await closeDispatcher(dispatcher);
|
|
291
|
+
await closeDispatcher(dispatcher, DISPATCHER_CLOSE_TIMEOUT_MS);
|
|
183
292
|
return response;
|
|
184
293
|
}
|
|
185
|
-
return responseWithDispatcherLifecycle(response, dispatcher);
|
|
294
|
+
return responseWithDispatcherLifecycle(response, dispatcher, DISPATCHER_CLOSE_TIMEOUT_MS);
|
|
186
295
|
}
|
|
187
296
|
function isLocalTestEnvironment(environment) {
|
|
188
297
|
return environment === "local" || environment === "test";
|
|
@@ -229,7 +338,10 @@ function createPinnedAgent(addresses) {
|
|
|
229
338
|
if (options.all) {
|
|
230
339
|
callback(
|
|
231
340
|
null,
|
|
232
|
-
candidates.map((entry) => ({
|
|
341
|
+
candidates.map((entry) => ({
|
|
342
|
+
address: entry.address,
|
|
343
|
+
family: entry.family
|
|
344
|
+
}))
|
|
233
345
|
);
|
|
234
346
|
return;
|
|
235
347
|
}
|
|
@@ -248,13 +360,19 @@ function createPinnedAgent(addresses) {
|
|
|
248
360
|
}
|
|
249
361
|
};
|
|
250
362
|
}
|
|
251
|
-
function responseWithDispatcherLifecycle(response, dispatcher) {
|
|
363
|
+
function responseWithDispatcherLifecycle(response, dispatcher, closeTimeoutMs) {
|
|
252
364
|
const reader = response.body.getReader();
|
|
365
|
+
let readerReleased = false;
|
|
366
|
+
const releaseReader = () => {
|
|
367
|
+
if (readerReleased) return;
|
|
368
|
+
readerReleased = true;
|
|
369
|
+
reader.releaseLock();
|
|
370
|
+
};
|
|
253
371
|
let disposed = null;
|
|
254
372
|
let cancelled = false;
|
|
255
373
|
const finish = (destroy, error) => {
|
|
256
374
|
if (!disposed) {
|
|
257
|
-
disposed = destroy ? destroyDispatcher(dispatcher, error) : closeDispatcher(dispatcher);
|
|
375
|
+
disposed = destroy ? destroyDispatcher(dispatcher, error) : closeDispatcher(dispatcher, closeTimeoutMs);
|
|
258
376
|
}
|
|
259
377
|
return disposed;
|
|
260
378
|
};
|
|
@@ -263,12 +381,14 @@ function responseWithDispatcherLifecycle(response, dispatcher) {
|
|
|
263
381
|
try {
|
|
264
382
|
const chunk = await reader.read();
|
|
265
383
|
if (chunk.done) {
|
|
384
|
+
releaseReader();
|
|
266
385
|
await finish(cancelled);
|
|
267
386
|
controller.close();
|
|
268
387
|
return;
|
|
269
388
|
}
|
|
270
389
|
controller.enqueue(chunk.value);
|
|
271
390
|
} catch (error) {
|
|
391
|
+
releaseReader();
|
|
272
392
|
await finish(true, error);
|
|
273
393
|
controller.error(error);
|
|
274
394
|
}
|
|
@@ -278,6 +398,7 @@ function responseWithDispatcherLifecycle(response, dispatcher) {
|
|
|
278
398
|
try {
|
|
279
399
|
await reader.cancel(reason);
|
|
280
400
|
} finally {
|
|
401
|
+
releaseReader();
|
|
281
402
|
await finish(true, reason);
|
|
282
403
|
}
|
|
283
404
|
}
|
|
@@ -294,11 +415,20 @@ function responseWithDispatcherLifecycle(response, dispatcher) {
|
|
|
294
415
|
});
|
|
295
416
|
return wrapped;
|
|
296
417
|
}
|
|
297
|
-
async function closeDispatcher(dispatcher) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
418
|
+
async function closeDispatcher(dispatcher, timeoutMs) {
|
|
419
|
+
let timeout;
|
|
420
|
+
const outcome = await Promise.race([
|
|
421
|
+
Promise.resolve().then(() => dispatcher.close()).then(
|
|
422
|
+
() => "closed",
|
|
423
|
+
() => "failed"
|
|
424
|
+
),
|
|
425
|
+
new Promise((resolve) => {
|
|
426
|
+
timeout = setTimeout(() => resolve("timed_out"), timeoutMs);
|
|
427
|
+
})
|
|
428
|
+
]);
|
|
429
|
+
if (timeout) clearTimeout(timeout);
|
|
430
|
+
if (outcome !== "closed") {
|
|
431
|
+
void destroyDispatcher(dispatcher);
|
|
302
432
|
}
|
|
303
433
|
}
|
|
304
434
|
async function destroyDispatcher(dispatcher, error) {
|
|
@@ -474,6 +604,7 @@ function ipv6Constant(address) {
|
|
|
474
604
|
export {
|
|
475
605
|
DestinationPolicyError,
|
|
476
606
|
OAUTH_MAX_RESPONSE_BYTES,
|
|
607
|
+
RequestDeadlineError,
|
|
477
608
|
ResponseBodyLimitError,
|
|
478
609
|
isInvalidAddress,
|
|
479
610
|
isLocalTestEnvironment,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// The explicit entrypoint avoids Bun's native `undici` compatibility shim,\n// which exposes an Agent-shaped object without Dispatcher methods.\nimport { Agent, fetch as undiciFetchImpl } from \"undici/index.js\";\nimport { lookup as nodeLookup } from \"node:dns/promises\";\nimport { isIP } from \"node:net\";\n\nexport type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;\n\nexport type DnsAddress = {\n address: string;\n family: 4 | 6;\n};\n\nexport type DnsLookup = (hostname: string) => Promise<readonly DnsAddress[]>;\n\nexport type OutboundNetworkSettings = {\n environment: string;\n integrationsAllowPrivateNetworkTargets: boolean;\n};\n\nexport type PinnedDestination = {\n url: URL;\n hostname: string;\n addresses: readonly DnsAddress[];\n};\n\nexport type DispatcherLifecycle = {\n /** The raw dispatcher handed to fetch; omitted for test-only lifecycle fakes. */\n dispatcher?: unknown;\n close: () => Promise<void> | void;\n destroy: (error?: unknown) => Promise<void> | void;\n};\n\nexport type PinnedFetchOptions = {\n fetchImpl?: FetchLike;\n dnsLookup?: DnsLookup;\n agentFactory?: (addresses: readonly DnsAddress[]) => DispatcherLifecycle;\n label?: string;\n requireHttpsOutsideLocalTest?: boolean;\n};\n\nexport const OAUTH_MAX_RESPONSE_BYTES = 1024 * 1024;\n\nexport type HttpUrlValidationOptions = {\n allowLoopbackHttp?: boolean;\n label?: string;\n};\n\n/** Validate a protocol endpoint before it is persisted, returned, or opened. */\nexport function validateHttpUrl(rawUrl: string, options: HttpUrlValidationOptions = {}): string {\n const label = options.label ?? \"HTTP endpoint\";\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new DestinationPolicyError(\"invalid_url\", `${label} URL is invalid`);\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new DestinationPolicyError(\n \"unsupported_protocol\",\n `${label} only supports http and https URLs`,\n );\n }\n if (url.username || url.password) {\n throw new DestinationPolicyError(\"invalid_url\", `${label} URL may not contain credentials`);\n }\n if (url.hash) {\n throw new DestinationPolicyError(\"invalid_url\", `${label} URL may not contain a fragment`);\n }\n if (\n url.protocol === \"http:\" &&\n !(options.allowLoopbackHttp && isLoopbackHostname(url.hostname))\n ) {\n throw new DestinationPolicyError(\"https_required\", `${label} must use https`);\n }\n return url.toString();\n}\n\n/**\n * Raised when a response cannot be safely consumed within its caller's byte\n * budget. The error intentionally contains no response bytes or provider\n * message: these readers are used on credential-bearing paths.\n */\nexport class ResponseBodyLimitError extends Error {\n constructor(\n readonly label: string,\n readonly actualBytes: number,\n readonly maxBytes: number,\n readonly reason: \"declared_length\" | \"stream_overflow\" | \"invalid_content_length\",\n ) {\n super(`${label} exceeded its ${maxBytes}-byte response limit`);\n this.name = \"ResponseBodyLimitError\";\n }\n}\n\n/**\n * Read a response through its stream with a hard byte ceiling.\n *\n * A declared Content-Length above the ceiling is rejected before reading. A\n * body exactly at the ceiling is accepted; the first byte beyond it cancels\n * the stream and rejects. Cancellation is important because pinnedFetch owns a\n * per-response dispatcher which must be closed on every exit path.\n */\nexport async function readResponseBodyBounded(\n response: Response,\n maxBytes: number,\n label: string,\n): Promise<Uint8Array> {\n if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {\n throw new RangeError(\"response body limit must be a non-negative safe integer\");\n }\n const declared = response.headers.get(\"content-length\");\n if (declared !== null) {\n const normalized = declared.trim();\n if (!/^\\d+$/.test(normalized)) {\n await cancelResponse(response);\n throw new ResponseBodyLimitError(label, 0, maxBytes, \"invalid_content_length\");\n }\n const declaredBytes = Number(normalized);\n if (!Number.isSafeInteger(declaredBytes) || declaredBytes > maxBytes) {\n await cancelResponse(response);\n throw new ResponseBodyLimitError(label, declaredBytes, maxBytes, \"declared_length\");\n }\n }\n if (!response.body) {\n return new Uint8Array();\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let receivedBytes = 0;\n try {\n for (;;) {\n const result = await reader.read();\n if (result.done) {\n break;\n }\n receivedBytes += result.value.byteLength;\n if (receivedBytes > maxBytes) {\n await reader.cancel().catch(() => undefined);\n throw new ResponseBodyLimitError(label, receivedBytes, maxBytes, \"stream_overflow\");\n }\n chunks.push(result.value);\n }\n } catch (error) {\n await reader.cancel().catch(() => undefined);\n throw error;\n } finally {\n reader.releaseLock();\n }\n\n const body = new Uint8Array(receivedBytes);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return body;\n}\n\nexport async function readResponseTextBounded(\n response: Response,\n maxBytes: number,\n label: string,\n): Promise<string> {\n return new TextDecoder().decode(await readResponseBodyBounded(response, maxBytes, label));\n}\n\nexport async function readResponseJsonBounded<T = unknown>(\n response: Response,\n maxBytes: number,\n label: string,\n): Promise<T> {\n return JSON.parse(await readResponseTextBounded(response, maxBytes, label)) as T;\n}\n\nexport type ResolvePinnedDestinationOptions = {\n dnsLookup?: DnsLookup;\n label?: string;\n requireHttpsOutsideLocalTest?: boolean;\n};\n\nexport type DestinationPolicyReason =\n | \"invalid_url\"\n | \"unsupported_protocol\"\n | \"https_required\"\n | \"dns_failed\"\n | \"dns_empty\"\n | \"invalid_dns_answer\"\n | \"private_or_special_use\";\n\nexport class DestinationPolicyError extends Error {\n constructor(\n readonly reason: DestinationPolicyReason,\n message: string,\n ) {\n super(message);\n this.name = \"DestinationPolicyError\";\n }\n}\n\nconst defaultDnsLookup: DnsLookup = async (hostname) => {\n const answers = await nodeLookup(hostname, { all: true });\n return answers.map((entry) => normalizeDnsAnswer(entry));\n};\n\nconst defaultFetch: FetchLike = (input, init) =>\n (undiciFetchImpl as unknown as FetchLike)(input, init);\n\n/** Undici fetch used by the pinned transport; Bun callers should prefer this over native fetch. */\nexport const undiciFetch: FetchLike = defaultFetch;\n\n/**\n * Resolve and policy-check one destination. The returned address set is the\n * complete DNS answer that the caller is allowed to use; the transport never\n * performs a second resolver call.\n */\nexport async function resolvePinnedDestination(\n rawUrl: string | URL,\n settings: OutboundNetworkSettings,\n options: ResolvePinnedDestinationOptions = {},\n): Promise<PinnedDestination> {\n const label = options.label ?? \"Outbound request\";\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new DestinationPolicyError(\"invalid_url\", `${label} URL is invalid`);\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new DestinationPolicyError(\n \"unsupported_protocol\",\n `${label} only supports http and https URLs`,\n );\n }\n const localTestEscape = isLocalTestEnvironment(settings.environment);\n if (options.requireHttpsOutsideLocalTest && !localTestEscape && url.protocol !== \"https:\") {\n throw new DestinationPolicyError(\n \"https_required\",\n `${label} must use https outside local/test`,\n );\n }\n\n const hostname = normalizeHostname(url.hostname);\n if (!hostname) {\n throw new DestinationPolicyError(\"invalid_url\", `${label} URL has no hostname`);\n }\n\n const literalFamily = isIP(hostname);\n let addresses: readonly DnsAddress[];\n try {\n addresses = literalFamily\n ? [{ address: hostname, family: literalFamily === 6 ? 6 : 4 }]\n : await (options.dnsLookup ?? defaultDnsLookup)(hostname);\n } catch (error) {\n if (error instanceof DestinationPolicyError && error.reason === \"invalid_dns_answer\") {\n throw error;\n }\n throw new DestinationPolicyError(\"dns_failed\", `${label} hostname could not be resolved`);\n }\n\n const normalizedAddresses = dedupeAddresses(addresses);\n if (normalizedAddresses.length === 0) {\n throw new DestinationPolicyError(\"dns_empty\", `${label} hostname has no addresses`);\n }\n if (normalizedAddresses.some((entry) => isInvalidAddress(entry.address))) {\n throw new DestinationPolicyError(\n \"invalid_dns_answer\",\n `${label} hostname returned an invalid address`,\n );\n }\n\n const privateEscape = localTestEscape || settings.integrationsAllowPrivateNetworkTargets === true;\n if (\n !privateEscape &&\n (isLocalHostname(hostname) ||\n normalizedAddresses.some((entry) => isNonPublicAddress(entry.address)))\n ) {\n throw new DestinationPolicyError(\n \"private_or_special_use\",\n `${label} may not target a private or special-use network address`,\n );\n }\n return { url, hostname, addresses: normalizedAddresses };\n}\n\n/**\n * Fetch through a dispatcher whose lookup is pinned to the result of exactly\n * one policy resolution. The response body owns the agent until completion,\n * cancellation, or stream failure.\n */\nexport async function pinnedFetch(\n input: string | URL | Request,\n init: RequestInit | undefined,\n settings: OutboundNetworkSettings,\n options: PinnedFetchOptions = {},\n): Promise<Response> {\n const rawUrl = input instanceof Request ? input.url : input;\n const destination = await resolvePinnedDestination(rawUrl, settings, options);\n const dispatcher =\n options.agentFactory?.(destination.addresses) ?? createPinnedAgent(destination.addresses);\n const fetchImpl = options.fetchImpl ?? defaultFetch;\n const fetchInit = {\n ...init,\n redirect: \"manual\" as const,\n dispatcher: dispatcher.dispatcher ?? dispatcher,\n } as RequestInit & { dispatcher: DispatcherLifecycle };\n let response: Response;\n try {\n response = await fetchImpl(input, fetchInit);\n } catch (error) {\n await destroyDispatcher(dispatcher, error);\n throw error;\n }\n if (!response.body) {\n await closeDispatcher(dispatcher);\n return response;\n }\n return responseWithDispatcherLifecycle(response, dispatcher);\n}\n\nexport function isLocalTestEnvironment(environment: string): boolean {\n return environment === \"local\" || environment === \"test\";\n}\n\n/** Return true for malformed, non-IPv4, and non-IPv6 address strings. */\nexport function isInvalidAddress(address: string): boolean {\n return isIP(stripAddressBrackets(address.trim())) === 0;\n}\n\n/**\n * Classify private, reserved, documentation, benchmark, multicast, and other\n * special-use answers. IPv4-mapped IPv6 addresses are classified through their\n * embedded IPv4 value.\n */\nexport function isNonPublicAddress(address: string): boolean {\n const normalized = stripAddressBrackets(address.trim().toLowerCase());\n const family = isIP(normalized);\n if (family === 0) {\n return true;\n }\n if (family === 4) {\n return isNonPublicIpv4(normalized);\n }\n const mappedText = ipv4FromMappedText(normalized);\n if (mappedText !== null) {\n return isNonPublicIpv4(mappedText);\n }\n const value = parseIpv6(normalized);\n if (value === null) {\n return true;\n }\n const mapped = ipv4FromMappedIpv6(value);\n if (mapped !== null) {\n return isNonPublicIpv4(mapped);\n }\n // Fail closed on unallocated/non-global IPv6 space. Current globally routed\n // unicast addresses live in 2000::/3; local, transition, multicast, and\n // future-use ranges must not become credential-bearing egress merely because\n // they were absent from a denylist.\n if (!hasIpv6Prefix(value, IPV6_GLOBAL_UNICAST_PREFIX, 3)) {\n return true;\n }\n return IPV6_SPECIAL_PREFIXES.some(([prefix, bits]) => hasIpv6Prefix(value, prefix, bits));\n}\n\n// Backwards-compatible name used by the database token-broker API.\nexport const isPrivateAddress = isNonPublicAddress;\n\nfunction createPinnedAgent(addresses: readonly DnsAddress[]): DispatcherLifecycle {\n const agent = new Agent({\n connect: {\n lookup: ((\n _hostname: string,\n options: { all?: boolean; family?: number },\n callback: (\n error: Error | null,\n address?: string | Array<{ address: string; family: number }>,\n family?: number,\n ) => void,\n ) => {\n const candidates =\n options.family === 4 || options.family === 6\n ? addresses.filter((entry) => entry.family === options.family)\n : addresses;\n if (candidates.length === 0) {\n callback(new Error(\"pinned DNS answer has no address for requested family\"));\n return;\n }\n if (options.all) {\n callback(\n null,\n candidates.map((entry) => ({ address: entry.address, family: entry.family })),\n );\n return;\n }\n const first = candidates[0]!;\n callback(null, first.address, first.family);\n }) as never,\n },\n });\n return {\n dispatcher: agent,\n close: async () => {\n await agent.close();\n },\n destroy: async (error) => {\n await agent.destroy(error instanceof Error ? error : null);\n },\n };\n}\n\nfunction responseWithDispatcherLifecycle(\n response: Response,\n dispatcher: DispatcherLifecycle,\n): Response {\n const reader = response.body!.getReader();\n let disposed: Promise<void> | null = null;\n let cancelled = false;\n const finish = (destroy: boolean, error?: unknown): Promise<void> => {\n if (!disposed) {\n disposed = destroy ? destroyDispatcher(dispatcher, error) : closeDispatcher(dispatcher);\n }\n return disposed;\n };\n const body = new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const chunk = await reader.read();\n if (chunk.done) {\n await finish(cancelled);\n controller.close();\n return;\n }\n controller.enqueue(chunk.value);\n } catch (error) {\n await finish(true, error);\n controller.error(error);\n }\n },\n async cancel(reason) {\n cancelled = true;\n try {\n await reader.cancel(reason);\n } finally {\n await finish(true, reason);\n }\n },\n });\n const wrapped = new Response(body, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n Object.defineProperties(wrapped, {\n redirected: { value: response.redirected },\n type: { value: response.type },\n url: { value: response.url },\n });\n return wrapped;\n}\n\nasync function closeDispatcher(dispatcher: DispatcherLifecycle): Promise<void> {\n try {\n await dispatcher.close();\n } catch {\n await destroyDispatcher(dispatcher);\n }\n}\n\nasync function destroyDispatcher(dispatcher: DispatcherLifecycle, error?: unknown): Promise<void> {\n try {\n await dispatcher.destroy(error);\n } catch {\n // Cleanup is best effort after the dispatcher has already failed.\n }\n}\n\nasync function cancelResponse(response: Response): Promise<void> {\n await response.body?.cancel().catch(() => undefined);\n}\n\nfunction normalizeHostname(hostname: string): string {\n return stripAddressBrackets(hostname.trim().toLowerCase()).replace(/\\.$/, \"\");\n}\n\nfunction stripAddressBrackets(address: string): string {\n return address.startsWith(\"[\") && address.endsWith(\"]\") ? address.slice(1, -1) : address;\n}\n\nfunction isLocalHostname(hostname: string): boolean {\n return hostname === \"localhost\" || hostname.endsWith(\".localhost\");\n}\n\nfunction isLoopbackHostname(hostname: string): boolean {\n const normalized = normalizeHostname(hostname);\n return (\n normalized === \"localhost\" ||\n normalized.endsWith(\".localhost\") ||\n normalized === \"::1\" ||\n isLoopbackIpv4(normalized)\n );\n}\n\nfunction isLoopbackIpv4(hostname: string): boolean {\n if (isIP(hostname) !== 4) return false;\n return hostname.split(\".\")[0] === \"127\";\n}\n\nfunction dedupeAddresses(addresses: readonly DnsAddress[]): DnsAddress[] {\n const out: DnsAddress[] = [];\n const seen = new Set<string>();\n if (!Array.isArray(addresses)) {\n throw invalidDnsAnswer();\n }\n for (const entry of addresses) {\n const normalized = normalizeDnsAnswer(entry);\n const key = `${normalized.family}:${normalized.address}`;\n if (!seen.has(key)) {\n seen.add(key);\n out.push(normalized);\n }\n }\n return out;\n}\n\n/**\n * Validate resolver metadata before it can influence either policy checks or\n * the pinned dispatcher. Never infer a family from the address while retaining\n * a conflicting resolver claim: an invalid answer is an invalid answer.\n */\nfunction normalizeDnsAnswer(entry: unknown): DnsAddress {\n if (!entry || typeof entry !== \"object\") {\n throw invalidDnsAnswer();\n }\n const candidate = entry as { address?: unknown; family?: unknown };\n if (typeof candidate.address !== \"string\") {\n throw invalidDnsAnswer();\n }\n const family = candidate.family;\n if (family !== 4 && family !== 6) {\n throw invalidDnsAnswer();\n }\n const address = stripAddressBrackets(candidate.address.trim().toLowerCase());\n const actualFamily = isIP(address);\n if (actualFamily !== family) {\n throw invalidDnsAnswer();\n }\n return { address, family };\n}\n\nfunction invalidDnsAnswer(): DestinationPolicyError {\n return new DestinationPolicyError(\n \"invalid_dns_answer\",\n \"hostname returned an invalid DNS answer\",\n );\n}\n\nfunction isNonPublicIpv4(address: string): boolean {\n const parts = address.split(\".\").map(Number);\n if (\n parts.length !== 4 ||\n parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)\n ) {\n return true;\n }\n const value = (parts[0]! << 24) | (parts[1]! << 16) | (parts[2]! << 8) | parts[3]!;\n const unsigned = value >>> 0;\n return (\n inIpv4Range(unsigned, 0x00000000, 0x00ffffff) ||\n inIpv4Range(unsigned, 0x0a000000, 0x0affffff) ||\n inIpv4Range(unsigned, 0x64400000, 0x647fffff) ||\n inIpv4Range(unsigned, 0x7f000000, 0x7fffffff) ||\n inIpv4Range(unsigned, 0xa9fe0000, 0xa9feffff) ||\n inIpv4Range(unsigned, 0xac100000, 0xac1fffff) ||\n inIpv4Range(unsigned, 0xc0000000, 0xc00000ff) ||\n inIpv4Range(unsigned, 0xc0000200, 0xc00002ff) ||\n inIpv4Range(unsigned, 0xc01fc400, 0xc01fc4ff) ||\n inIpv4Range(unsigned, 0xc034c100, 0xc034c1ff) ||\n inIpv4Range(unsigned, 0xc0586300, 0xc05863ff) ||\n inIpv4Range(unsigned, 0xc0a80000, 0xc0a8ffff) ||\n inIpv4Range(unsigned, 0xc0af3000, 0xc0af30ff) ||\n inIpv4Range(unsigned, 0xc6120000, 0xc613ffff) ||\n inIpv4Range(unsigned, 0xc6336400, 0xc63364ff) ||\n inIpv4Range(unsigned, 0xcb007100, 0xcb0071ff) ||\n inIpv4Range(unsigned, 0xe0000000, 0xffffffff)\n );\n}\n\nfunction inIpv4Range(value: number, start: number, end: number): boolean {\n return value >= start && value <= end;\n}\n\nconst IPV6_GLOBAL_UNICAST_PREFIX = ipv6Constant(\"2000::\");\n\nconst IPV6_SPECIAL_PREFIXES: readonly [bigint, number][] = [\n [ipv6Constant(\"::\"), 96],\n [ipv6Constant(\"64:ff9b::\"), 96],\n [ipv6Constant(\"64:ff9b:1::\"), 48],\n [ipv6Constant(\"100::\"), 64],\n // IETF protocol assignments contain globally reachable carve-outs, but they\n // are control-plane anycast/protocol addresses rather than integration\n // endpoints. Credential-bearing MCP/OAuth egress fails closed on the full\n // special-purpose block.\n [ipv6Constant(\"2001::\"), 23],\n [ipv6Constant(\"2001:db8::\"), 32],\n [ipv6Constant(\"2002::\"), 16],\n [ipv6Constant(\"2620:4f:8000::\"), 48],\n [ipv6Constant(\"3ffe::\"), 16],\n [ipv6Constant(\"3fff::\"), 20],\n [ipv6Constant(\"5f00::\"), 16],\n [ipv6Constant(\"fc00::\"), 7],\n [ipv6Constant(\"fe80::\"), 10],\n [ipv6Constant(\"fec0::\"), 10],\n [ipv6Constant(\"ff00::\"), 8],\n];\n\nfunction hasIpv6Prefix(value: bigint, prefix: bigint, bits: number): boolean {\n const shift = 128n - BigInt(bits);\n return value >> shift === prefix >> shift;\n}\n\nfunction ipv4FromMappedIpv6(value: bigint): string | null {\n if (value >> 32n !== 0xffffn) {\n return null;\n }\n const embedded = Number(value & 0xffffffffn);\n return `${embedded >>> 24}.${(embedded >>> 16) & 0xff}.${(embedded >>> 8) & 0xff}.${embedded & 0xff}`;\n}\n\nfunction ipv4FromMappedText(address: string): string | null {\n if (!address.startsWith(\"::ffff:\")) {\n return null;\n }\n const embedded = address.slice(\"::ffff:\".length);\n if (isIP(embedded) === 4) {\n return embedded;\n }\n const parts = embedded.split(\":\");\n if (parts.length !== 2 || parts.some((part) => !/^[0-9a-f]{1,4}$/.test(part))) {\n return null;\n }\n const high = Number.parseInt(parts[0]!, 16);\n const low = Number.parseInt(parts[1]!, 16);\n return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;\n}\n\nfunction parseIpv6(address: string): bigint | null {\n const percent = address.indexOf(\"%\");\n if (percent >= 0) {\n return null;\n }\n let value = address;\n if (value.includes(\".\")) {\n const lastColon = value.lastIndexOf(\":\");\n const dotted = value.slice(lastColon + 1);\n const parts = dotted.split(\".\").map(Number);\n if (\n parts.length !== 4 ||\n parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)\n ) {\n return null;\n }\n const hex =\n ((parts[0]! << 8) | parts[1]!).toString(16).padStart(4, \"0\") +\n ((parts[2]! << 8) | parts[3]!).toString(16).padStart(4, \"0\");\n value = `${value.slice(0, lastColon + 1)}${hex}`;\n }\n const halves = value.split(\"::\");\n if (halves.length > 2) {\n return null;\n }\n const left = halves[0] ? halves[0].split(\":\") : [];\n const right = halves.length === 2 && halves[1] ? halves[1].split(\":\") : [];\n if (left.concat(right).some((part) => !/^[0-9a-f]{1,4}$/i.test(part))) {\n return null;\n }\n const missing = halves.length === 2 ? 8 - left.length - right.length : 0;\n if (missing < 0 || (halves.length === 1 && missing !== 0)) {\n return null;\n }\n const groups = [...left, ...Array.from({ length: missing }, () => \"0\"), ...right];\n if (groups.length !== 8) {\n return null;\n }\n return groups.reduce((acc, group) => (acc << 16n) | BigInt(`0x${group}`), 0n);\n}\n\nfunction ipv6Constant(address: string): bigint {\n const value = parseIpv6(address);\n if (value === null) {\n throw new Error(`invalid IPv6 constant: ${address}`);\n }\n return value;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// The explicit entrypoint avoids Bun's native `undici` compatibility shim,\n// which exposes an Agent-shaped object without Dispatcher methods.\nimport { Agent, fetch as undiciFetchImpl, request as undiciRequestImpl } from \"undici/index.js\";\nimport { lookup as nodeLookup } from \"node:dns/promises\";\nimport { isIP } from \"node:net\";\n\nexport type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;\n\nexport type DnsAddress = {\n address: string;\n family: 4 | 6;\n};\n\nexport type DnsLookup = (hostname: string) => Promise<readonly DnsAddress[]>;\n\nexport type OutboundNetworkSettings = {\n environment: string;\n integrationsAllowPrivateNetworkTargets: boolean;\n};\n\nexport type PinnedDestination = {\n url: URL;\n hostname: string;\n addresses: readonly DnsAddress[];\n};\n\nexport type DispatcherLifecycle = {\n /** The raw dispatcher handed to fetch; omitted for test-only lifecycle fakes. */\n dispatcher?: unknown;\n close: () => Promise<void> | void;\n destroy: (error?: unknown) => Promise<void> | void;\n};\n\nexport type PinnedFetchOptions = {\n fetchImpl?: FetchLike;\n dnsLookup?: DnsLookup;\n agentFactory?: (addresses: readonly DnsAddress[]) => DispatcherLifecycle;\n label?: string;\n requireHttpsOutsideLocalTest?: boolean;\n};\n\nconst DISPATCHER_CLOSE_TIMEOUT_MS = 100;\n\nexport const OAUTH_MAX_RESPONSE_BYTES = 1024 * 1024;\n\nexport type HttpUrlValidationOptions = {\n allowLoopbackHttp?: boolean;\n label?: string;\n};\n\n/** Validate a protocol endpoint before it is persisted, returned, or opened. */\nexport function validateHttpUrl(rawUrl: string, options: HttpUrlValidationOptions = {}): string {\n const label = options.label ?? \"HTTP endpoint\";\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new DestinationPolicyError(\"invalid_url\", `${label} URL is invalid`);\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new DestinationPolicyError(\n \"unsupported_protocol\",\n `${label} only supports http and https URLs`,\n );\n }\n if (url.username || url.password) {\n throw new DestinationPolicyError(\"invalid_url\", `${label} URL may not contain credentials`);\n }\n if (url.hash) {\n throw new DestinationPolicyError(\"invalid_url\", `${label} URL may not contain a fragment`);\n }\n if (\n url.protocol === \"http:\" &&\n !(options.allowLoopbackHttp && isLoopbackHostname(url.hostname))\n ) {\n throw new DestinationPolicyError(\"https_required\", `${label} must use https`);\n }\n return url.toString();\n}\n\n/**\n * Raised when a response cannot be safely consumed within its caller's byte\n * budget. The error intentionally contains no response bytes or provider\n * message: these readers are used on credential-bearing paths.\n */\nexport class ResponseBodyLimitError extends Error {\n constructor(\n readonly label: string,\n readonly actualBytes: number,\n readonly maxBytes: number,\n readonly reason: \"declared_length\" | \"stream_overflow\" | \"invalid_content_length\",\n ) {\n super(`${label} exceeded its ${maxBytes}-byte response limit`);\n this.name = \"ResponseBodyLimitError\";\n }\n}\n\n/** Raised when a caller-supplied absolute request deadline expires. */\nexport class RequestDeadlineError extends Error {\n constructor(readonly label: string) {\n super(`${label} timed out`);\n this.name = \"RequestDeadlineError\";\n }\n}\n\nexport type BoundedResponseReadOptions = {\n signal?: AbortSignal;\n};\n\n/**\n * Read a response through its stream with a hard byte ceiling.\n *\n * A declared Content-Length above the ceiling is rejected before reading. A\n * body exactly at the ceiling is accepted; the first byte beyond it cancels\n * the stream and rejects. Cancellation is important because pinnedFetch owns a\n * per-response dispatcher which must be closed on every exit path.\n */\nexport async function readResponseBodyBounded(\n response: Response,\n maxBytes: number,\n label: string,\n options: BoundedResponseReadOptions = {},\n): Promise<Uint8Array> {\n if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {\n throw new RangeError(\"response body limit must be a non-negative safe integer\");\n }\n const declared = response.headers.get(\"content-length\");\n if (declared !== null) {\n const normalized = declared.trim();\n if (!/^\\d+$/.test(normalized)) {\n await cancelResponse(response);\n throw new ResponseBodyLimitError(label, 0, maxBytes, \"invalid_content_length\");\n }\n const declaredBytes = Number(normalized);\n if (!Number.isSafeInteger(declaredBytes) || declaredBytes > maxBytes) {\n await cancelResponse(response);\n throw new ResponseBodyLimitError(label, declaredBytes, maxBytes, \"declared_length\");\n }\n }\n if (!response.body) {\n return new Uint8Array();\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let receivedBytes = 0;\n try {\n for (;;) {\n const result = await readStreamChunk(reader, options.signal, label);\n if (result.done) {\n break;\n }\n receivedBytes += result.value.byteLength;\n if (receivedBytes > maxBytes) {\n await reader.cancel().catch(() => undefined);\n throw new ResponseBodyLimitError(label, receivedBytes, maxBytes, \"stream_overflow\");\n }\n chunks.push(result.value);\n }\n } catch (error) {\n void reader.cancel(error).catch(() => undefined);\n throw error;\n } finally {\n try {\n reader.releaseLock();\n } catch {\n // An aborted read may still own the lock until the underlying transport\n // observes cancellation. The response is already being cancelled above.\n }\n }\n\n const body = new Uint8Array(receivedBytes);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return body;\n}\n\nexport async function readResponseTextBounded(\n response: Response,\n maxBytes: number,\n label: string,\n options: BoundedResponseReadOptions = {},\n): Promise<string> {\n return new TextDecoder().decode(\n await readResponseBodyBounded(response, maxBytes, label, options),\n );\n}\n\nexport async function readResponseJsonBounded<T = unknown>(\n response: Response,\n maxBytes: number,\n label: string,\n options: BoundedResponseReadOptions = {},\n): Promise<T> {\n return JSON.parse(await readResponseTextBounded(response, maxBytes, label, options)) as T;\n}\n\ntype ResponseStreamReadResult = Awaited<\n ReturnType<ReadableStreamDefaultReader<Uint8Array>[\"read\"]>\n>;\n\nasync function readStreamChunk(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n signal: AbortSignal | undefined,\n label: string,\n): Promise<ResponseStreamReadResult> {\n if (!signal) return await reader.read();\n if (signal.aborted) throw new RequestDeadlineError(label);\n return await new Promise<ResponseStreamReadResult>((resolve, reject) => {\n let settled = false;\n const finish = (callback: () => void) => {\n if (settled) return;\n settled = true;\n signal.removeEventListener(\"abort\", onAbort);\n callback();\n };\n const onAbort = () => {\n const error = new RequestDeadlineError(label);\n void reader.cancel(error).catch(() => undefined);\n finish(() => reject(error));\n };\n signal.addEventListener(\"abort\", onAbort, { once: true });\n void reader.read().then(\n (result) => finish(() => resolve(result)),\n (error) => finish(() => reject(error)),\n );\n });\n}\n\nexport type ResolvePinnedDestinationOptions = {\n dnsLookup?: DnsLookup;\n label?: string;\n requireHttpsOutsideLocalTest?: boolean;\n};\n\nexport type DestinationPolicyReason =\n | \"invalid_url\"\n | \"unsupported_protocol\"\n | \"https_required\"\n | \"dns_failed\"\n | \"dns_empty\"\n | \"invalid_dns_answer\"\n | \"private_or_special_use\";\n\nexport class DestinationPolicyError extends Error {\n constructor(\n readonly reason: DestinationPolicyReason,\n message: string,\n ) {\n super(message);\n this.name = \"DestinationPolicyError\";\n }\n}\n\nconst defaultDnsLookup: DnsLookup = async (hostname) => {\n const answers = await nodeLookup(hostname, { all: true });\n return answers.map((entry) => normalizeDnsAnswer(entry));\n};\n\nconst defaultFetch: FetchLike = (input, init) =>\n (undiciFetchImpl as unknown as FetchLike)(input, init);\n\nconst pinnedRequestFetch: FetchLike = async (input, init) => {\n const source = input instanceof Request ? input : null;\n const url = source?.url ?? input.toString();\n const method = init?.method ?? source?.method ?? \"GET\";\n const headers = new Headers(source?.headers);\n if (init?.headers) {\n new Headers(init.headers).forEach((value, name) => headers.set(name, value));\n }\n // Undici request() deliberately does not auto-decompress. Asking providers\n // for identity encoding keeps the returned web Response body truthful.\n if (!headers.has(\"accept-encoding\")) headers.set(\"accept-encoding\", \"identity\");\n const rawBody = init?.body ?? (method !== \"GET\" && method !== \"HEAD\" ? source?.body : undefined);\n const body = await normalizeUndiciRequestBody(rawBody, headers);\n const dispatcher = (init as (RequestInit & { dispatcher?: unknown }) | undefined)?.dispatcher;\n const result = await undiciRequestImpl(url, {\n method: method as never,\n headers,\n ...(body !== undefined && body !== null ? { body: body as never } : {}),\n ...(init?.signal ? { signal: init.signal } : source?.signal ? { signal: source.signal } : {}),\n ...(dispatcher ? { dispatcher: dispatcher as never } : {}),\n maxRedirections: 0,\n });\n const iterator = result.body[Symbol.asyncIterator]();\n const responseBody = new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const chunk = await iterator.next();\n if (chunk.done) {\n controller.close();\n return;\n }\n controller.enqueue(\n chunk.value instanceof Uint8Array ? chunk.value : new Uint8Array(chunk.value),\n );\n } catch (error) {\n controller.error(error);\n }\n },\n async cancel(reason) {\n result.body.destroy(reason instanceof Error ? reason : undefined);\n await iterator.return?.();\n },\n });\n const responseHeaders = new Headers();\n for (const [name, value] of Object.entries(result.headers)) {\n if (Array.isArray(value)) {\n for (const entry of value) responseHeaders.append(name, entry);\n } else if (value !== undefined) {\n responseHeaders.set(name, value);\n }\n }\n const response = new Response(responseBody, {\n status: result.statusCode,\n headers: responseHeaders,\n });\n Object.defineProperty(response, \"url\", { value: url });\n return response;\n};\n\n/**\n * Preserve Fetch BodyInit semantics for values that Undici request() does not\n * normalize itself. URLSearchParams is otherwise mistaken for an iterable of\n * body chunks and each [name, value] tuple reaches the socket as an Array.\n */\nasync function normalizeUndiciRequestBody(\n body: BodyInit | null | undefined,\n headers: Headers,\n): Promise<BodyInit | Uint8Array | null | undefined> {\n if (body instanceof URLSearchParams) {\n if (!headers.has(\"content-type\")) {\n headers.set(\"content-type\", \"application/x-www-form-urlencoded;charset=UTF-8\");\n }\n return body.toString();\n }\n if (body instanceof Blob) {\n if (body.type && !headers.has(\"content-type\")) headers.set(\"content-type\", body.type);\n return new Uint8Array(await body.arrayBuffer());\n }\n if (body instanceof ArrayBuffer) return new Uint8Array(body);\n if (ArrayBuffer.isView(body)) {\n return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);\n }\n return body;\n}\n\n/** Undici fetch used by the pinned transport; Bun callers should prefer this over native fetch. */\nexport const undiciFetch: FetchLike = defaultFetch;\n\n/**\n * Resolve and policy-check one destination. The returned address set is the\n * complete DNS answer that the caller is allowed to use; the transport never\n * performs a second resolver call.\n */\nexport async function resolvePinnedDestination(\n rawUrl: string | URL,\n settings: OutboundNetworkSettings,\n options: ResolvePinnedDestinationOptions = {},\n): Promise<PinnedDestination> {\n const label = options.label ?? \"Outbound request\";\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new DestinationPolicyError(\"invalid_url\", `${label} URL is invalid`);\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new DestinationPolicyError(\n \"unsupported_protocol\",\n `${label} only supports http and https URLs`,\n );\n }\n const localTestEscape = isLocalTestEnvironment(settings.environment);\n if (options.requireHttpsOutsideLocalTest && !localTestEscape && url.protocol !== \"https:\") {\n throw new DestinationPolicyError(\n \"https_required\",\n `${label} must use https outside local/test`,\n );\n }\n\n const hostname = normalizeHostname(url.hostname);\n if (!hostname) {\n throw new DestinationPolicyError(\"invalid_url\", `${label} URL has no hostname`);\n }\n\n const literalFamily = isIP(hostname);\n let addresses: readonly DnsAddress[];\n try {\n addresses = literalFamily\n ? [{ address: hostname, family: literalFamily === 6 ? 6 : 4 }]\n : await (options.dnsLookup ?? defaultDnsLookup)(hostname);\n } catch (error) {\n if (error instanceof DestinationPolicyError && error.reason === \"invalid_dns_answer\") {\n throw error;\n }\n throw new DestinationPolicyError(\"dns_failed\", `${label} hostname could not be resolved`);\n }\n\n const normalizedAddresses = dedupeAddresses(addresses);\n if (normalizedAddresses.length === 0) {\n throw new DestinationPolicyError(\"dns_empty\", `${label} hostname has no addresses`);\n }\n if (normalizedAddresses.some((entry) => isInvalidAddress(entry.address))) {\n throw new DestinationPolicyError(\n \"invalid_dns_answer\",\n `${label} hostname returned an invalid address`,\n );\n }\n\n const privateEscape = localTestEscape || settings.integrationsAllowPrivateNetworkTargets === true;\n if (\n !privateEscape &&\n (isLocalHostname(hostname) ||\n normalizedAddresses.some((entry) => isNonPublicAddress(entry.address)))\n ) {\n throw new DestinationPolicyError(\n \"private_or_special_use\",\n `${label} may not target a private or special-use network address`,\n );\n }\n return { url, hostname, addresses: normalizedAddresses };\n}\n\n/**\n * Fetch through a dispatcher whose lookup is pinned to the result of exactly\n * one policy resolution. The response body owns the one-shot agent until\n * completion, cancellation, or stream failure.\n */\nexport async function pinnedFetch(\n input: string | URL | Request,\n init: RequestInit | undefined,\n settings: OutboundNetworkSettings,\n options: PinnedFetchOptions = {},\n): Promise<Response> {\n const rawUrl = input instanceof Request ? input.url : input;\n const destination = await resolvePinnedDestination(rawUrl, settings, options);\n const dispatcher =\n options.agentFactory?.(destination.addresses) ?? createPinnedAgent(destination.addresses);\n const fetchImpl = options.fetchImpl ?? pinnedRequestFetch;\n const fetchInit = {\n ...init,\n redirect: \"manual\" as const,\n dispatcher: dispatcher.dispatcher ?? dispatcher,\n } as RequestInit & { dispatcher: DispatcherLifecycle };\n let response: Response;\n try {\n response = await fetchImpl(input, fetchInit);\n } catch (error) {\n await destroyDispatcher(dispatcher, error);\n throw error;\n }\n if (!response.body) {\n await closeDispatcher(dispatcher, DISPATCHER_CLOSE_TIMEOUT_MS);\n return response;\n }\n return responseWithDispatcherLifecycle(response, dispatcher, DISPATCHER_CLOSE_TIMEOUT_MS);\n}\n\nexport function isLocalTestEnvironment(environment: string): boolean {\n return environment === \"local\" || environment === \"test\";\n}\n\n/** Return true for malformed, non-IPv4, and non-IPv6 address strings. */\nexport function isInvalidAddress(address: string): boolean {\n return isIP(stripAddressBrackets(address.trim())) === 0;\n}\n\n/**\n * Classify private, reserved, documentation, benchmark, multicast, and other\n * special-use answers. IPv4-mapped IPv6 addresses are classified through their\n * embedded IPv4 value.\n */\nexport function isNonPublicAddress(address: string): boolean {\n const normalized = stripAddressBrackets(address.trim().toLowerCase());\n const family = isIP(normalized);\n if (family === 0) {\n return true;\n }\n if (family === 4) {\n return isNonPublicIpv4(normalized);\n }\n const mappedText = ipv4FromMappedText(normalized);\n if (mappedText !== null) {\n return isNonPublicIpv4(mappedText);\n }\n const value = parseIpv6(normalized);\n if (value === null) {\n return true;\n }\n const mapped = ipv4FromMappedIpv6(value);\n if (mapped !== null) {\n return isNonPublicIpv4(mapped);\n }\n // Fail closed on unallocated/non-global IPv6 space. Current globally routed\n // unicast addresses live in 2000::/3; local, transition, multicast, and\n // future-use ranges must not become credential-bearing egress merely because\n // they were absent from a denylist.\n if (!hasIpv6Prefix(value, IPV6_GLOBAL_UNICAST_PREFIX, 3)) {\n return true;\n }\n return IPV6_SPECIAL_PREFIXES.some(([prefix, bits]) => hasIpv6Prefix(value, prefix, bits));\n}\n\n// Backwards-compatible name used by the database token-broker API.\nexport const isPrivateAddress = isNonPublicAddress;\n\nfunction createPinnedAgent(addresses: readonly DnsAddress[]): DispatcherLifecycle {\n const agent = new Agent({\n connect: {\n lookup: ((\n _hostname: string,\n options: { all?: boolean; family?: number },\n callback: (\n error: Error | null,\n address?: string | Array<{ address: string; family: number }>,\n family?: number,\n ) => void,\n ) => {\n const candidates =\n options.family === 4 || options.family === 6\n ? addresses.filter((entry) => entry.family === options.family)\n : addresses;\n if (candidates.length === 0) {\n callback(new Error(\"pinned DNS answer has no address for requested family\"));\n return;\n }\n if (options.all) {\n callback(\n null,\n candidates.map((entry) => ({\n address: entry.address,\n family: entry.family,\n })),\n );\n return;\n }\n const first = candidates[0]!;\n callback(null, first.address, first.family);\n }) as never,\n },\n });\n return {\n dispatcher: agent,\n close: async () => {\n await agent.close();\n },\n destroy: async (error) => {\n await agent.destroy(error instanceof Error ? error : null);\n },\n };\n}\n\nfunction responseWithDispatcherLifecycle(\n response: Response,\n dispatcher: DispatcherLifecycle,\n closeTimeoutMs: number,\n): Response {\n const reader = response.body!.getReader();\n let readerReleased = false;\n const releaseReader = () => {\n if (readerReleased) return;\n readerReleased = true;\n reader.releaseLock();\n };\n let disposed: Promise<void> | null = null;\n let cancelled = false;\n const finish = (destroy: boolean, error?: unknown): Promise<void> => {\n if (!disposed) {\n disposed = destroy\n ? destroyDispatcher(dispatcher, error)\n : closeDispatcher(dispatcher, closeTimeoutMs);\n }\n return disposed;\n };\n const body = new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const chunk = await reader.read();\n if (chunk.done) {\n // The dispatcher cannot finish gracefully while OpenGeni still owns\n // the provider response's reader lock. Release it before teardown;\n // otherwise Undici can wait forever after the complete body arrived.\n releaseReader();\n await finish(cancelled);\n controller.close();\n return;\n }\n controller.enqueue(chunk.value);\n } catch (error) {\n releaseReader();\n await finish(true, error);\n controller.error(error);\n }\n },\n async cancel(reason) {\n cancelled = true;\n try {\n await reader.cancel(reason);\n } finally {\n releaseReader();\n await finish(true, reason);\n }\n },\n });\n const wrapped = new Response(body, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n Object.defineProperties(wrapped, {\n redirected: { value: response.redirected },\n type: { value: response.type },\n url: { value: response.url },\n });\n return wrapped;\n}\n\nasync function closeDispatcher(dispatcher: DispatcherLifecycle, timeoutMs: number): Promise<void> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n const outcome = await Promise.race([\n Promise.resolve()\n .then(() => dispatcher.close())\n .then(\n () => \"closed\" as const,\n () => \"failed\" as const,\n ),\n new Promise<\"timed_out\">((resolve) => {\n timeout = setTimeout(() => resolve(\"timed_out\"), timeoutMs);\n }),\n ]);\n if (timeout) clearTimeout(timeout);\n if (outcome !== \"closed\") {\n // Destruction starts immediately. Do not await a provider/dispatcher\n // shutdown promise after the bounded graceful-close deadline has elapsed.\n void destroyDispatcher(dispatcher);\n }\n}\n\nasync function destroyDispatcher(dispatcher: DispatcherLifecycle, error?: unknown): Promise<void> {\n try {\n await dispatcher.destroy(error);\n } catch {\n // Cleanup is best effort after the dispatcher has already failed.\n }\n}\n\nasync function cancelResponse(response: Response): Promise<void> {\n await response.body?.cancel().catch(() => undefined);\n}\n\nfunction normalizeHostname(hostname: string): string {\n return stripAddressBrackets(hostname.trim().toLowerCase()).replace(/\\.$/, \"\");\n}\n\nfunction stripAddressBrackets(address: string): string {\n return address.startsWith(\"[\") && address.endsWith(\"]\") ? address.slice(1, -1) : address;\n}\n\nfunction isLocalHostname(hostname: string): boolean {\n return hostname === \"localhost\" || hostname.endsWith(\".localhost\");\n}\n\nfunction isLoopbackHostname(hostname: string): boolean {\n const normalized = normalizeHostname(hostname);\n return (\n normalized === \"localhost\" ||\n normalized.endsWith(\".localhost\") ||\n normalized === \"::1\" ||\n isLoopbackIpv4(normalized)\n );\n}\n\nfunction isLoopbackIpv4(hostname: string): boolean {\n if (isIP(hostname) !== 4) return false;\n return hostname.split(\".\")[0] === \"127\";\n}\n\nfunction dedupeAddresses(addresses: readonly DnsAddress[]): DnsAddress[] {\n const out: DnsAddress[] = [];\n const seen = new Set<string>();\n if (!Array.isArray(addresses)) {\n throw invalidDnsAnswer();\n }\n for (const entry of addresses) {\n const normalized = normalizeDnsAnswer(entry);\n const key = `${normalized.family}:${normalized.address}`;\n if (!seen.has(key)) {\n seen.add(key);\n out.push(normalized);\n }\n }\n return out;\n}\n\n/**\n * Validate resolver metadata before it can influence either policy checks or\n * the pinned dispatcher. Never infer a family from the address while retaining\n * a conflicting resolver claim: an invalid answer is an invalid answer.\n */\nfunction normalizeDnsAnswer(entry: unknown): DnsAddress {\n if (!entry || typeof entry !== \"object\") {\n throw invalidDnsAnswer();\n }\n const candidate = entry as { address?: unknown; family?: unknown };\n if (typeof candidate.address !== \"string\") {\n throw invalidDnsAnswer();\n }\n const family = candidate.family;\n if (family !== 4 && family !== 6) {\n throw invalidDnsAnswer();\n }\n const address = stripAddressBrackets(candidate.address.trim().toLowerCase());\n const actualFamily = isIP(address);\n if (actualFamily !== family) {\n throw invalidDnsAnswer();\n }\n return { address, family };\n}\n\nfunction invalidDnsAnswer(): DestinationPolicyError {\n return new DestinationPolicyError(\n \"invalid_dns_answer\",\n \"hostname returned an invalid DNS answer\",\n );\n}\n\nfunction isNonPublicIpv4(address: string): boolean {\n const parts = address.split(\".\").map(Number);\n if (\n parts.length !== 4 ||\n parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)\n ) {\n return true;\n }\n const value = (parts[0]! << 24) | (parts[1]! << 16) | (parts[2]! << 8) | parts[3]!;\n const unsigned = value >>> 0;\n return (\n inIpv4Range(unsigned, 0x00000000, 0x00ffffff) ||\n inIpv4Range(unsigned, 0x0a000000, 0x0affffff) ||\n inIpv4Range(unsigned, 0x64400000, 0x647fffff) ||\n inIpv4Range(unsigned, 0x7f000000, 0x7fffffff) ||\n inIpv4Range(unsigned, 0xa9fe0000, 0xa9feffff) ||\n inIpv4Range(unsigned, 0xac100000, 0xac1fffff) ||\n inIpv4Range(unsigned, 0xc0000000, 0xc00000ff) ||\n inIpv4Range(unsigned, 0xc0000200, 0xc00002ff) ||\n inIpv4Range(unsigned, 0xc01fc400, 0xc01fc4ff) ||\n inIpv4Range(unsigned, 0xc034c100, 0xc034c1ff) ||\n inIpv4Range(unsigned, 0xc0586300, 0xc05863ff) ||\n inIpv4Range(unsigned, 0xc0a80000, 0xc0a8ffff) ||\n inIpv4Range(unsigned, 0xc0af3000, 0xc0af30ff) ||\n inIpv4Range(unsigned, 0xc6120000, 0xc613ffff) ||\n inIpv4Range(unsigned, 0xc6336400, 0xc63364ff) ||\n inIpv4Range(unsigned, 0xcb007100, 0xcb0071ff) ||\n inIpv4Range(unsigned, 0xe0000000, 0xffffffff)\n );\n}\n\nfunction inIpv4Range(value: number, start: number, end: number): boolean {\n return value >= start && value <= end;\n}\n\nconst IPV6_GLOBAL_UNICAST_PREFIX = ipv6Constant(\"2000::\");\n\nconst IPV6_SPECIAL_PREFIXES: readonly [bigint, number][] = [\n [ipv6Constant(\"::\"), 96],\n [ipv6Constant(\"64:ff9b::\"), 96],\n [ipv6Constant(\"64:ff9b:1::\"), 48],\n [ipv6Constant(\"100::\"), 64],\n // IETF protocol assignments contain globally reachable carve-outs, but they\n // are control-plane anycast/protocol addresses rather than integration\n // endpoints. Credential-bearing MCP/OAuth egress fails closed on the full\n // special-purpose block.\n [ipv6Constant(\"2001::\"), 23],\n [ipv6Constant(\"2001:db8::\"), 32],\n [ipv6Constant(\"2002::\"), 16],\n [ipv6Constant(\"2620:4f:8000::\"), 48],\n [ipv6Constant(\"3ffe::\"), 16],\n [ipv6Constant(\"3fff::\"), 20],\n [ipv6Constant(\"5f00::\"), 16],\n [ipv6Constant(\"fc00::\"), 7],\n [ipv6Constant(\"fe80::\"), 10],\n [ipv6Constant(\"fec0::\"), 10],\n [ipv6Constant(\"ff00::\"), 8],\n];\n\nfunction hasIpv6Prefix(value: bigint, prefix: bigint, bits: number): boolean {\n const shift = 128n - BigInt(bits);\n return value >> shift === prefix >> shift;\n}\n\nfunction ipv4FromMappedIpv6(value: bigint): string | null {\n if (value >> 32n !== 0xffffn) {\n return null;\n }\n const embedded = Number(value & 0xffffffffn);\n return `${embedded >>> 24}.${(embedded >>> 16) & 0xff}.${(embedded >>> 8) & 0xff}.${embedded & 0xff}`;\n}\n\nfunction ipv4FromMappedText(address: string): string | null {\n if (!address.startsWith(\"::ffff:\")) {\n return null;\n }\n const embedded = address.slice(\"::ffff:\".length);\n if (isIP(embedded) === 4) {\n return embedded;\n }\n const parts = embedded.split(\":\");\n if (parts.length !== 2 || parts.some((part) => !/^[0-9a-f]{1,4}$/.test(part))) {\n return null;\n }\n const high = Number.parseInt(parts[0]!, 16);\n const low = Number.parseInt(parts[1]!, 16);\n return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;\n}\n\nfunction parseIpv6(address: string): bigint | null {\n const percent = address.indexOf(\"%\");\n if (percent >= 0) {\n return null;\n }\n let value = address;\n if (value.includes(\".\")) {\n const lastColon = value.lastIndexOf(\":\");\n const dotted = value.slice(lastColon + 1);\n const parts = dotted.split(\".\").map(Number);\n if (\n parts.length !== 4 ||\n parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)\n ) {\n return null;\n }\n const hex =\n ((parts[0]! << 8) | parts[1]!).toString(16).padStart(4, \"0\") +\n ((parts[2]! << 8) | parts[3]!).toString(16).padStart(4, \"0\");\n value = `${value.slice(0, lastColon + 1)}${hex}`;\n }\n const halves = value.split(\"::\");\n if (halves.length > 2) {\n return null;\n }\n const left = halves[0] ? halves[0].split(\":\") : [];\n const right = halves.length === 2 && halves[1] ? halves[1].split(\":\") : [];\n if (left.concat(right).some((part) => !/^[0-9a-f]{1,4}$/i.test(part))) {\n return null;\n }\n const missing = halves.length === 2 ? 8 - left.length - right.length : 0;\n if (missing < 0 || (halves.length === 1 && missing !== 0)) {\n return null;\n }\n const groups = [...left, ...Array.from({ length: missing }, () => \"0\"), ...right];\n if (groups.length !== 8) {\n return null;\n }\n return groups.reduce((acc, group) => (acc << 16n) | BigInt(`0x${group}`), 0n);\n}\n\nfunction ipv6Constant(address: string): bigint {\n const value = parseIpv6(address);\n if (value === null) {\n throw new Error(`invalid IPv6 constant: ${address}`);\n }\n return value;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/network",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "DNS-pinned outbound HTTP transport for OpenGeni credential-bearing requests.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"provenance": true
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
-
"build": "
|
|
33
|
-
"typecheck": "
|
|
32
|
+
"build": "bun ../../scripts/build-typescript-package.ts",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
34
|
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The explicit entrypoint avoids Bun's native `undici` compatibility shim,
|
|
2
2
|
// which exposes an Agent-shaped object without Dispatcher methods.
|
|
3
|
-
import { Agent, fetch as undiciFetchImpl } from "undici/index.js";
|
|
3
|
+
import { Agent, fetch as undiciFetchImpl, request as undiciRequestImpl } from "undici/index.js";
|
|
4
4
|
import { lookup as nodeLookup } from "node:dns/promises";
|
|
5
5
|
import { isIP } from "node:net";
|
|
6
6
|
|
|
@@ -39,6 +39,8 @@ export type PinnedFetchOptions = {
|
|
|
39
39
|
requireHttpsOutsideLocalTest?: boolean;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
const DISPATCHER_CLOSE_TIMEOUT_MS = 100;
|
|
43
|
+
|
|
42
44
|
export const OAUTH_MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
43
45
|
|
|
44
46
|
export type HttpUrlValidationOptions = {
|
|
@@ -93,6 +95,18 @@ export class ResponseBodyLimitError extends Error {
|
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
/** Raised when a caller-supplied absolute request deadline expires. */
|
|
99
|
+
export class RequestDeadlineError extends Error {
|
|
100
|
+
constructor(readonly label: string) {
|
|
101
|
+
super(`${label} timed out`);
|
|
102
|
+
this.name = "RequestDeadlineError";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type BoundedResponseReadOptions = {
|
|
107
|
+
signal?: AbortSignal;
|
|
108
|
+
};
|
|
109
|
+
|
|
96
110
|
/**
|
|
97
111
|
* Read a response through its stream with a hard byte ceiling.
|
|
98
112
|
*
|
|
@@ -105,6 +119,7 @@ export async function readResponseBodyBounded(
|
|
|
105
119
|
response: Response,
|
|
106
120
|
maxBytes: number,
|
|
107
121
|
label: string,
|
|
122
|
+
options: BoundedResponseReadOptions = {},
|
|
108
123
|
): Promise<Uint8Array> {
|
|
109
124
|
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
|
110
125
|
throw new RangeError("response body limit must be a non-negative safe integer");
|
|
@@ -131,7 +146,7 @@ export async function readResponseBodyBounded(
|
|
|
131
146
|
let receivedBytes = 0;
|
|
132
147
|
try {
|
|
133
148
|
for (;;) {
|
|
134
|
-
const result = await reader.
|
|
149
|
+
const result = await readStreamChunk(reader, options.signal, label);
|
|
135
150
|
if (result.done) {
|
|
136
151
|
break;
|
|
137
152
|
}
|
|
@@ -143,10 +158,15 @@ export async function readResponseBodyBounded(
|
|
|
143
158
|
chunks.push(result.value);
|
|
144
159
|
}
|
|
145
160
|
} catch (error) {
|
|
146
|
-
|
|
161
|
+
void reader.cancel(error).catch(() => undefined);
|
|
147
162
|
throw error;
|
|
148
163
|
} finally {
|
|
149
|
-
|
|
164
|
+
try {
|
|
165
|
+
reader.releaseLock();
|
|
166
|
+
} catch {
|
|
167
|
+
// An aborted read may still own the lock until the underlying transport
|
|
168
|
+
// observes cancellation. The response is already being cancelled above.
|
|
169
|
+
}
|
|
150
170
|
}
|
|
151
171
|
|
|
152
172
|
const body = new Uint8Array(receivedBytes);
|
|
@@ -162,16 +182,52 @@ export async function readResponseTextBounded(
|
|
|
162
182
|
response: Response,
|
|
163
183
|
maxBytes: number,
|
|
164
184
|
label: string,
|
|
185
|
+
options: BoundedResponseReadOptions = {},
|
|
165
186
|
): Promise<string> {
|
|
166
|
-
return new TextDecoder().decode(
|
|
187
|
+
return new TextDecoder().decode(
|
|
188
|
+
await readResponseBodyBounded(response, maxBytes, label, options),
|
|
189
|
+
);
|
|
167
190
|
}
|
|
168
191
|
|
|
169
192
|
export async function readResponseJsonBounded<T = unknown>(
|
|
170
193
|
response: Response,
|
|
171
194
|
maxBytes: number,
|
|
172
195
|
label: string,
|
|
196
|
+
options: BoundedResponseReadOptions = {},
|
|
173
197
|
): Promise<T> {
|
|
174
|
-
return JSON.parse(await readResponseTextBounded(response, maxBytes, label)) as T;
|
|
198
|
+
return JSON.parse(await readResponseTextBounded(response, maxBytes, label, options)) as T;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
type ResponseStreamReadResult = Awaited<
|
|
202
|
+
ReturnType<ReadableStreamDefaultReader<Uint8Array>["read"]>
|
|
203
|
+
>;
|
|
204
|
+
|
|
205
|
+
async function readStreamChunk(
|
|
206
|
+
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
207
|
+
signal: AbortSignal | undefined,
|
|
208
|
+
label: string,
|
|
209
|
+
): Promise<ResponseStreamReadResult> {
|
|
210
|
+
if (!signal) return await reader.read();
|
|
211
|
+
if (signal.aborted) throw new RequestDeadlineError(label);
|
|
212
|
+
return await new Promise<ResponseStreamReadResult>((resolve, reject) => {
|
|
213
|
+
let settled = false;
|
|
214
|
+
const finish = (callback: () => void) => {
|
|
215
|
+
if (settled) return;
|
|
216
|
+
settled = true;
|
|
217
|
+
signal.removeEventListener("abort", onAbort);
|
|
218
|
+
callback();
|
|
219
|
+
};
|
|
220
|
+
const onAbort = () => {
|
|
221
|
+
const error = new RequestDeadlineError(label);
|
|
222
|
+
void reader.cancel(error).catch(() => undefined);
|
|
223
|
+
finish(() => reject(error));
|
|
224
|
+
};
|
|
225
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
226
|
+
void reader.read().then(
|
|
227
|
+
(result) => finish(() => resolve(result)),
|
|
228
|
+
(error) => finish(() => reject(error)),
|
|
229
|
+
);
|
|
230
|
+
});
|
|
175
231
|
}
|
|
176
232
|
|
|
177
233
|
export type ResolvePinnedDestinationOptions = {
|
|
@@ -207,6 +263,91 @@ const defaultDnsLookup: DnsLookup = async (hostname) => {
|
|
|
207
263
|
const defaultFetch: FetchLike = (input, init) =>
|
|
208
264
|
(undiciFetchImpl as unknown as FetchLike)(input, init);
|
|
209
265
|
|
|
266
|
+
const pinnedRequestFetch: FetchLike = async (input, init) => {
|
|
267
|
+
const source = input instanceof Request ? input : null;
|
|
268
|
+
const url = source?.url ?? input.toString();
|
|
269
|
+
const method = init?.method ?? source?.method ?? "GET";
|
|
270
|
+
const headers = new Headers(source?.headers);
|
|
271
|
+
if (init?.headers) {
|
|
272
|
+
new Headers(init.headers).forEach((value, name) => headers.set(name, value));
|
|
273
|
+
}
|
|
274
|
+
// Undici request() deliberately does not auto-decompress. Asking providers
|
|
275
|
+
// for identity encoding keeps the returned web Response body truthful.
|
|
276
|
+
if (!headers.has("accept-encoding")) headers.set("accept-encoding", "identity");
|
|
277
|
+
const rawBody = init?.body ?? (method !== "GET" && method !== "HEAD" ? source?.body : undefined);
|
|
278
|
+
const body = await normalizeUndiciRequestBody(rawBody, headers);
|
|
279
|
+
const dispatcher = (init as (RequestInit & { dispatcher?: unknown }) | undefined)?.dispatcher;
|
|
280
|
+
const result = await undiciRequestImpl(url, {
|
|
281
|
+
method: method as never,
|
|
282
|
+
headers,
|
|
283
|
+
...(body !== undefined && body !== null ? { body: body as never } : {}),
|
|
284
|
+
...(init?.signal ? { signal: init.signal } : source?.signal ? { signal: source.signal } : {}),
|
|
285
|
+
...(dispatcher ? { dispatcher: dispatcher as never } : {}),
|
|
286
|
+
maxRedirections: 0,
|
|
287
|
+
});
|
|
288
|
+
const iterator = result.body[Symbol.asyncIterator]();
|
|
289
|
+
const responseBody = new ReadableStream<Uint8Array>({
|
|
290
|
+
async pull(controller) {
|
|
291
|
+
try {
|
|
292
|
+
const chunk = await iterator.next();
|
|
293
|
+
if (chunk.done) {
|
|
294
|
+
controller.close();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
controller.enqueue(
|
|
298
|
+
chunk.value instanceof Uint8Array ? chunk.value : new Uint8Array(chunk.value),
|
|
299
|
+
);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
controller.error(error);
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
async cancel(reason) {
|
|
305
|
+
result.body.destroy(reason instanceof Error ? reason : undefined);
|
|
306
|
+
await iterator.return?.();
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
const responseHeaders = new Headers();
|
|
310
|
+
for (const [name, value] of Object.entries(result.headers)) {
|
|
311
|
+
if (Array.isArray(value)) {
|
|
312
|
+
for (const entry of value) responseHeaders.append(name, entry);
|
|
313
|
+
} else if (value !== undefined) {
|
|
314
|
+
responseHeaders.set(name, value);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const response = new Response(responseBody, {
|
|
318
|
+
status: result.statusCode,
|
|
319
|
+
headers: responseHeaders,
|
|
320
|
+
});
|
|
321
|
+
Object.defineProperty(response, "url", { value: url });
|
|
322
|
+
return response;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Preserve Fetch BodyInit semantics for values that Undici request() does not
|
|
327
|
+
* normalize itself. URLSearchParams is otherwise mistaken for an iterable of
|
|
328
|
+
* body chunks and each [name, value] tuple reaches the socket as an Array.
|
|
329
|
+
*/
|
|
330
|
+
async function normalizeUndiciRequestBody(
|
|
331
|
+
body: BodyInit | null | undefined,
|
|
332
|
+
headers: Headers,
|
|
333
|
+
): Promise<BodyInit | Uint8Array | null | undefined> {
|
|
334
|
+
if (body instanceof URLSearchParams) {
|
|
335
|
+
if (!headers.has("content-type")) {
|
|
336
|
+
headers.set("content-type", "application/x-www-form-urlencoded;charset=UTF-8");
|
|
337
|
+
}
|
|
338
|
+
return body.toString();
|
|
339
|
+
}
|
|
340
|
+
if (body instanceof Blob) {
|
|
341
|
+
if (body.type && !headers.has("content-type")) headers.set("content-type", body.type);
|
|
342
|
+
return new Uint8Array(await body.arrayBuffer());
|
|
343
|
+
}
|
|
344
|
+
if (body instanceof ArrayBuffer) return new Uint8Array(body);
|
|
345
|
+
if (ArrayBuffer.isView(body)) {
|
|
346
|
+
return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
347
|
+
}
|
|
348
|
+
return body;
|
|
349
|
+
}
|
|
350
|
+
|
|
210
351
|
/** Undici fetch used by the pinned transport; Bun callers should prefer this over native fetch. */
|
|
211
352
|
export const undiciFetch: FetchLike = defaultFetch;
|
|
212
353
|
|
|
@@ -286,8 +427,8 @@ export async function resolvePinnedDestination(
|
|
|
286
427
|
|
|
287
428
|
/**
|
|
288
429
|
* Fetch through a dispatcher whose lookup is pinned to the result of exactly
|
|
289
|
-
* one policy resolution. The response body owns the agent until
|
|
290
|
-
* cancellation, or stream failure.
|
|
430
|
+
* one policy resolution. The response body owns the one-shot agent until
|
|
431
|
+
* completion, cancellation, or stream failure.
|
|
291
432
|
*/
|
|
292
433
|
export async function pinnedFetch(
|
|
293
434
|
input: string | URL | Request,
|
|
@@ -299,7 +440,7 @@ export async function pinnedFetch(
|
|
|
299
440
|
const destination = await resolvePinnedDestination(rawUrl, settings, options);
|
|
300
441
|
const dispatcher =
|
|
301
442
|
options.agentFactory?.(destination.addresses) ?? createPinnedAgent(destination.addresses);
|
|
302
|
-
const fetchImpl = options.fetchImpl ??
|
|
443
|
+
const fetchImpl = options.fetchImpl ?? pinnedRequestFetch;
|
|
303
444
|
const fetchInit = {
|
|
304
445
|
...init,
|
|
305
446
|
redirect: "manual" as const,
|
|
@@ -313,10 +454,10 @@ export async function pinnedFetch(
|
|
|
313
454
|
throw error;
|
|
314
455
|
}
|
|
315
456
|
if (!response.body) {
|
|
316
|
-
await closeDispatcher(dispatcher);
|
|
457
|
+
await closeDispatcher(dispatcher, DISPATCHER_CLOSE_TIMEOUT_MS);
|
|
317
458
|
return response;
|
|
318
459
|
}
|
|
319
|
-
return responseWithDispatcherLifecycle(response, dispatcher);
|
|
460
|
+
return responseWithDispatcherLifecycle(response, dispatcher, DISPATCHER_CLOSE_TIMEOUT_MS);
|
|
320
461
|
}
|
|
321
462
|
|
|
322
463
|
export function isLocalTestEnvironment(environment: string): boolean {
|
|
@@ -390,7 +531,10 @@ function createPinnedAgent(addresses: readonly DnsAddress[]): DispatcherLifecycl
|
|
|
390
531
|
if (options.all) {
|
|
391
532
|
callback(
|
|
392
533
|
null,
|
|
393
|
-
candidates.map((entry) => ({
|
|
534
|
+
candidates.map((entry) => ({
|
|
535
|
+
address: entry.address,
|
|
536
|
+
family: entry.family,
|
|
537
|
+
})),
|
|
394
538
|
);
|
|
395
539
|
return;
|
|
396
540
|
}
|
|
@@ -413,13 +557,22 @@ function createPinnedAgent(addresses: readonly DnsAddress[]): DispatcherLifecycl
|
|
|
413
557
|
function responseWithDispatcherLifecycle(
|
|
414
558
|
response: Response,
|
|
415
559
|
dispatcher: DispatcherLifecycle,
|
|
560
|
+
closeTimeoutMs: number,
|
|
416
561
|
): Response {
|
|
417
562
|
const reader = response.body!.getReader();
|
|
563
|
+
let readerReleased = false;
|
|
564
|
+
const releaseReader = () => {
|
|
565
|
+
if (readerReleased) return;
|
|
566
|
+
readerReleased = true;
|
|
567
|
+
reader.releaseLock();
|
|
568
|
+
};
|
|
418
569
|
let disposed: Promise<void> | null = null;
|
|
419
570
|
let cancelled = false;
|
|
420
571
|
const finish = (destroy: boolean, error?: unknown): Promise<void> => {
|
|
421
572
|
if (!disposed) {
|
|
422
|
-
disposed = destroy
|
|
573
|
+
disposed = destroy
|
|
574
|
+
? destroyDispatcher(dispatcher, error)
|
|
575
|
+
: closeDispatcher(dispatcher, closeTimeoutMs);
|
|
423
576
|
}
|
|
424
577
|
return disposed;
|
|
425
578
|
};
|
|
@@ -428,12 +581,17 @@ function responseWithDispatcherLifecycle(
|
|
|
428
581
|
try {
|
|
429
582
|
const chunk = await reader.read();
|
|
430
583
|
if (chunk.done) {
|
|
584
|
+
// The dispatcher cannot finish gracefully while OpenGeni still owns
|
|
585
|
+
// the provider response's reader lock. Release it before teardown;
|
|
586
|
+
// otherwise Undici can wait forever after the complete body arrived.
|
|
587
|
+
releaseReader();
|
|
431
588
|
await finish(cancelled);
|
|
432
589
|
controller.close();
|
|
433
590
|
return;
|
|
434
591
|
}
|
|
435
592
|
controller.enqueue(chunk.value);
|
|
436
593
|
} catch (error) {
|
|
594
|
+
releaseReader();
|
|
437
595
|
await finish(true, error);
|
|
438
596
|
controller.error(error);
|
|
439
597
|
}
|
|
@@ -443,6 +601,7 @@ function responseWithDispatcherLifecycle(
|
|
|
443
601
|
try {
|
|
444
602
|
await reader.cancel(reason);
|
|
445
603
|
} finally {
|
|
604
|
+
releaseReader();
|
|
446
605
|
await finish(true, reason);
|
|
447
606
|
}
|
|
448
607
|
},
|
|
@@ -460,11 +619,24 @@ function responseWithDispatcherLifecycle(
|
|
|
460
619
|
return wrapped;
|
|
461
620
|
}
|
|
462
621
|
|
|
463
|
-
async function closeDispatcher(dispatcher: DispatcherLifecycle): Promise<void> {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
622
|
+
async function closeDispatcher(dispatcher: DispatcherLifecycle, timeoutMs: number): Promise<void> {
|
|
623
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
624
|
+
const outcome = await Promise.race([
|
|
625
|
+
Promise.resolve()
|
|
626
|
+
.then(() => dispatcher.close())
|
|
627
|
+
.then(
|
|
628
|
+
() => "closed" as const,
|
|
629
|
+
() => "failed" as const,
|
|
630
|
+
),
|
|
631
|
+
new Promise<"timed_out">((resolve) => {
|
|
632
|
+
timeout = setTimeout(() => resolve("timed_out"), timeoutMs);
|
|
633
|
+
}),
|
|
634
|
+
]);
|
|
635
|
+
if (timeout) clearTimeout(timeout);
|
|
636
|
+
if (outcome !== "closed") {
|
|
637
|
+
// Destruction starts immediately. Do not await a provider/dispatcher
|
|
638
|
+
// shutdown promise after the bounded graceful-close deadline has elapsed.
|
|
639
|
+
void destroyDispatcher(dispatcher);
|
|
468
640
|
}
|
|
469
641
|
}
|
|
470
642
|
|