abelworkflow 1.0.0 → 1.1.1

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.
@@ -1,23 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { createNodeInsecureDispatcher, installProviderTlsFetch } from "./tls-fetch.mjs";
3
-
4
- let nodeInsecureDispatcher: unknown;
5
-
6
- function getNodeInsecureDispatcher() {
7
- if (nodeInsecureDispatcher) return nodeInsecureDispatcher;
8
- nodeInsecureDispatcher = createNodeInsecureDispatcher();
9
- return nodeInsecureDispatcher;
10
- }
11
2
 
12
3
  export default function (pi: ExtensionAPI) {
13
- if (typeof globalThis.fetch === "function") {
14
- const runtime = typeof (globalThis as any).Bun === "undefined" ? "node" : "bun";
15
- installProviderTlsFetch({
16
- runtime,
17
- insecureDispatcher: runtime === "node" ? getNodeInsecureDispatcher : undefined
18
- });
19
- }
20
-
21
4
  pi.on("before_provider_request", (event, ctx) => {
22
5
  const payload = event.payload as any;
23
6
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return;
@@ -62,23 +62,12 @@ function mergeClaudeSettingsWithDefaults(settings) {
62
62
  };
63
63
  }
64
64
 
65
- function applyClaudeInsecureTlsSetting(env = {}, enabled = false) {
66
- const nextEnv = { ...env };
67
- if (enabled) {
68
- nextEnv.NODE_TLS_REJECT_UNAUTHORIZED = "0";
69
- } else if (nextEnv.NODE_TLS_REJECT_UNAUTHORIZED === "0") {
70
- delete nextEnv.NODE_TLS_REJECT_UNAUTHORIZED;
71
- }
72
- return nextEnv;
73
- }
74
-
75
65
  function getExistingClaudeApiConfig(settings) {
76
66
  const env = mergeClaudeSettingsWithDefaults(settings).env;
77
67
  return {
78
68
  baseUrl: env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
79
69
  key: env.ANTHROPIC_API_KEY || "",
80
- model: claudeModelEnvKeys.map((field) => env[field]).find(Boolean) || "",
81
- insecureTls: env.NODE_TLS_REJECT_UNAUTHORIZED === "0"
70
+ model: claudeModelEnvKeys.map((field) => env[field]).find(Boolean) || ""
82
71
  };
83
72
  }
84
73
 
@@ -109,14 +98,9 @@ function ensureApprovedClaudeApiKey(config, apiKey) {
109
98
  function buildClaudeApiSettings(settings, {
110
99
  baseUrl,
111
100
  key,
112
- model,
113
- insecureTls = false
101
+ model
114
102
  }) {
115
103
  const nextSettings = mergeClaudeSettingsWithDefaults(settings);
116
- nextSettings.env = applyClaudeInsecureTlsSetting(
117
- nextSettings.env && typeof nextSettings.env === "object" ? nextSettings.env : {},
118
- insecureTls
119
- );
120
104
  nextSettings.env.ANTHROPIC_BASE_URL = baseUrl;
121
105
  nextSettings.env.ANTHROPIC_API_KEY = key;
122
106
  delete nextSettings.env.ANTHROPIC_AUTH_TOKEN;
@@ -134,7 +118,6 @@ async function persistClaudeConfiguration(paths, { settings, metaConfig }) {
134
118
  async function configureClaudeApi(paths = defaultPaths, promptApi) {
135
119
  const {
136
120
  assertNotCancelled,
137
- confirmOrCancel,
138
121
  passwordPromptOptions,
139
122
  required,
140
123
  requiredUnlessExisting,
@@ -158,11 +141,6 @@ async function configureClaudeApi(paths = defaultPaths, promptApi) {
158
141
  assertNotCancelled(key);
159
142
  const finalKey = resolvePasswordValue(key, existing.key);
160
143
 
161
- const insecureTls = await confirmOrCancel({
162
- message: "是否跳过 Claude Code TLS 证书校验?仅证书无法修复时启用(会放宽该进程全部 HTTPS 请求)",
163
- initialValue: existing.insecureTls
164
- });
165
-
166
144
  const model = await p.text({
167
145
  message: "Claude Code 模型",
168
146
  initialValue: existing.model || undefined,
@@ -173,8 +151,7 @@ async function configureClaudeApi(paths = defaultPaths, promptApi) {
173
151
  const apiSettings = buildClaudeApiSettings(settings, {
174
152
  baseUrl,
175
153
  key: finalKey,
176
- model,
177
- insecureTls
154
+ model
178
155
  });
179
156
  const metaConfig = await readJsonFileSafe(paths.claudeMetaConfigPath, {}, { sensitive: true });
180
157
  metaConfig.hasCompletedOnboarding = true;
@@ -188,7 +165,6 @@ async function configureClaudeApi(paths = defaultPaths, promptApi) {
188
165
  }
189
166
 
190
167
  export {
191
- applyClaudeInsecureTlsSetting,
192
168
  buildClaudeApiSettings,
193
169
  buildDefaultClaudeSettings,
194
170
  configureClaudeApi,
@@ -25,6 +25,8 @@ import {
25
25
  import { defaultPaths, maskSecret, pathToLabel } from "../paths.mjs";
26
26
  import { normalizeOpenAiBaseUrl } from "./pi.mjs";
27
27
 
28
+ const CODEX_ENV_KEY = "OPENAI_API_KEY";
29
+
28
30
  const publishedCodexDeveloperInstructionHashes = new Set([
29
31
  "957adb227dd3fc298985acca38db4c22b8e018d387dab91f1ea8287db35385db"
30
32
  ]);
@@ -249,13 +251,6 @@ async function deployBundledCodexAgents(paths = defaultPaths, previousManagedFil
249
251
  return result;
250
252
  }
251
253
 
252
- function getDefaultCodexEnvKey(providerId) {
253
- if (providerId === "openai") return "OPENAI_API_KEY";
254
- if (providerId === "abelworkflow") return "ABELWORKFLOW_API_KEY";
255
- const digest = createHash("sha256").update(providerId).digest("hex").toUpperCase();
256
- return `ABELWORKFLOW_PROVIDER_${digest}_API_KEY`;
257
- }
258
-
259
254
  function getCodexProviderSectionName(providerId) {
260
255
  return `model_providers.${formatTomlKeySegment(providerId)}`;
261
256
  }
@@ -275,8 +270,7 @@ function assertCodexAuthKeyAvailable(auth, envKey) {
275
270
  function resolveExistingCodexApiConfig(content, auth = {}) {
276
271
  const providerId = readTopLevelTomlString(content, "model_provider") || "abelworkflow";
277
272
  const provider = parseTomlSection(content, getCodexProviderSectionName(providerId));
278
- const configuredEnvKey = provider.temp_env_key || "";
279
- const envKey = configuredEnvKey || getDefaultCodexEnvKey(providerId);
273
+ const envKey = CODEX_ENV_KEY;
280
274
  assertCodexAuthKeyAvailable(auth, envKey);
281
275
  const apiKey = typeof auth[envKey] === "string" ? auth[envKey] : "";
282
276
 
@@ -331,7 +325,7 @@ async function configureCodexApi(paths = defaultPaths, promptApi, ownership = {}
331
325
  const shouldDeploySubagents = await confirmOrCancel({ message: "是否部署 Codex subagents 配置?", initialValue: true });
332
326
  let managedCodexAgentFiles = { ...(ownership.managedCodexAgentFiles ?? {}) };
333
327
 
334
- const envKey = existing.envKey || "OPENAI_API_KEY";
328
+ const envKey = CODEX_ENV_KEY;
335
329
  const currentContent = await pathExists(paths.codexConfigPath) ? await readFile(paths.codexConfigPath, "utf8") : "";
336
330
  const templateContent = await loadBundledCodexConfigTemplate(paths);
337
331
  const content = buildCodexConfigContent(currentContent, {
@@ -340,8 +334,7 @@ async function configureCodexApi(paths = defaultPaths, promptApi, ownership = {}
340
334
  includeSubagentDefaults: shouldDeploySubagents,
341
335
  providerId,
342
336
  providerName,
343
- baseUrl,
344
- envKey
337
+ baseUrl
345
338
  });
346
339
 
347
340
  const auth = mergeCodexAuthData(
@@ -382,8 +375,7 @@ function buildCodexConfigContent(currentContent, {
382
375
  includeSubagentDefaults = true,
383
376
  providerId,
384
377
  providerName,
385
- baseUrl,
386
- envKey
378
+ baseUrl
387
379
  }) {
388
380
  const effectiveTemplateContent = includeSubagentDefaults
389
381
  ? templateContent
@@ -403,7 +395,7 @@ function buildCodexConfigContent(currentContent, {
403
395
  name: providerName,
404
396
  base_url: baseUrl,
405
397
  wire_api: "responses",
406
- temp_env_key: envKey,
398
+ temp_env_key: CODEX_ENV_KEY,
407
399
  requires_openai_auth: true,
408
400
  supports_websockets: true
409
401
  });
@@ -1,7 +1,6 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
2
  import { join } from "node:path";
3
3
  import * as p from "@clack/prompts";
4
- import { piInsecureTlsHeader } from "../../extensions/pi-gpt-responses-compat/tls-fetch.mjs";
5
4
  import {
6
5
  readJsonFileSafe,
7
6
  readJsoncFileSafe,
@@ -310,36 +309,17 @@ function buildPiModelConfig(modelId, existingModel = {}) {
310
309
  };
311
310
  }
312
311
 
313
- function hasPiInsecureTlsSetting(modelsConfig = {}, providerId) {
314
- const headers = modelsConfig.providers?.[requirePiProviderId(providerId)]?.headers;
315
- return headers && typeof headers === "object"
316
- ? Object.keys(headers).some((key) => key.toLowerCase() === piInsecureTlsHeader)
317
- : false;
318
- }
319
-
320
312
  function buildPiModelsConfig(modelsConfig = {}, {
321
313
  providerId,
322
314
  baseUrl,
323
315
  api,
324
- modelIds,
325
- insecureTls = false
316
+ modelIds
326
317
  }) {
327
318
  const targetProviderId = requirePiProviderId(providerId);
328
319
  const providers = modelsConfig.providers && typeof modelsConfig.providers === "object" ? modelsConfig.providers : {};
329
320
  const currentProvider = providers[targetProviderId] && typeof providers[targetProviderId] === "object"
330
321
  ? providers[targetProviderId]
331
322
  : {};
332
- const headers = currentProvider.headers && typeof currentProvider.headers === "object"
333
- ? { ...currentProvider.headers }
334
- : {};
335
- for (const key of Object.keys(headers)) {
336
- if (key.toLowerCase() === piInsecureTlsHeader) {
337
- delete headers[key];
338
- }
339
- }
340
- if (insecureTls) {
341
- headers[piInsecureTlsHeader] = new URL(baseUrl).origin;
342
- }
343
323
  const existingModels = new Map(
344
324
  (Array.isArray(currentProvider.models) ? currentProvider.models : [])
345
325
  .filter((model) => model?.id)
@@ -357,11 +337,6 @@ function buildPiModelsConfig(modelsConfig = {}, {
357
337
  models: modelIds.map((modelId) => buildPiModelConfig(modelId, existingModels.get(modelId)))
358
338
  };
359
339
  delete provider.apiKey;
360
- if (Object.keys(headers).length) {
361
- provider.headers = headers;
362
- } else {
363
- delete provider.headers;
364
- }
365
340
 
366
341
  return {
367
342
  ...modelsConfig,
@@ -413,7 +388,6 @@ function buildPiConfiguration({ auth, models, settings }, {
413
388
  baseUrl,
414
389
  api,
415
390
  modelIds,
416
- insecureTls,
417
391
  defaultModel
418
392
  }) {
419
393
  const targetProviderId = requirePiProviderId(providerId);
@@ -425,8 +399,7 @@ function buildPiConfiguration({ auth, models, settings }, {
425
399
  providerId: targetProviderId,
426
400
  baseUrl,
427
401
  api,
428
- modelIds,
429
- insecureTls
402
+ modelIds
430
403
  }),
431
404
  settings: buildPiSettingsConfig(settings, targetProviderId, defaultModel)
432
405
  };
@@ -455,7 +428,6 @@ async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = as
455
428
  }
456
429
  const {
457
430
  assertNotCancelled,
458
- confirmOrCancel,
459
431
  passwordPromptOptions,
460
432
  required,
461
433
  requiredUnlessExisting,
@@ -495,11 +467,6 @@ async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = as
495
467
  const inferredApi = inferPiApiFromBaseUrl(baseUrlInput);
496
468
  const baseUrl = normalizeOpenAiBaseUrl(baseUrlInput);
497
469
 
498
- const insecureTls = await confirmOrCancel({
499
- message: `是否仅为 ${providerLabel} 中转请求跳过 TLS 证书校验?仅证书无法修复时启用`,
500
- initialValue: hasPiInsecureTlsSetting(modelsConfig, providerId)
501
- });
502
-
503
470
  const piApiOptions = getPiApiPromptOptions();
504
471
  const initialApi = piApiOptions.some((option) => option.value === existing.api)
505
472
  ? existing.api
@@ -542,7 +509,6 @@ async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = as
542
509
  baseUrl,
543
510
  api,
544
511
  modelIds,
545
- insecureTls,
546
512
  defaultModel: finalDefaultModel
547
513
  });
548
514
  await ensurePiResourcesLinked(paths);
@@ -564,7 +530,6 @@ export {
564
530
  configurePiApi,
565
531
  detectPiEffectiveModel,
566
532
  getPiApiPromptOptions,
567
- hasPiInsecureTlsSetting,
568
533
  inferPiApiFromBaseUrl,
569
534
  normalizeOpenAiBaseUrl,
570
535
  parsePiRpcEffectiveModel,
@@ -56,6 +56,15 @@ async function configureGrokSearchEnv(paths, ensureSkillPresent = async () => {}
56
56
  initialValue: Boolean(existing.TAVILY_API_KEY)
57
57
  });
58
58
 
59
+ const tavilyUrl = useTavily
60
+ ? await p.text({
61
+ message: "Tavily API URL",
62
+ initialValue: existing.TAVILY_API_URL || "https://api.tavily.com",
63
+ validate: required()
64
+ })
65
+ : null;
66
+ if (useTavily) assertNotCancelled(tavilyUrl);
67
+
59
68
  const tavilyKey = useTavily
60
69
  ? await p.password(passwordPromptOptions(
61
70
  "Tavily API Key",
@@ -70,6 +79,7 @@ async function configureGrokSearchEnv(paths, ensureSkillPresent = async () => {}
70
79
  GROK_API_URL: baseUrl,
71
80
  GROK_API_KEY: finalApiKey,
72
81
  GROK_MODEL: model,
82
+ TAVILY_API_URL: tavilyUrl,
73
83
  TAVILY_API_KEY: finalTavilyKey,
74
84
  TAVILY_ENABLED: useTavily ? "true" : null
75
85
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abelworkflow",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Install AbelWorkflow into ~/.agents and create Claude/Codex symlinks.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -216,7 +216,17 @@ def _emit_tavily_warning(message: str) -> None:
216
216
  RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}
217
217
 
218
218
 
219
+ class StreamEmbeddedError(Exception):
220
+ """SSE 流内嵌错误事件(HTTP 200 + data: {"error": ...})。"""
221
+
222
+
223
+ class EmptyStreamError(Exception):
224
+ """流式响应解析完成但内容为空。"""
225
+
226
+
219
227
  def _is_retryable_exception(exc) -> bool:
228
+ if isinstance(exc, (StreamEmbeddedError, EmptyStreamError)):
229
+ return True
220
230
  if isinstance(exc, (httpx.TimeoutException, httpx.NetworkError, httpx.ConnectError, httpx.RemoteProtocolError)):
221
231
  return True
222
232
  if isinstance(exc, httpx.HTTPStatusError):
@@ -501,6 +511,7 @@ def _normalize_tavily_base_url(raw: str) -> str:
501
511
 
502
512
  _http_client: Optional[httpx.AsyncClient] = None
503
513
  _DEFAULT_TIMEOUT = httpx.Timeout(connect=6.0, read=60.0, write=10.0, pool=None)
514
+ _NON_STREAM_TIMEOUT = httpx.Timeout(connect=6.0, read=10.0, write=10.0, pool=None)
504
515
 
505
516
 
506
517
  async def get_http_client() -> httpx.AsyncClient:
@@ -586,16 +597,23 @@ class GrokSearchProvider:
586
597
  return await self._execute(payload)
587
598
 
588
599
  async def _execute(self, payload: dict) -> str:
589
- """执行请求:先尝试非流式,失败时回退到流式。"""
600
+ """执行请求:流式优先,失败时回退到非流式(10s 超时 fail-fast)。"""
590
601
  try:
602
+ return await self._execute_stream(payload)
603
+ except httpx.HTTPStatusError as e:
604
+ if e.response.status_code not in RETRYABLE_STATUS_CODES:
605
+ raise
606
+ if config.debug_enabled:
607
+ print(f"[DEBUG] 流式失败: {e},回退到非流式", file=sys.stderr)
591
608
  return await self._execute_non_stream(payload)
592
- except (httpx.HTTPStatusError, json.JSONDecodeError) as e:
609
+ except (httpx.TimeoutException, httpx.NetworkError, httpx.RemoteProtocolError,
610
+ StreamEmbeddedError, EmptyStreamError, json.JSONDecodeError) as e:
593
611
  if config.debug_enabled:
594
- print(f"[DEBUG] 非流式失败: {e},回退到流式", file=sys.stderr)
595
- return await self._execute_stream(payload)
612
+ print(f"[DEBUG] 流式失败: {e},回退到非流式", file=sys.stderr)
613
+ return await self._execute_non_stream(payload)
596
614
 
597
615
  async def _execute_non_stream(self, payload: dict) -> str:
598
- """非流式请求(首选,对短响应更快)。"""
616
+ """非流式请求(流式失败后的回退方案,10s read 超时 fail-fast)。"""
599
617
  payload_copy = {**payload, "stream": False}
600
618
  client = await get_http_client()
601
619
 
@@ -610,6 +628,7 @@ class GrokSearchProvider:
610
628
  f"{self.api_url}/chat/completions",
611
629
  headers=self._headers,
612
630
  json=payload_copy,
631
+ timeout=_NON_STREAM_TIMEOUT,
613
632
  )
614
633
  response.raise_for_status()
615
634
  data = response.json()
@@ -619,7 +638,7 @@ class GrokSearchProvider:
619
638
  return ""
620
639
 
621
640
  async def _execute_stream(self, payload: dict) -> str:
622
- """流式请求(大响应的回退方案)。"""
641
+ """流式请求(首选,chunk 保活避免网关超时)。"""
623
642
  payload_copy = {**payload, "stream": True}
624
643
  client = await get_http_client()
625
644
 
@@ -655,24 +674,30 @@ class GrokSearchProvider:
655
674
  try:
656
675
  json_str = line[5:].lstrip()
657
676
  data = json.loads(json_str)
658
- choices = data.get("choices", [])
659
- if choices:
660
- delta = choices[0].get("delta", {})
661
- if "content" in delta:
662
- content += delta["content"]
663
677
  except (json.JSONDecodeError, IndexError):
664
678
  continue
679
+ if isinstance(data, dict) and "error" in data:
680
+ raise StreamEmbeddedError(json.dumps(data["error"], ensure_ascii=False)[:300])
681
+ choices = data.get("choices", [])
682
+ if choices:
683
+ delta = choices[0].get("delta", {})
684
+ if "content" in delta:
685
+ content += delta["content"]
665
686
 
666
687
  if not content and full_body_buffer:
667
688
  try:
668
689
  full_text = "".join(full_body_buffer)
669
690
  data = json.loads(full_text)
691
+ if isinstance(data, dict) and "error" in data:
692
+ raise StreamEmbeddedError(json.dumps(data["error"], ensure_ascii=False)[:300])
670
693
  if "choices" in data and data["choices"]:
671
694
  message = data["choices"][0].get("message", {})
672
695
  content = message.get("content", "")
673
696
  except json.JSONDecodeError:
674
697
  pass
675
698
 
699
+ if not content.strip():
700
+ raise EmptyStreamError("流式响应内容为空")
676
701
  return content
677
702
 
678
703
 
@@ -935,8 +960,9 @@ async def cmd_web_search(args):
935
960
  except ValueError as e:
936
961
  print(json.dumps({"error": str(e)}, ensure_ascii=False), file=sys.stderr)
937
962
  sys.exit(1)
938
- except httpx.HTTPStatusError as e:
939
- print(json.dumps({"error": f"API错误: {e.response.status_code}"}, ensure_ascii=False), file=sys.stderr)
963
+ except httpx.HTTPError as e:
964
+ detail = str(e.response.status_code) if isinstance(e, httpx.HTTPStatusError) else (str(e) or type(e).__name__)
965
+ print(json.dumps({"error": f"API错误: {detail}"}, ensure_ascii=False), file=sys.stderr)
940
966
  sys.exit(1)
941
967
 
942
968
 
@@ -974,8 +1000,9 @@ async def cmd_web_fetch(args):
974
1000
  except ValueError as e:
975
1001
  print(f"错误: {e}", file=sys.stderr)
976
1002
  sys.exit(1)
977
- except httpx.HTTPStatusError as e:
978
- print(f"API错误: {e.response.status_code}", file=sys.stderr)
1003
+ except httpx.HTTPError as e:
1004
+ detail = str(e.response.status_code) if isinstance(e, httpx.HTTPStatusError) else (str(e) or type(e).__name__)
1005
+ print(f"API错误: {detail}", file=sys.stderr)
979
1006
  sys.exit(1)
980
1007
 
981
1008
  if not result and tavily_error and not use_grok_fallback:
@@ -1,188 +0,0 @@
1
- export const piInsecureTlsHeader = "x-abelworkflow-insecure-tls";
2
-
3
- const installedFetchMarker = Symbol.for("abelworkflow.pi-provider-tls-fetch");
4
- const undiciGlobalDispatchers = [
5
- Symbol.for("undici.globalDispatcher.2"),
6
- Symbol.for("undici.globalDispatcher.1")
7
- ];
8
- const redirectStatuses = new Set([301, 302, 303, 307, 308]);
9
- const maxRedirects = 5;
10
-
11
- function getMarkedHeaders(input, init) {
12
- const source = init?.headers !== undefined
13
- ? init.headers
14
- : typeof Request !== "undefined" && input instanceof Request
15
- ? input.headers
16
- : undefined;
17
- if (source === undefined) return null;
18
-
19
- const headers = new Headers(source);
20
- if (!headers.has(piInsecureTlsHeader)) return null;
21
- const allowedOrigin = headers.get(piInsecureTlsHeader);
22
- headers.delete(piInsecureTlsHeader);
23
- return { allowedOrigin, headers };
24
- }
25
-
26
- function getRequestUrl(input) {
27
- return typeof Request !== "undefined" && input instanceof Request
28
- ? input.url
29
- : input instanceof URL
30
- ? input.href
31
- : input;
32
- }
33
-
34
- function getRequestOrigin(input) {
35
- try {
36
- return new URL(getRequestUrl(input)).origin;
37
- } catch {
38
- return null;
39
- }
40
- }
41
-
42
- function redirectRequestInit(input, init, status) {
43
- const method = String(init.method ?? (
44
- typeof Request !== "undefined" && input instanceof Request ? input.method : "GET"
45
- )).toUpperCase();
46
- const switchToGet = status === 303 && method !== "HEAD"
47
- || (status === 301 || status === 302) && method === "POST";
48
- if (!switchToGet) {
49
- if (typeof Request !== "undefined" && input instanceof Request && method !== "GET" && method !== "HEAD") {
50
- throw new Error("Pi insecure TLS cannot safely replay a redirected Request body");
51
- }
52
- return init;
53
- }
54
-
55
- const headers = new Headers(init.headers);
56
- for (const name of ["content-encoding", "content-language", "content-length", "content-location", "content-type"]) {
57
- headers.delete(name);
58
- }
59
- const nextInit = { ...init, method: "GET", headers };
60
- delete nextInit.body;
61
- return nextInit;
62
- }
63
-
64
- async function fetchWithSameOriginRedirects({ fetchOnce, input, init, allowedOrigin }) {
65
- let nextInput = input;
66
- let nextInit = init;
67
-
68
- for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount += 1) {
69
- const response = await fetchOnce(nextInput, nextInit);
70
- const location = redirectStatuses.has(response?.status) ? response.headers?.get?.("location") : null;
71
- if (!location) return response;
72
- if (redirectCount === maxRedirects) {
73
- await response.body?.cancel?.();
74
- throw new Error(`Pi insecure TLS exceeded ${maxRedirects} same-origin redirects`);
75
- }
76
-
77
- const target = new URL(location, getRequestUrl(nextInput));
78
- if (target.origin !== allowedOrigin) {
79
- await response.body?.cancel?.();
80
- throw new Error(`Pi insecure TLS blocked cross-origin redirect to ${target.origin}`);
81
- }
82
- await response.body?.cancel?.();
83
- nextInit = redirectRequestInit(nextInput, nextInit, response.status);
84
- nextInput = target.href;
85
- }
86
- }
87
-
88
- function applyNodeDispatcher(fetchImpl, input, init, dispatcherSource) {
89
- let dispatcher;
90
- try {
91
- dispatcher = typeof dispatcherSource === "function" ? dispatcherSource() : dispatcherSource;
92
- } catch (error) {
93
- return Promise.reject(error);
94
- }
95
-
96
- if (dispatcher && typeof dispatcher.then === "function") {
97
- return dispatcher.then((resolved) => applyNodeDispatcher(fetchImpl, input, init, resolved));
98
- }
99
- if (!dispatcher || typeof dispatcher.dispatch !== "function") {
100
- return Promise.reject(new Error("Pi insecure TLS requires an injected Undici dispatcher"));
101
- }
102
- return fetchImpl(input, { ...init, dispatcher });
103
- }
104
-
105
- function getUndiciDispatcherConstructor(target) {
106
- return undiciGlobalDispatchers
107
- .map((symbol) => target[symbol]?.constructor)
108
- .find((constructor) => typeof constructor === "function");
109
- }
110
-
111
- function initializeUndiciGlobalDispatcher(target) {
112
- if (typeof target.fetch !== "function") return;
113
- try {
114
- void Promise.resolve(target.fetch("data:,")).catch(() => {});
115
- } catch {
116
- }
117
- }
118
-
119
- export function createNodeInsecureDispatcher(target = globalThis) {
120
- let Dispatcher = getUndiciDispatcherConstructor(target);
121
- if (typeof Dispatcher !== "function") {
122
- initializeUndiciGlobalDispatcher(target);
123
- Dispatcher = getUndiciDispatcherConstructor(target);
124
- }
125
- if (typeof Dispatcher !== "function") {
126
- throw new Error("Pi insecure TLS requires the active Undici dispatcher");
127
- }
128
- return new Dispatcher({
129
- allowH2: false,
130
- connect: { rejectUnauthorized: false },
131
- requestTls: { rejectUnauthorized: false }
132
- });
133
- }
134
-
135
- export function createProviderTlsFetch({
136
- fetchImpl = globalThis.fetch,
137
- runtime = typeof globalThis.Bun === "undefined" ? "node" : "bun",
138
- insecureDispatcher
139
- } = {}) {
140
- if (typeof fetchImpl !== "function") {
141
- throw new TypeError("A fetch implementation is required");
142
- }
143
-
144
- return function providerTlsFetch(input, init) {
145
- const marked = getMarkedHeaders(input, init);
146
- if (!marked) return fetchImpl(input, init);
147
-
148
- const nextInit = { ...(init ?? {}), headers: marked.headers };
149
- if (!marked.allowedOrigin || getRequestOrigin(input) !== marked.allowedOrigin) {
150
- return fetchImpl(input, nextInit);
151
- }
152
-
153
- nextInit.redirect = "manual";
154
- const fetchOnce = runtime === "bun"
155
- ? (nextInput, redirectInit) => {
156
- const tls = redirectInit.tls && typeof redirectInit.tls === "object" ? redirectInit.tls : {};
157
- return fetchImpl(nextInput, {
158
- ...redirectInit,
159
- tls: { ...tls, rejectUnauthorized: false }
160
- });
161
- }
162
- : (nextInput, redirectInit) => applyNodeDispatcher(
163
- fetchImpl,
164
- nextInput,
165
- redirectInit,
166
- insecureDispatcher
167
- );
168
-
169
- return fetchWithSameOriginRedirects({
170
- fetchOnce,
171
- input,
172
- init: nextInit,
173
- allowedOrigin: marked.allowedOrigin
174
- });
175
- };
176
- }
177
-
178
- export function installProviderTlsFetch({ target = globalThis, ...options } = {}) {
179
- if (target.fetch?.[installedFetchMarker]) return target.fetch;
180
-
181
- const wrapped = createProviderTlsFetch({
182
- ...options,
183
- fetchImpl: target.fetch.bind(target)
184
- });
185
- Object.defineProperty(wrapped, installedFetchMarker, { value: true });
186
- target.fetch = wrapped;
187
- return wrapped;
188
- }