@bitkyc08/opencodex 2.6.23 → 2.6.24

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.
@@ -16,7 +16,7 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-Yuh5eZiD.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-Ci7RsPIr.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-BcHhxo1I.css">
21
21
  </head>
22
22
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.6.23",
3
+ "version": "2.6.24",
4
4
  "description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
package/src/server.ts CHANGED
@@ -1480,9 +1480,8 @@ function isLoopbackRequestHost(value: string | null): boolean {
1480
1480
  function isLoopbackOriginValue(value: string): boolean {
1481
1481
  try {
1482
1482
  const parsed = new URL(value);
1483
- if (parsed.protocol !== "http:") return false;
1484
- if (!isLoopbackHostname(parsed.hostname)) return false;
1485
- return parsed.port === configuredPort();
1483
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
1484
+ return isLoopbackHostname(parsed.hostname);
1486
1485
  } catch {
1487
1486
  return false;
1488
1487
  }
@@ -1497,12 +1496,22 @@ function isSameOriginAsRequest(req: Request, origin: string): boolean {
1497
1496
  }
1498
1497
 
1499
1498
  function isAllowedRequestOrigin(req: Request, config: OcxConfig): boolean {
1499
+ function isExtraAllowedOrigin(origin: string, cfg: OcxConfig): boolean {
1500
+ if (!cfg.corsAllowOrigins?.length) return false;
1501
+ return cfg.corsAllowOrigins.some(allowed => {
1502
+ try {
1503
+ return new URL(allowed).origin === new URL(origin).origin;
1504
+ } catch {
1505
+ return allowed === origin;
1506
+ }
1507
+ });
1508
+ }
1500
1509
  const origin = req.headers.get("Origin");
1501
1510
  if (!isApiAuthRequired(config)) {
1502
1511
  if (!isLoopbackRequestHost(req.headers.get("Host"))) return false;
1503
- return !origin || isLoopbackOriginValue(origin);
1512
+ return !origin || isLoopbackOriginValue(origin) || isExtraAllowedOrigin(origin, config);
1504
1513
  }
1505
- return !origin || isLoopbackOriginValue(origin) || isSameOriginAsRequest(req, origin);
1514
+ return !origin || isLoopbackOriginValue(origin) || isSameOriginAsRequest(req, origin) || isExtraAllowedOrigin(origin, config);
1506
1515
  }
1507
1516
 
1508
1517
  export function corsHeaders(req?: Request, config?: OcxConfig): Record<string, string> {
@@ -1557,13 +1566,27 @@ export function assertServerAuthConfig(config: OcxConfig): void {
1557
1566
 
1558
1567
  export function hasValidApiAuth(req: Request, config: OcxConfig): boolean {
1559
1568
  if (!isApiAuthRequired(config)) return true;
1569
+ const actual = req.headers.get("x-opencodex-api-key")?.trim()
1570
+ || req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
1571
+ if (!actual) return false;
1572
+ // Check env-based token
1560
1573
  const expected = configuredApiAuthToken(config);
1561
- const actual = req.headers.get("x-opencodex-api-key")?.trim();
1562
- if (!expected || !actual) return false;
1563
- const enc = new TextEncoder();
1564
- const expectedBytes = enc.encode(expected);
1565
- const actualBytes = enc.encode(actual);
1566
- return expectedBytes.length === actualBytes.length && timingSafeEqual(actualBytes, expectedBytes);
1574
+ if (expected) {
1575
+ const enc = new TextEncoder();
1576
+ const expectedBytes = enc.encode(expected);
1577
+ const actualBytes = enc.encode(actual);
1578
+ if (expectedBytes.length === actualBytes.length && timingSafeEqual(actualBytes, expectedBytes)) return true;
1579
+ }
1580
+ // Check config-based API keys
1581
+ if (config.apiKeys?.length) {
1582
+ const enc = new TextEncoder();
1583
+ const actualBytes = enc.encode(actual);
1584
+ for (const k of config.apiKeys) {
1585
+ const keyBytes = enc.encode(k.key);
1586
+ if (keyBytes.length === actualBytes.length && timingSafeEqual(actualBytes, keyBytes)) return true;
1587
+ }
1588
+ }
1589
+ return false;
1567
1590
  }
1568
1591
 
1569
1592
  function requireApiAuth(req: Request, config: OcxConfig, kind: "management" | "data-plane"): Response | null {
@@ -2070,6 +2093,37 @@ async function handleManagementAPI(req: Request, url: URL, config: OcxConfig): P
2070
2093
  return jsonResponse({ success: true });
2071
2094
  }
2072
2095
 
2096
+ // ---------------------------------------------------------------------------
2097
+ // API Keys management
2098
+ // ---------------------------------------------------------------------------
2099
+ if (url.pathname === "/api/keys" && req.method === "GET") {
2100
+ const keys = config.apiKeys ?? [];
2101
+ return jsonResponse({ keys: keys.map(k => ({ id: k.id, name: k.name, prefix: k.key.slice(0, 8) + "...", createdAt: k.createdAt })), endpoint: `http://${config.hostname ?? "127.0.0.1"}:${config.port ?? 10100}/v1/responses` }, 200, req, config);
2102
+ }
2103
+
2104
+ if (url.pathname === "/api/keys" && req.method === "POST") {
2105
+ const body = await req.json() as { name?: string };
2106
+ const name = (body.name ?? "").trim() || "default";
2107
+ // Generate key from provider keys hash + random salt
2108
+ const providerKeys = Object.values(config.providers).map(p => p.apiKey ?? "").filter(Boolean).join("|");
2109
+ const salt = crypto.randomUUID();
2110
+ const hashInput = `${providerKeys}|${salt}|${Date.now()}`;
2111
+ const hashBuf = new Bun.CryptoHasher("sha256").update(hashInput).digest();
2112
+ const key = "ocx_" + Buffer.from(hashBuf).toString("hex").slice(0, 40);
2113
+ const entry = { id: crypto.randomUUID(), name, key, createdAt: new Date().toISOString() };
2114
+ config.apiKeys = [...(config.apiKeys ?? []), entry];
2115
+ saveConfig(config);
2116
+ return jsonResponse({ id: entry.id, name: entry.name, key: entry.key, createdAt: entry.createdAt }, 201, req, config);
2117
+ }
2118
+
2119
+ if (url.pathname === "/api/keys" && req.method === "DELETE") {
2120
+ const body = await req.json() as { id?: string };
2121
+ if (!body.id) return jsonResponse({ error: "id required" }, 400, req, config);
2122
+ config.apiKeys = (config.apiKeys ?? []).filter(k => k.id !== body.id);
2123
+ saveConfig(config);
2124
+ return jsonResponse({ success: true }, 200, req, config);
2125
+ }
2126
+
2073
2127
  if (url.pathname === "/api/stop" && req.method === "POST") {
2074
2128
  const { restoreNativeCodex } = await import("./codex-inject");
2075
2129
  const { stopServiceIfInstalled } = await import("./service");
package/src/types.ts CHANGED
@@ -251,6 +251,8 @@ export interface OcxConfig {
251
251
  shutdownTimeoutMs?: number;
252
252
  /** Advertise supports_websockets so Codex opens the WS endpoint. Default false; set true to opt in. */
253
253
  websockets?: boolean;
254
+ /** Generated API keys for external access to the proxy's /v1/responses endpoint. */
255
+ apiKeys?: Array<{ id: string; name: string; key: string; createdAt: string }>;
254
256
  /** Auto-start/sync the proxy from the Codex shim before launching Codex. Default true. */
255
257
  codexAutoStart?: boolean;
256
258
  /**
@@ -278,6 +280,8 @@ export interface OcxConfig {
278
280
  upstreamFailoverThreshold?: number;
279
281
  /** Background proactive token refresh ("Token Guardian"). Off by default; see OcxTokenGuardianConfig. */
280
282
  tokenGuardian?: OcxTokenGuardianConfig;
283
+ /** Additional origins allowed for CORS (e.g. ["https://clisu-oracle.tail19a2d7.ts.net"]). Loopback origins are always allowed. */
284
+ corsAllowOrigins?: string[];
281
285
  }
282
286
 
283
287
  /**