Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids.
| `visionSidecar?` | `OcxVisionSidecarConfig` | on | Vision sidecar options (see below). |
| `images?` | `OcxImagesConfig` | automatic OpenAI selection | Standalone Images relay options for Codex's built-in `image_gen` tool (see below). |
| `tokenGuardian?` | `OcxTokenGuardianConfig` | off | Optional proactive OAuth refresh and Codex-account warmup policy; fields are listed below. |
| `corsAllowOrigins?` | `string[]` | `[]` | Additional exact origins allowed by CORS. Loopback origins are always allowed. |
| `corsAllowOrigins?` | `string[]` | `[]` | Additional exact origins allowed by CORS. Loopback origins are always allowed. Authority-based browser extension origins such as `chrome-extension://<extension-id>` are supported; `*` is not a wildcard. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -i '^configuration\.md$' docs-site/src/content/docs | sort
rg -n -C 2 'corsAllowOrigins|chrome-extension|not a wildcard|不是通配符' \
  docs-site/src/content/docs

Repository: lidge-jun/opencodex

Length of output: 6108


Sync localized CORS documentation

Update the corsAllowOrigins rows in the Japanese (line 66), Korean (line 67), and Russian (line 71) pages. State that chrome-extension://<extension-id> origins are supported and * is not a wildcard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-site/src/content/docs/reference/configuration.md` at line 73, Update the
corsAllowOrigins documentation rows in the Japanese, Korean, and Russian
localized pages to match the English description: state that authority-based
browser extension origins such as chrome-extension://<extension-id> are
supported and that * is not a wildcard, while preserving the existing
configuration details.

Source: Path instructions


`codexAccountNamespaces` keys are public selectors: 1–64 characters, starting and ending with an
ASCII letter or number, with letters, numbers, `.`, `_`, or `-` inside; reserved JavaScript object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ no-replace 方式创建 `config.json.pre-openai-tiers-v2.bak`,并把已知旧
| `webSearchSidecar?` | `OcxWebSearchSidecarConfig` | 开启 | 网络搜索 sidecar 选项(见下文)。 |
| `visionSidecar?` | `OcxVisionSidecarConfig` | 开启 | 视觉 sidecar 选项(见下文)。 |
| `tokenGuardian?` | `OcxTokenGuardianConfig` | 关闭 | 可选的 proactive OAuth 刷新和 Codex account warmup 策略;字段见下文。 |
| `corsAllowOrigins?` | `string[]` | `[]` | CORS 额外允许的精确 origin。loopback origin 始终允许。 |
| `corsAllowOrigins?` | `string[]` | `[]` | CORS 额外允许的精确 origin。loopback origin 始终允许;支持 `chrome-extension://<扩展 ID>` 等基于 authority 的浏览器扩展 origin,`*` 不是通配符。 |

`codexAccountNamespaces` 的 key 是公开 selector:长度为 1–64 个字符,首尾必须是 ASCII 字母或数字,
中间可使用字母、数字、`.`、`_` 或 `-`;保留的 JavaScript object 名称会被拒绝。value 必须是有效的
Expand Down
23 changes: 18 additions & 5 deletions src/server/auth-cors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,28 @@ export function isAllowedRequestOrigin(req: Request, config: OcxConfig): boolean

function isExtraAllowedOrigin(origin: string, cfg: OcxConfig): boolean {
if (!cfg.corsAllowOrigins?.length) return false;
const parsedOrigin = comparableOrigin(origin);
return cfg.corsAllowOrigins.some(allowed => {
try {
return new URL(allowed).origin === new URL(origin).origin;
} catch {
return allowed === origin;
}
const parsedAllowed = comparableOrigin(allowed);
return parsedOrigin !== null && parsedAllowed !== null
? parsedAllowed === parsedOrigin
: allowed === origin;
});
}

function comparableOrigin(value: string): string | null {
try {
const parsed = new URL(value);
if (parsed.origin !== "null") return parsed.origin;
// WHATWG URL exposes authority-based custom schemes (for example browser
// extensions) as opaque `null` origins. Compare their scheme + authority so
// one allowlisted extension cannot admit every other opaque origin.
return parsed.host ? `${parsed.protocol}//${parsed.host}` : null;
} catch {
return null;
}
}

export function managementRequestOrigin(req: Request, config: OcxConfig): string | null {
const host = req.headers.get("Host");
const parsedHost = parseHttpHost(host);
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@ export interface OcxConfig {
combos?: Record<string, OcxComboConfig>;
/** Background proactive token refresh ("Token Guardian"). Off by default; see OcxTokenGuardianConfig. */
tokenGuardian?: OcxTokenGuardianConfig;
/** Additional origins allowed for CORS (e.g. ["https://clisu-oracle.tail19a2d7.ts.net"]). Loopback origins are always allowed. */
/** Additional exact origins allowed for CORS (e.g. HTTPS or chrome-extension://<id>). Loopback origins are always allowed. */
corsAllowOrigins?: string[];
}

Expand Down
37 changes: 37 additions & 0 deletions tests/server-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,43 @@ describe("server local API auth", () => {
}
});

test("extension allowlist gates preflight and data-plane requests by authority", async () => {
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
mkdirSync(TEST_DIR, { recursive: true });
process.env.OPENCODEX_HOME = TEST_DIR;
const extensionOrigin = "chrome-extension://modkelfkcfjpgbfmnbnllalkiogfofh";
saveConfig({
...config("127.0.0.1"),
corsAllowOrigins: [extensionOrigin],
});
stubModelDiscoveryFor("https://api.example.test");

const server = startServer(0);
const modelsUrl = new URL("/v1/models", server.url);
try {
const preflight = await fetch(modelsUrl, {
method: "OPTIONS",
headers: {
origin: extensionOrigin,
"access-control-request-method": "GET",
},
});
expect(preflight.status).toBe(204);
expect(preflight.headers.get("access-control-allow-origin")).toBe(extensionOrigin);

const accepted = await fetch(modelsUrl, { headers: { origin: extensionOrigin } });
expect(accepted.status).toBe(200);
expect(accepted.headers.get("access-control-allow-origin")).toBe(extensionOrigin);

const rejected = await fetch(modelsUrl, {
headers: { origin: "chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" },
});
expect(rejected.status).toBe(403);
} finally {
await server.stop(true);
}
});

test("loopback management API rejects host-header same-origin rebinding", async () => {
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
mkdirSync(TEST_DIR, { recursive: true });
Expand Down
35 changes: 35 additions & 0 deletions tests/server-loopback-host-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,38 @@ describe("isAllowedRequestOrigin over a forwarded port", () => {
).toBe(false);
});
});

describe("isAllowedRequestOrigin with extension origins", () => {
test("admits only the configured browser extension authority", () => {
const config = {
...loopbackConfig,
corsAllowOrigins: ["chrome-extension://modkelfkcfjpgbfmnbnllalkiogfofh"],
} as OcxConfig;

expect(
isAllowedRequestOrigin(
request("localhost:10100", "chrome-extension://modkelfkcfjpgbfmnbnllalkiogfofh"),
config,
),
).toBe(true);
expect(
isAllowedRequestOrigin(
request("localhost:10100", "chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
config,
),
).toBe(false);
expect(
isAllowedRequestOrigin(
request("localhost:10100", "moz-extension://modkelfkcfjpgbfmnbnllalkiogfofh"),
config,
),
).toBe(false);

expect(
isAllowedRequestOrigin(
request("localhost:10100", "chrome-extension://modkelfkcfjpgbfmnbnllalkiogfofh"),
{ ...loopbackConfig, corsAllowOrigins: ["*"] } as OcxConfig,
),
).toBe(false);
});
});
Loading