@yansigit/opencodex 2.36.1 → 2.36.2-dev.20260829.53

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.
Files changed (83) hide show
  1. package/README.md +9 -4
  2. package/gui/dist/assets/{ApiKeys-CsmNyf4I.js → ApiKeys-BhwqQvO8.js} +1 -1
  3. package/gui/dist/assets/{Claude-B4sHsGTD.js → Claude-DYNY8SHC.js} +1 -1
  4. package/gui/dist/assets/{CodexSet-BuAQ4YQn.js → CodexSet-tSN1aWmM.js} +1 -1
  5. package/gui/dist/assets/{FileIntegrationPage-BC65sEaS.js → FileIntegrationPage-B68tn2JT.js} +1 -1
  6. package/gui/dist/assets/{Grok-DNJWlISf.js → Grok-Dr6FN8SD.js} +1 -1
  7. package/gui/dist/assets/{Integrations-vGJshFZK.js → Integrations-jvqYofMm.js} +2 -2
  8. package/gui/dist/assets/{IntegrationsOverview-DYMkDFHE.js → IntegrationsOverview-Bjc7JSgw.js} +1 -1
  9. package/gui/dist/assets/{Logs-Coc7_RPP.js → Logs-C23CdgmP.js} +1 -1
  10. package/gui/dist/assets/{Models-CKVT8Meb.js → Models-Ci4wK3HO.js} +1 -1
  11. package/gui/dist/assets/{NumberStepper-Dug4wR8n.js → NumberStepper-nggECFok.js} +1 -1
  12. package/gui/dist/assets/Providers-DasIL4ng.js +1 -0
  13. package/gui/dist/assets/{RestoreDialog-BpuvVU2w.js → RestoreDialog-CW7LbS-w.js} +1 -1
  14. package/gui/dist/assets/{Startup-DAUQqNEN.js → Startup-Cx3dZre0.js} +1 -1
  15. package/gui/dist/assets/{Storage-DfP95SlJ.js → Storage-D0iUYQED.js} +1 -1
  16. package/gui/dist/assets/{Subagents-CMNsZLfk.js → Subagents-phLFffyl.js} +1 -1
  17. package/gui/dist/assets/{Usage-ESFRfBMO.js → Usage-NvYxG04o.js} +1 -1
  18. package/gui/dist/assets/{codex-stale-banner-W4FhikN7.js → codex-stale-banner-B5VQPO3t.js} +1 -1
  19. package/gui/dist/assets/{data-surface-B8PX8rjm.js → data-surface-B0APtUxm.js} +1 -1
  20. package/gui/dist/assets/{data-surface-CETFXqiA.js → data-surface-OiQUZok8.js} +1 -1
  21. package/gui/dist/assets/index-C8flH881.css +1 -0
  22. package/gui/dist/assets/{index-DHQHBRhb.js → index-CZ1jyQCF.js} +3 -3
  23. package/gui/dist/assets/{model-display-OBTJhQwT.js → model-display-Dlrzica2.js} +1 -1
  24. package/gui/dist/assets/{provider-payload-CJRJUn7w.js → provider-payload-Ca3Ff8oP.js} +1 -1
  25. package/gui/dist/assets/{section-tabs-B8sLMiz6.js → section-tabs-Bpm5lFcZ.js} +1 -1
  26. package/gui/dist/assets/shared-BHpPx-nZ.js +69 -0
  27. package/gui/dist/index.html +3 -3
  28. package/package.json +2 -1
  29. package/src/adapters/base.ts +2 -0
  30. package/src/adapters/command-code.ts +2 -1
  31. package/src/adapters/google-http.ts +34 -14
  32. package/src/adapters/google.ts +4 -1
  33. package/src/cli/dispatch.ts +2 -2
  34. package/src/cli/help.ts +2 -2
  35. package/src/cli/index.ts +9 -1
  36. package/src/cli/init.ts +52 -3
  37. package/src/cli/registry.ts +2 -2
  38. package/src/codex/account-lifecycle.ts +4 -8
  39. package/src/codex/inject.ts +14 -9
  40. package/src/config/process-state.ts +15 -0
  41. package/src/config.ts +17 -0
  42. package/src/generated/compatibility-version.json +67 -47
  43. package/src/lib/bun-stream-caps.ts +2 -1
  44. package/src/lib/pinned-http.ts +7 -1
  45. package/src/lib/process-control.ts +3 -2
  46. package/src/lib/provider-outbound.ts +77 -27
  47. package/src/lib/provider-runtime-fetch.ts +23 -0
  48. package/src/lib/provider-tls-profile.ts +97 -11
  49. package/src/lib/server-tls.ts +47 -0
  50. package/src/lib/test-provider-fetch.ts +6 -0
  51. package/src/lib/test-server-start.ts +5 -0
  52. package/src/lib/upstream-retry.ts +39 -25
  53. package/src/oauth/anthropic.ts +4 -3
  54. package/src/oauth/chatgpt.ts +7 -3
  55. package/src/oauth/command-code.ts +3 -2
  56. package/src/oauth/cursor.ts +3 -2
  57. package/src/oauth/github-copilot.ts +7 -5
  58. package/src/oauth/google-antigravity.ts +5 -4
  59. package/src/oauth/key-providers.ts +11 -13
  60. package/src/oauth/kimi.ts +7 -6
  61. package/src/oauth/kiro.ts +3 -2
  62. package/src/oauth/nous.ts +5 -5
  63. package/src/oauth/transport.ts +77 -0
  64. package/src/oauth/xai.ts +5 -4
  65. package/src/providers/replit/probe.ts +8 -3
  66. package/src/providers/xai-transport.ts +16 -2
  67. package/src/server/auth-cors.ts +15 -1
  68. package/src/server/chat-native.ts +9 -3
  69. package/src/server/index.ts +63 -23
  70. package/src/server/management/config-routes.ts +117 -15
  71. package/src/server/management/context.ts +4 -0
  72. package/src/server/management/provider-routes.ts +1 -0
  73. package/src/server/responses/compact.ts +5 -2
  74. package/src/server/responses/core.ts +16 -19
  75. package/src/server/responses/fetch-helpers.ts +15 -7
  76. package/src/server/ws-bridge.ts +33 -4
  77. package/src/service.ts +10 -1
  78. package/src/types/config.ts +9 -0
  79. package/src/types/provider.ts +2 -0
  80. package/src/types.ts +1 -0
  81. package/gui/dist/assets/Providers-DyaCHRWL.js +0 -1
  82. package/gui/dist/assets/index-pLho_LBF.css +0 -1
  83. package/gui/dist/assets/shared-DI18-uF7.js +0 -69
@@ -2,6 +2,7 @@ import type { OcxProviderConfig } from "../types";
2
2
  import { deriveKeyLoginMap, enrichProviderFromRegistry, type DerivedKeyLoginProvider } from "../providers/derive";
3
3
  import { resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
4
4
  import { parseGoogleCookieJar, validateAiStudioCookies } from "./google-aistudio-auth";
5
+ import { providerOutboundGet, providerOutboundPost, type ProviderOutboundDependencies } from "../lib/provider-outbound";
5
6
 
6
7
  /**
7
8
  * API-key "login" providers: not OAuth — the flow opens the provider's dashboard so the user can
@@ -72,6 +73,7 @@ export async function validateApiKey(
72
73
  providerName: string,
73
74
  provider: KeyLoginProvider,
74
75
  key: string,
76
+ dependencies: Pick<ProviderOutboundDependencies, "fetch"> = {},
75
77
  ): Promise<boolean | "unknown"> {
76
78
  try {
77
79
  // A public model catalog cannot prove that the supplied key is valid. Returning unknown keeps
@@ -80,17 +82,15 @@ export async function validateApiKey(
80
82
 
81
83
  if (provider.adapter === "anthropic") {
82
84
  const base = provider.baseUrl.replace(/\/v1\/?$/, "");
83
- const res = await fetch(`${base}/v1/messages`, {
84
- method: "POST",
85
+ const res = await providerOutboundPost(providerName, provider, `${base}/v1/messages`, {
85
86
  headers: anthropicKeyValidationHeaders(provider, key),
87
+ signal: AbortSignal.timeout(8_000),
86
88
  body: JSON.stringify({
87
89
  model: provider.defaultModel ?? "claude-haiku-4-5",
88
90
  max_tokens: 1,
89
91
  messages: [{ role: "user", content: "ping" }],
90
92
  }),
91
- redirect: "error",
92
- signal: AbortSignal.timeout(8000),
93
- });
93
+ }, dependencies);
94
94
  if (res.ok) return true;
95
95
  if (res.status === 401 || res.status === 403) return false;
96
96
  return "unknown";
@@ -106,11 +106,10 @@ export async function validateApiKey(
106
106
  if (provider.adapter === "google" && (provider.googleMode ?? "ai-studio") === "ai-studio") {
107
107
  // Generative Language API rejects Bearer-wrapped API keys; probe models.list with the
108
108
  // documented x-goog-api-key header instead (pageSize=1 — validation only needs a 200).
109
- const res = await fetch(`${provider.baseUrl}/v1beta/models?pageSize=1`, {
109
+ const res = await providerOutboundGet(providerName, provider, `${provider.baseUrl}/v1beta/models?pageSize=1`, {
110
110
  headers: { "x-goog-api-key": key },
111
- redirect: "error",
112
- signal: AbortSignal.timeout(8000),
113
- });
111
+ signal: AbortSignal.timeout(8_000),
112
+ }, dependencies);
114
113
  if (res.ok) return true;
115
114
  if (res.status === 400 || res.status === 401 || res.status === 403) return false;
116
115
  return "unknown";
@@ -127,11 +126,10 @@ export async function validateApiKey(
127
126
  provider.baseUrl,
128
127
  `${provider.baseUrl}/models`,
129
128
  );
130
- const res = await fetch(modelsUrl, {
129
+ const res = await providerOutboundGet(providerName, configuredProvider, modelsUrl, {
131
130
  headers: { Authorization: `Bearer ${key}` },
132
- redirect: "error",
133
- signal: AbortSignal.timeout(8000),
134
- });
131
+ signal: AbortSignal.timeout(8_000),
132
+ }, dependencies);
135
133
  if (res.ok) return true;
136
134
  if (res.status === 401 || res.status === 403) return false;
137
135
  return "unknown";
package/src/oauth/kimi.ts CHANGED
@@ -6,6 +6,7 @@ import { randomUUID } from "node:crypto";
6
6
  import { recordOwnedConfigPath } from "../lib/config-ownership";
7
7
  import { getConfigDir } from "../config";
8
8
  import type { OAuthController, OAuthCredentials } from "./types";
9
+ import { oauthFetch } from "./transport";
9
10
 
10
11
  const CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
11
12
  const DEFAULT_OAUTH_HOST = "https://auth.kimi.com";
@@ -129,13 +130,13 @@ function getKimiCommonHeaders(): Record<string, string> {
129
130
  async function requestDeviceAuthorization(): Promise<{
130
131
  userCode: string; deviceCode: string; verificationUriComplete: string; expiresInMs: number; intervalMs: number;
131
132
  }> {
132
- const response = await fetch(`${resolveOAuthHost()}/api/oauth/device_authorization`, {
133
+ const response = await oauthFetch(`${resolveOAuthHost()}/api/oauth/device_authorization`, {
133
134
  method: "POST",
134
135
  headers: { "Content-Type": "application/x-www-form-urlencoded", ...getKimiCommonHeaders() },
135
136
  body: new URLSearchParams({ client_id: CLIENT_ID }),
136
137
  });
137
138
  if (!response.ok) {
138
- throw new Error(`Kimi device authorization failed: ${response.status} ${await response.text()}`);
139
+ throw new Error(`Kimi device authorization failed: ${response.status}`);
139
140
  }
140
141
  const payload = (await response.json()) as DeviceAuthorizationResponse;
141
142
  if (!payload.user_code || !payload.device_code || !payload.verification_uri) {
@@ -184,7 +185,7 @@ async function pollForToken(deviceCode: string, intervalMs: number, expiresInMs:
184
185
  let waitMs = Math.max(1000, intervalMs);
185
186
  while (Date.now() < deadline) {
186
187
  if (signal?.aborted) throw new Error("Login cancelled");
187
- const response = await fetch(`${resolveOAuthHost()}/api/oauth/token`, {
188
+ const response = await oauthFetch(`${resolveOAuthHost()}/api/oauth/token`, {
188
189
  method: "POST",
189
190
  headers: { "Content-Type": "application/x-www-form-urlencoded", ...getKimiCommonHeaders() },
190
191
  body: new URLSearchParams({ client_id: CLIENT_ID, device_code: deviceCode, grant_type: "urn:ietf:params:oauth:grant-type:device_code" }),
@@ -202,7 +203,7 @@ async function pollForToken(deviceCode: string, intervalMs: number, expiresInMs:
202
203
  }
203
204
  if (error === "expired_token") throw new Error("Kimi device authorization expired");
204
205
  if (error === "access_denied") throw new Error("Kimi device authorization denied");
205
- throw new Error(`Kimi device flow failed: ${error ?? response.status}${payload.error_description ? `: ${payload.error_description}` : ""}`);
206
+ throw new Error(`Kimi device flow failed (${response.status})`);
206
207
  }
207
208
  throw new Error("Kimi device flow timed out");
208
209
  }
@@ -222,14 +223,14 @@ export async function loginKimi(ctrl: OAuthController): Promise<OAuthCredentials
222
223
  }
223
224
 
224
225
  export async function refreshKimiToken(refreshToken: string): Promise<OAuthCredentials> {
225
- const response = await fetch(`${resolveOAuthHost()}/api/oauth/token`, {
226
+ const response = await oauthFetch(`${resolveOAuthHost()}/api/oauth/token`, {
226
227
  method: "POST",
227
228
  headers: { "Content-Type": "application/x-www-form-urlencoded", ...getKimiCommonHeaders() },
228
229
  body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: CLIENT_ID }),
229
230
  });
230
231
  if (!response.ok) {
231
232
  const payload = (await response.json().catch(() => undefined)) as TokenResponse | undefined;
232
- throw new Error(`Kimi token refresh failed: ${response.status}${payload?.error_description ? `: ${payload.error_description}` : ""}`);
233
+ throw new Error(`Kimi token refresh failed: ${response.status}`);
233
234
  }
234
235
  return parseTokenPayload((await response.json()) as TokenResponse, refreshToken);
235
236
  }
package/src/oauth/kiro.ts CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  import { homedir } from "node:os";
30
30
  import { getAccountSet, saveAccountCredential } from "./store";
31
31
  import { KIRO_BUILDER_ID_SERVICE_PROFILE_ARN } from "../adapters/kiro-constants";
32
+ import { oauthFetch } from "./transport";
32
33
 
33
34
  const DEFAULT_REGION = "us-east-1";
34
35
  const REFRESH_URL = "https://prod.{region}.auth.desktop.kiro.dev/refreshToken";
@@ -548,7 +549,7 @@ function kiroRefreshSignal(signal?: AbortSignal): AbortSignal {
548
549
 
549
550
  async function refreshKiroDesktopToken(refresh: string, signal?: AbortSignal, metadata?: KiroOAuthMetadata): Promise<OAuthCredentials> {
550
551
  const region = resolveKiroRegion(metadata);
551
- const res = await fetch(REFRESH_URL.replace("{region}", region), {
552
+ const res = await oauthFetch(REFRESH_URL.replace("{region}", region), {
552
553
  method: "POST",
553
554
  headers: { "Content-Type": "application/json" },
554
555
  body: JSON.stringify({ refreshToken: refresh }),
@@ -584,7 +585,7 @@ async function refreshAwsSsoOidcToken(
584
585
  const clientSecret = metadata?.clientSecret;
585
586
  if (!clientId || !clientSecret) return refreshKiroDesktopToken(refresh, signal, metadata);
586
587
  const region = resolveKiroRegion(metadata);
587
- const run = async (refreshToken: string): Promise<Response> => fetch(OIDC_URL.replace("{region}", region), {
588
+ const run = async (refreshToken: string): Promise<Response> => oauthFetch(OIDC_URL.replace("{region}", region), {
588
589
  method: "POST",
589
590
  headers: { "Content-Type": "application/json" },
590
591
  body: JSON.stringify({
package/src/oauth/nous.ts CHANGED
@@ -40,6 +40,7 @@ import type { OAuthController, OAuthCredentials } from "./types";
40
40
  import { getAuthStorePath } from "./store";
41
41
  import { atomicWriteFile, hardenConfigDir, hardenExistingSecret } from "../config";
42
42
  import { BOUNDED_BODY_MAX_BYTES, readBoundedResponseBytes } from "../lib/bounded-body";
43
+ import { oauthFetch } from "./transport";
43
44
 
44
45
  export const NOUS_PORTAL_BASE_URL = "https://portal.nousresearch.com";
45
46
  export const NOUS_INFERENCE_BASE_URL = "https://inference-api.nousresearch.com/v1";
@@ -535,7 +536,7 @@ async function requestDeviceAuthorization(signal?: AbortSignal): Promise<{
535
536
  intervalMs: number;
536
537
  }> {
537
538
  const effectiveSignal = requestSignal(signal);
538
- const response = await fetch(`${resolvePortalBaseUrl()}/api/oauth/device/code`, {
539
+ const response = await oauthFetch(`${resolvePortalBaseUrl()}/api/oauth/device/code`, {
539
540
  method: "POST",
540
541
  headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" },
541
542
  body: new URLSearchParams({
@@ -593,7 +594,7 @@ async function pollForToken(
593
594
  let response: Response;
594
595
  const effectiveSignal = requestSignal(signal);
595
596
  try {
596
- response = await fetch(`${resolvePortalBaseUrl()}/api/oauth/token`, {
597
+ response = await oauthFetch(`${resolvePortalBaseUrl()}/api/oauth/token`, {
597
598
  method: "POST",
598
599
  headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" },
599
600
  body: new URLSearchParams({
@@ -646,8 +647,7 @@ async function pollForToken(
646
647
  // Unknown OAuth error: report it from the parsed payload, not by
647
648
  // re-reading the (already consumed) response body.
648
649
  if (error) {
649
- const detail = typeof payload.error_description === "string" ? `: ${payload.error_description}` : "";
650
- throw new NousTokenError(response.status, String(error), `Nous Portal device authorization failed (${error})${detail}`);
650
+ throw new NousTokenError(response.status, String(error), `Nous Portal device authorization failed (${response.status})`);
651
651
  }
652
652
  throw tokenErrorFromPayload(response.status, payload);
653
653
  }
@@ -717,7 +717,7 @@ export async function refreshNousToken(refreshToken: string, signal?: AbortSigna
717
717
  let response: Response;
718
718
  const effectiveSignal = requestSignal(signal);
719
719
  try {
720
- response = await fetch(`${baseUrl}/api/oauth/token`, {
720
+ response = await oauthFetch(`${baseUrl}/api/oauth/token`, {
721
721
  method: "POST",
722
722
  headers: {
723
723
  Accept: "application/json",
@@ -0,0 +1,77 @@
1
+ const OAUTH_TIMEOUT_MS = 30_000;
2
+ const MAX_OAUTH_RESPONSE_BYTES = 1024 * 1024;
3
+
4
+ export class OAuthTransportError extends Error {
5
+ override readonly name = "OAuthTransportError";
6
+ }
7
+
8
+ async function boundedResponse(response: Response, signal: AbortSignal): Promise<Response> {
9
+ const declaredLength = Number(response.headers.get("content-length"));
10
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_OAUTH_RESPONSE_BYTES) {
11
+ try { await response.body?.cancel(); } catch { /* ignore cancellation failures */ }
12
+ throw new OAuthTransportError("OAuth response exceeded 1 MiB");
13
+ }
14
+ if (!response.body) return response;
15
+ const reader = response.body.getReader();
16
+ const abort = () => { void reader.cancel(signal.reason).catch(() => undefined); };
17
+ if (signal.aborted) abort();
18
+ else signal.addEventListener("abort", abort, { once: true });
19
+ const chunks: Uint8Array[] = [];
20
+ let size = 0;
21
+ try {
22
+ for (;;) {
23
+ const { done, value } = await reader.read();
24
+ if (done) break;
25
+ size += value.byteLength;
26
+ if (size > MAX_OAUTH_RESPONSE_BYTES) {
27
+ await reader.cancel().catch(() => undefined);
28
+ throw new OAuthTransportError("OAuth response exceeded 1 MiB");
29
+ }
30
+ chunks.push(value);
31
+ }
32
+ } finally {
33
+ signal.removeEventListener("abort", abort);
34
+ reader.releaseLock();
35
+ }
36
+ const body = new Uint8Array(size);
37
+ let offset = 0;
38
+ for (const chunk of chunks) {
39
+ body.set(chunk, offset);
40
+ offset += chunk.byteLength;
41
+ }
42
+ return new Response(body, {
43
+ status: response.status,
44
+ statusText: response.statusText,
45
+ headers: response.headers,
46
+ });
47
+ }
48
+
49
+ export async function oauthFetch(
50
+ input: string | URL | Request,
51
+ init: RequestInit = {},
52
+ executor: typeof globalThis.fetch = globalThis.fetch,
53
+ ): Promise<Response> {
54
+ let url: URL;
55
+ try {
56
+ url = input instanceof Request ? new URL(input.url) : new URL(String(input));
57
+ } catch {
58
+ throw new OAuthTransportError("OAuth endpoint is invalid");
59
+ }
60
+ if (url.username || url.password) throw new OAuthTransportError("OAuth endpoint must not contain credentials");
61
+ if (url.protocol !== "https:") throw new OAuthTransportError("OAuth endpoint must use HTTPS");
62
+ const timeout = AbortSignal.timeout(OAUTH_TIMEOUT_MS);
63
+ const signal = init.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
64
+ try {
65
+ const response = await executor(input, { ...init, redirect: "manual", signal });
66
+ if (response.status >= 300 && response.status < 400) {
67
+ try { await response.body?.cancel(); } catch { /* ignore cancellation failures */ }
68
+ throw new OAuthTransportError(`OAuth endpoint refused the request (HTTP ${response.status})`);
69
+ }
70
+ return await boundedResponse(response, signal);
71
+ } catch (error) {
72
+ if (error instanceof OAuthTransportError) throw error;
73
+ if (init.signal?.aborted) throw init.signal.reason;
74
+ if (timeout.aborted) throw timeout.reason;
75
+ throw new OAuthTransportError("OAuth request failed");
76
+ }
77
+ }
package/src/oauth/xai.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
3
3
  import { generatePKCE } from "./pkce";
4
4
  import type { LocalTokenImportMode, OAuthController, OAuthCredentials } from "./types";
5
+ import { oauthFetch } from "./transport";
5
6
 
6
7
  const XAI_OAUTH_ISSUER = "https://auth.x.ai";
7
8
  export const XAI_OAUTH_DISCOVERY_URL = `${XAI_OAUTH_ISSUER}/.well-known/openid-configuration`;
@@ -54,12 +55,12 @@ function validateXaiEndpoint(rawUrl: string): string {
54
55
  }
55
56
 
56
57
  export async function discoverXaiOAuthEndpoints(signal?: AbortSignal): Promise<XaiDiscovery> {
57
- const response = await fetch(XAI_OAUTH_DISCOVERY_URL, {
58
+ const response = await oauthFetch(XAI_OAUTH_DISCOVERY_URL, {
58
59
  headers: { Accept: "application/json" },
59
60
  signal: requestSignal(signal),
60
61
  });
61
62
  if (!response.ok) {
62
- throw new Error(`xAI OAuth discovery failed: ${response.status} ${await response.text()}`);
63
+ throw new Error(`xAI OAuth discovery failed: ${response.status}`);
63
64
  }
64
65
 
65
66
  const payload = (await response.json()) as XaiDiscoveryPayload;
@@ -96,14 +97,14 @@ export class XaiTokenRequestError extends Error { constructor(public readonly st
96
97
  export interface XaiTokenRetryDeps { sleep?:(ms:number)=>Promise<void>; random?:()=>number }
97
98
  function isAbortError(error:unknown):boolean{return error instanceof DOMException&&error.name==="AbortError";}
98
99
  function retryDelay(attempt:number,retryAfter:string|null,random:()=>number):number{const base=attempt===1?100:250,j=Math.round(base*(.75+random()*.5)),seconds=retryAfter!==null&&/^\d+$/.test(retryAfter)?Number(retryAfter):0;return Math.min(2000,Math.max(j,seconds*1000));}
99
- async function readTokenError(response:Response):Promise<XaiTokenRequestError>{let oauthError:string|undefined,detail="";try{const body=await response.json() as {error?:unknown;error_description?:unknown};if(typeof body.error==="string")oauthError=body.error;if(typeof body.error_description==="string")detail=body.error_description;}catch{}const suffix=detail?`: ${detail}`:oauthError?`: ${oauthError}`:"";return new XaiTokenRequestError(response.status,oauthError,`xAI token request failed: ${response.status}${suffix}`);}
100
+ async function readTokenError(response:Response):Promise<XaiTokenRequestError>{let oauthError:string|undefined;try{const body=await response.json() as {error?:unknown};if(typeof body.error==="string"&&/^(?:access_denied|expired_token|invalid_client|invalid_grant|invalid_request|temporarily_unavailable|unauthorized_client|unsupported_grant_type)$/.test(body.error))oauthError=body.error;}catch{oauthError=undefined;}return new XaiTokenRequestError(response.status,oauthError,`xAI token request failed: ${response.status}`);}
100
101
  export async function postXaiToken(
101
102
  tokenEndpoint: string,
102
103
  body: Record<string, string>,
103
104
  signal?: AbortSignal, deps:XaiTokenRetryDeps={},
104
105
  ): Promise<XaiTokenPayload> {
105
106
  const sleep=deps.sleep??(ms=>Bun.sleep(ms)),random=deps.random??Math.random;let last:unknown;
106
- for(let attempt=1;attempt<=3;attempt++){let response:Response;try{response=await fetch(tokenEndpoint, {
107
+ for(let attempt=1;attempt<=3;attempt++){let response:Response;try{response=await oauthFetch(tokenEndpoint, {
107
108
  method: "POST",
108
109
  headers: {
109
110
  Accept: "application/json",
@@ -1,4 +1,4 @@
1
- import { providerOutboundGet, providerRedirectError } from "../../lib/provider-outbound";
1
+ import { ProviderOutboundPolicyError, providerOutboundGet, providerRedirectError } from "../../lib/provider-outbound";
2
2
  import { extractProviderModelItems, readBoundedDiscoveryJson } from "../model-discovery";
3
3
  import type { OcxProviderConfig } from "../../types";
4
4
  import {
@@ -41,6 +41,11 @@ function probeProviderStub(origin: ValidatedReplitOrigin): ProbeProviderConfig {
41
41
  };
42
42
  }
43
43
 
44
+ function probeFailureCategory(error: unknown): "redirect_rejected" | "timeout" | "internal" {
45
+ if (error instanceof ProviderOutboundPolicyError && / returned 3\d\d redirect;/.test(error.message)) return "redirect_rejected";
46
+ return error instanceof DOMException && error.name === "TimeoutError" ? "timeout" : "internal";
47
+ }
48
+
44
49
  async function cancelResponseBody(response: Response): Promise<void> {
45
50
  try {
46
51
  await response.body?.cancel();
@@ -96,7 +101,7 @@ export async function probeReplitGateway(
96
101
  return {
97
102
  ok: false,
98
103
  stage: "healthz",
99
- category: error instanceof DOMException && error.name === "TimeoutError" ? "timeout" : "internal",
104
+ category: probeFailureCategory(error),
100
105
  error: error instanceof Error ? error.message : "healthz probe failed",
101
106
  latencyMs: Date.now() - healthzStarted,
102
107
  };
@@ -132,7 +137,7 @@ export async function probeReplitGateway(
132
137
  return {
133
138
  ok: false,
134
139
  stage: "models",
135
- category: error instanceof DOMException && error.name === "TimeoutError" ? "timeout" : "internal",
140
+ category: probeFailureCategory(error),
136
141
  error: error instanceof Error ? error.message : "models probe failed",
137
142
  latencyMs: Date.now() - modelsStarted,
138
143
  };
@@ -1,6 +1,8 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import type { OcxProviderConfig } from "../types";
3
3
  import { resolveGithubCopilotTransport } from "./github-copilot-transport";
4
+ import { testProviderFetch } from "../lib/test-provider-fetch";
5
+ import { RUNTIME_PROVIDER_FETCH, type RuntimeProviderFetch } from "../lib/provider-runtime-fetch";
4
6
 
5
7
  export const XAI_GROK_CLI_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
6
8
 
@@ -46,6 +48,7 @@ export const XAI_CONV_ID_HEADER = XAI_GROK_COMPATIBILITY.headers.conversationId;
46
48
  export type OcxProviderTransport = OcxProviderConfig & {
47
49
  /** Request executor used only at runtime; never persisted. */
48
50
  fetch?: typeof globalThis.fetch;
51
+ [RUNTIME_PROVIDER_FETCH]?: RuntimeProviderFetch;
49
52
  };
50
53
 
51
54
  const XAI_GROK_CLI_HEADERS: Readonly<Record<string, string>> = {
@@ -156,14 +159,25 @@ export function resolveProviderTransport(
156
159
  // transient retries reuse one id so the upstream can dedupe them. A rotated key resolves a
157
160
  // fresh transport, which gets its own id.
158
161
  const requestId = configuredRequestId ?? randomUUID();
159
- const baseFetch = provider.fetch ?? globalThis.fetch;
162
+ const baseFetch = testProviderFetch(provider) ?? globalThis.fetch;
160
163
  const attemptFetch = ((input, init) =>
161
164
  baseFetch(input, withGeneratedRequestId(init, requestId, stableHeaders))) as typeof globalThis.fetch;
162
165
 
163
- return {
166
+ const resolved = {
164
167
  ...provider,
165
168
  ...(provider.authMode === "oauth" ? { baseUrl: XAI_GROK_CLI_BASE_URL } : {}),
166
169
  headers,
167
170
  fetch: attemptFetch,
168
171
  };
172
+ // Enumerable symbols survive runtime provider spreads (credential/key rotation), while
173
+ // JSON serialization still cannot persist or accept them from configuration.
174
+ Object.defineProperty(resolved, RUNTIME_PROVIDER_FETCH, {
175
+ value: {
176
+ providerName: "xai",
177
+ origins: ["https://api.x.ai", "https://cli-chat-proxy.grok.com"],
178
+ fetch: attemptFetch,
179
+ },
180
+ enumerable: true,
181
+ });
182
+ return resolved;
169
183
  }
@@ -1,4 +1,5 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
+ import { assertServerTlsFiles, serverTlsConfigError } from "../lib/server-tls";
2
3
  import { formatErrorResponse } from "../bridge";
3
4
  import {
4
5
  codexAutoStartEnabled,
@@ -294,7 +295,10 @@ export function requestPolicyView(config: OcxConfig, bindHostname: string): Requ
294
295
  };
295
296
  }
296
297
 
297
- export function assertServerAuthConfig(config: OcxConfig): void {
298
+ export function assertServerAuthConfig(
299
+ config: OcxConfig,
300
+ options: { allowPlaintextRemoteForTests?: boolean } = {},
301
+ ): void {
298
302
  const hasConfiguredDataCredential = !!configuredApiAuthToken(config)
299
303
  || (config.apiKeys ?? []).some(entry => !!entry.key.trim());
300
304
  if (isApiAuthRequired(config) && !hasConfiguredDataCredential) {
@@ -302,6 +306,12 @@ export function assertServerAuthConfig(config: OcxConfig): void {
302
306
  "A data-plane credential (OPENCODEX_API_AUTH_TOKEN or config.apiKeys) is required when binding opencodex to a non-loopback hostname",
303
307
  );
304
308
  }
309
+ const tlsError = serverTlsConfigError(config.tls);
310
+ if (tlsError) throw new Error(`Invalid server TLS configuration: ${tlsError}`);
311
+ if (isApiAuthRequired(config) && !config.tls && !options.allowPlaintextRemoteForTests) {
312
+ throw new Error("Native TLS is required when binding opencodex to a non-loopback hostname; configure tls.certFile, tls.keyFile, and tls.publicOrigin or bind to loopback behind a TLS tunnel");
313
+ }
314
+ if (config.tls) assertServerTlsFiles(config.tls);
305
315
  }
306
316
 
307
317
  function secretEquals(actual: string, expected: string | undefined): boolean {
@@ -607,6 +617,9 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
607
617
  // it before it reaches the management API response.
608
618
  return `provider ${JSON.stringify(redactSecretString(name))} ${retryOn429Error}`;
609
619
  }
620
+ if (raw.replayTransientFailures !== undefined && typeof raw.replayTransientFailures !== "boolean") {
621
+ return `provider ${JSON.stringify(redactSecretString(name))} replayTransientFailures must be a boolean`;
622
+ }
610
623
  const requestPacingError = requestPacingConfigError(raw.requestPacing);
611
624
  if (requestPacingError) {
612
625
  return `provider ${JSON.stringify(redactSecretString(name))} ${requestPacingError}`;
@@ -733,6 +746,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
733
746
  "defaultAliases",
734
747
  "disabled",
735
748
  "allowPrivateNetwork",
749
+ "replayTransientFailures",
736
750
  "authMode",
737
751
  "googleMode",
738
752
  "apiKeyTransport",
@@ -20,7 +20,7 @@ import { resolveClientRetryAfter } from "../lib/retry-after";
20
20
  import { isModelTextOnly } from "../vision";
21
21
  import {
22
22
  applyUpstreamRecoveryInit,
23
- fetchWithResetRetry,
23
+ fetchWithTransientRetry,
24
24
  prepareSameTarget429Wait,
25
25
  type UpstreamSendRecovery,
26
26
  } from "../lib/upstream-retry";
@@ -202,9 +202,10 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
202
202
  return fail(400, error instanceof Error ? error.message : String(error), "invalid_request_error");
203
203
  }
204
204
 
205
+ const replayBudget = activeProvider.replayTransientFailures ? { remaining: 2 } : undefined;
205
206
  const send = async (request: AdapterRequest, recovery?: "rate-limit-429" | "key-429"): Promise<Response> => {
206
207
  try {
207
- return await fetchWithResetRetry(
208
+ return await fetchWithTransientRetry(
208
209
  (transportRecovery?: UpstreamSendRecovery) => {
209
210
  noteAttemptSend(attempt, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
210
211
  return fetchWithHeaderTimeout(
@@ -223,7 +224,12 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
223
224
  }),
224
225
  );
225
226
  },
226
- { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
227
+ {
228
+ abortSignal: upstream.signal,
229
+ label: safeHostLabel(request.url),
230
+ replayTransientFailures: activeProvider.replayTransientFailures,
231
+ replayBudget,
232
+ },
227
233
  );
228
234
  } finally {
229
235
  request.releaseBodyObservation?.();