@cortexkit/aft-opencode 0.18.2 → 0.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,7 @@ import {
10
10
  } from "node:fs";
11
11
  import { isIP } from "node:net";
12
12
  import { join } from "node:path";
13
+ import { Agent, type Dispatcher, fetch as undiciFetch } from "undici";
13
14
  import { log, warn } from "../logger";
14
15
 
15
16
  /** Max response body size (10 MB) */
@@ -22,8 +23,24 @@ const MAX_REDIRECTS = 5;
22
23
 
23
24
  interface FetchUrlOptions {
24
25
  allowPrivate?: boolean;
26
+ /** Test-only injection point. Production fetches use undici with DNS pinning. */
27
+ fetchImpl?: FetchImpl;
28
+ /** Test-only injection point. Production DNS resolution uses node:dns/promises.lookup. */
29
+ lookup?: LookupFn;
30
+ /** Test-only injection point for observing the DNS-pinned dispatcher. */
31
+ dispatcherFactory?: (validatedIp: string) => Dispatcher;
25
32
  }
26
33
 
34
+ type LookupFn = typeof lookup;
35
+ interface FetchInit {
36
+ signal?: AbortSignal;
37
+ redirect?: "manual";
38
+ dispatcher?: Dispatcher;
39
+ headers?: Record<string, string>;
40
+ }
41
+
42
+ type FetchImpl = (input: string, init: FetchInit) => Promise<Response>;
43
+
27
44
  interface CacheMeta {
28
45
  url: string;
29
46
  contentType: string;
@@ -163,11 +180,15 @@ function isPrivateIp(address: string): boolean {
163
180
  return true;
164
181
  }
165
182
 
166
- async function assertPublicUrl(url: URL, allowPrivate: boolean): Promise<void> {
183
+ async function assertPublicUrl(
184
+ url: URL,
185
+ allowPrivate: boolean,
186
+ dnsLookup: LookupFn = lookup,
187
+ ): Promise<string | undefined> {
167
188
  if (url.protocol !== "http:" && url.protocol !== "https:") {
168
189
  throw new Error(`Only http:// and https:// URLs are supported, got: ${url.protocol}`);
169
190
  }
170
- if (allowPrivate) return;
191
+ if (allowPrivate) return undefined;
171
192
 
172
193
  // URL.hostname returns IPv6 literals wrapped in brackets ("[::1]") per WHATWG.
173
194
  // Strip them before checking the address.
@@ -180,15 +201,29 @@ async function assertPublicUrl(url: URL, allowPrivate: boolean): Promise<void> {
180
201
  if (isPrivateIp(hostname)) {
181
202
  throw new Error(`Blocked private URL host ${url.hostname} (${hostname})`);
182
203
  }
183
- return;
204
+ return hostname;
184
205
  }
185
206
 
186
- const addresses = await lookup(hostname, { all: true, verbatim: true });
207
+ const addresses = await dnsLookup(hostname, { all: true, verbatim: true });
187
208
  for (const { address } of addresses) {
188
209
  if (isPrivateIp(address)) {
189
210
  throw new Error(`Blocked private URL host ${url.hostname} (${address})`);
190
211
  }
191
212
  }
213
+ if (addresses.length === 0) {
214
+ throw new Error(`Failed to resolve URL host ${url.hostname}`);
215
+ }
216
+ return addresses[0].address;
217
+ }
218
+
219
+ function createPinnedDispatcher(validatedIp: string): Dispatcher {
220
+ return new Agent({
221
+ connect: {
222
+ lookup: (_hostname, _opts, callback) => {
223
+ callback(null, validatedIp, validatedIp.includes(":") ? 6 : 4);
224
+ },
225
+ },
226
+ });
192
227
  }
193
228
 
194
229
  function resolveRedirectUrl(currentUrl: URL, location: string | null): URL {
@@ -198,19 +233,29 @@ function resolveRedirectUrl(currentUrl: URL, location: string | null): URL {
198
233
  return new URL(location, currentUrl);
199
234
  }
200
235
 
201
- async function fetchWithRedirects(startUrl: URL, allowPrivate: boolean): Promise<Response> {
236
+ async function fetchWithRedirects(
237
+ startUrl: URL,
238
+ allowPrivate: boolean,
239
+ deps: Pick<FetchUrlOptions, "dispatcherFactory" | "fetchImpl" | "lookup"> = {},
240
+ ): Promise<Response> {
202
241
  let currentUrl = startUrl;
242
+ const fetchImpl = deps.fetchImpl ?? (undiciFetch as FetchImpl);
243
+ const dnsLookup = deps.lookup ?? lookup;
203
244
 
204
245
  for (let redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount++) {
205
- await assertPublicUrl(currentUrl, allowPrivate);
246
+ const validatedIp = await assertPublicUrl(currentUrl, allowPrivate, dnsLookup);
247
+ const dispatcher = validatedIp
248
+ ? (deps.dispatcherFactory ?? createPinnedDispatcher)(validatedIp)
249
+ : undefined;
206
250
 
207
251
  const controller = new AbortController();
208
252
  const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
209
253
  let response: Response;
210
254
  try {
211
- response = await fetch(currentUrl.href, {
255
+ response = await fetchImpl(currentUrl.href, {
212
256
  signal: controller.signal,
213
257
  redirect: "manual",
258
+ dispatcher,
214
259
  headers: {
215
260
  "user-agent": "aft-opencode-plugin",
216
261
  // Prioritize markdown for content-negotiating servers (GitHub API, many docs sites).
@@ -316,7 +361,7 @@ export async function fetchUrlToTempFile(
316
361
  }
317
362
 
318
363
  log(`Fetching URL: ${url}`);
319
- const response = await fetchWithRedirects(parsed, allowPrivate);
364
+ const response = await fetchWithRedirects(parsed, allowPrivate, options);
320
365
 
321
366
  if (!response.ok) {
322
367
  throw new Error(`HTTP ${response.status} ${response.statusText} fetching ${url}`);