mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 18:52:18 +03:00
* Bypass proxy compaction for native Codex context
* Add native ChatGPT Web provider pipeline
* Add managed browser and tunnel deployment
* Add ChatGPT Web setup and doctor UI
* Document and test ChatGPT Web integration
* fix(security): register chatgpt-web-codex-doctor in LOCAL_ONLY_API_PATTERNS
The diagnostic route under /api/providers/{id}/chatgpt-web-codex-doctor
was not registered in the spawn-capable route guard. Adding it for
parity with the existing /login pattern.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(providers): route chatgpt-web-codex admin routes through a service boundary
The provider CRUD/doctor routes imported chatgpt-web-codex helpers
(finalizeValidatedChatGptWebCodexSecrets, encode/decodeChatGptWebCodexSecrets,
getChatGptWebCodexDoctorStatus) directly from open-sse/executors/**, which
no-restricted-imports (EXECUTOR_IMPORT_RESTRICTION) forbids for src/app/**
files — executor implementations must stay behind an open-sse handler or
service boundary.
Add open-sse/services/chatgptWebCodexAdmin.ts as a thin re-export boundary
(mirroring the existing tokenRefresh.ts re-export pattern) and import from
there instead. No behavior change.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
152 lines
5.3 KiB
TypeScript
152 lines
5.3 KiB
TypeScript
import { createHash, randomUUID } from "node:crypto";
|
|
|
|
import { normalizeCodexSessionId } from "./codexClient.ts";
|
|
|
|
const CODEX_INSTALLATION_SALT = "omniroute-codex-installation";
|
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
|
|
export type CodexClientIdentity = {
|
|
sessionId: string;
|
|
turnId: string;
|
|
windowId: string;
|
|
installationId: string;
|
|
};
|
|
|
|
function normalizeUuid(value: unknown): string | null {
|
|
return typeof value === "string" && UUID_PATTERN.test(value.trim()) ? value.trim() : null;
|
|
}
|
|
|
|
function uuidFromStableValue(value: string): string {
|
|
const hash = createHash("sha256").update(value).digest("hex");
|
|
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-a${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
|
|
}
|
|
|
|
export function getCodexInstallationId(
|
|
providerSpecificData?: Record<string, unknown> | null
|
|
): string {
|
|
const explicit = normalizeUuid(providerSpecificData?.codexInstallationId);
|
|
if (explicit) return explicit;
|
|
|
|
const stableSource =
|
|
typeof providerSpecificData?.workspaceId === "string" && providerSpecificData.workspaceId.trim()
|
|
? providerSpecificData.workspaceId.trim()
|
|
: typeof providerSpecificData?.accountId === "string" && providerSpecificData.accountId.trim()
|
|
? providerSpecificData.accountId.trim()
|
|
: typeof providerSpecificData?.email === "string" && providerSpecificData.email.trim()
|
|
? providerSpecificData.email.trim()
|
|
: "default";
|
|
|
|
return uuidFromStableValue(`${CODEX_INSTALLATION_SALT}:${stableSource}`);
|
|
}
|
|
|
|
export function createCodexClientIdentity(
|
|
sessionId: string | null,
|
|
providerSpecificData?: Record<string, unknown> | null
|
|
): CodexClientIdentity | null {
|
|
const normalizedSessionId = normalizeCodexSessionId(sessionId);
|
|
if (!normalizedSessionId) return null;
|
|
return {
|
|
sessionId: normalizedSessionId,
|
|
turnId: randomUUID(),
|
|
windowId: `${normalizedSessionId}:0`,
|
|
installationId: getCodexInstallationId(providerSpecificData),
|
|
};
|
|
}
|
|
|
|
export function applyCodexClientIdentityHeaders(
|
|
headers: Record<string, string>,
|
|
identity?: CodexClientIdentity | null
|
|
): void {
|
|
if (!identity) return;
|
|
headers["session_id"] = identity.sessionId;
|
|
headers["x-client-request-id"] = identity.sessionId;
|
|
headers["x-codex-window-id"] = identity.windowId;
|
|
headers["x-codex-turn-metadata"] = JSON.stringify({
|
|
session_id: identity.sessionId,
|
|
thread_source: "user",
|
|
turn_id: identity.turnId,
|
|
sandbox: "none",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* #3697: detect the Codex CLI as the request *client* (not the routed provider) from
|
|
* request headers, so the model-echo shim can fire regardless of which upstream provider
|
|
* ultimately serves the request (e.g. `codex/gpt-5.5-xhigh` routed through a combo).
|
|
* Mirrors the `originator`/User-Agent detection proven in `isCodexModelCatalogClient`
|
|
* (PR #3481, `src/app/api/v1/models/catalogRequest.ts`) — Codex CLI sends an `originator`
|
|
* header of `codex_exec`/`codex_cli_rs` and a matching `codex_*` User-Agent — but works off
|
|
* a plain headers bag (`Headers` or a header-name→value record) instead of a `Request`,
|
|
* since chatCore's `clientRawRequest.headers` is not always a `Request`.
|
|
*/
|
|
export function isCodexOriginatedHeaders(
|
|
headers: Headers | Record<string, unknown> | null | undefined
|
|
): boolean {
|
|
const getHeader = (name: string): string => {
|
|
if (headers instanceof Headers) {
|
|
return headers.get(name)?.toLowerCase() ?? "";
|
|
}
|
|
if (headers && typeof headers === "object") {
|
|
for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {
|
|
if (key.toLowerCase() === name && typeof value === "string") {
|
|
return value.toLowerCase();
|
|
}
|
|
}
|
|
}
|
|
return "";
|
|
};
|
|
|
|
if (getHeader("originator").startsWith("codex")) return true;
|
|
return getHeader("user-agent").startsWith("codex");
|
|
}
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: null;
|
|
}
|
|
|
|
/** Require the native Codex thread/turn binding; prompt text and cache keys are not authority. */
|
|
export function hasNativeCodexTurnBinding(body: unknown): boolean {
|
|
const metadata = asRecord(asRecord(body)?.client_metadata);
|
|
const raw = metadata?.["x-codex-turn-metadata"];
|
|
let turn = asRecord(raw);
|
|
if (typeof raw === "string") {
|
|
try {
|
|
turn = asRecord(JSON.parse(raw));
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
return (
|
|
typeof turn?.thread_id === "string" &&
|
|
turn.thread_id.trim().length > 0 &&
|
|
typeof turn.turn_id === "string" &&
|
|
turn.turn_id.trim().length > 0
|
|
);
|
|
}
|
|
|
|
export function isVerifiedNativeCodexRequest(
|
|
body: unknown,
|
|
headers: Headers | Record<string, unknown> | null | undefined
|
|
): boolean {
|
|
return isCodexOriginatedHeaders(headers) && hasNativeCodexTurnBinding(body);
|
|
}
|
|
|
|
export function applyCodexClientMetadata(
|
|
body: Record<string, unknown>,
|
|
identity?: CodexClientIdentity | null
|
|
): void {
|
|
if (!identity) return;
|
|
const existing =
|
|
body.client_metadata &&
|
|
typeof body.client_metadata === "object" &&
|
|
!Array.isArray(body.client_metadata)
|
|
? (body.client_metadata as Record<string, unknown>)
|
|
: {};
|
|
body.client_metadata = {
|
|
...existing,
|
|
"x-codex-installation-id": identity.installationId,
|
|
};
|
|
}
|