Files
OmniRoute/open-sse/executors/codex/appServerAuthProbe.ts
Armin Anton” ∴ 8f390efffd feat(codex): self-contained codex app-server transport (executor + provider + sign-in) (#11205)
Merged after conflict resolution: the 5 conflicting test files were the base-red drains that #11201 already landed on the tip — kept the tip versions; the feature content is untouched. Validated on the combined batch board + this branch: codex-app-server + codex-gpt56-catalog 25/25, typecheck:core clean, docs-counts green (351 providers), provider-consistency 268/351/0. The opt-in codex-app-server transport (JSON-RPC-over-WS, turn/completed-awaited close, Responses SSE bridge) leaves the default codex path untouched. Thank you @arminanton — a 3.4k-line transport with the docs wave and tests to match!
2026-08-23 10:20:06 -03:00

103 lines
4.0 KiB
TypeScript

/**
* Layer-2 auth-status probe for the Codex app-server transport.
*
* The HTTP `/readyz` endpoint only proves the app-server PROCESS is up — not that
* its Codex CLI is signed in. A public user whose CLI is not yet authenticated
* would otherwise see a green "ready" badge and then fail on the first turn with
* an upstream auth error. This probe opens the same JSON-RPC/WebSocket the
* executor uses and calls `account/read` (verified against codex 0.149.0): an
* authenticated server returns `{ account: { type, email, planType }, ... }`;
* a logged-out server returns no account (or an error). So the presence of
* `result.account` is the "authenticated" signal.
*
* Kept separate from the executor turn path so the health check pulls in only the
* lightweight client + transport, and so it is independently unit-testable with a
* fake websocketFn.
*/
import {
CodexAppServerClient,
type CodexAppServerWebsocketFn,
} from "./appServerClient.ts";
import type { CodexAppServerConfig } from "./appServerConfig.ts";
export type CodexAppServerAuthStatus =
| { state: "authenticated"; account: { type?: string; email?: string; planType?: string } }
| { state: "logged_out"; reason: string }
| { state: "unknown"; reason: string };
interface AccountReadResult {
account?: { type?: unknown; email?: unknown; planType?: unknown } | null;
requiresOpenaiAuth?: unknown;
}
function str(v: unknown): string | undefined {
return typeof v === "string" && v.length > 0 ? v : undefined;
}
/**
* Open a short-lived WS to the app-server, initialize, and read the account.
* Returns an auth status; never throws (maps failures to state "unknown").
*
* @param config resolved app-server config (url + capability token).
* @param websocketFn the wreq-js websocket factory
* (getCodexAppServerWebsocketTransport()); when null, returns "unknown".
* @param timeoutMs overall budget for connect + account/read.
*/
export async function probeCodexAppServerAuth(
config: CodexAppServerConfig,
websocketFn: CodexAppServerWebsocketFn | null,
timeoutMs = 8000
): Promise<CodexAppServerAuthStatus> {
if (!websocketFn) {
return { state: "unknown", reason: "websocket transport unavailable" };
}
const client = new CodexAppServerClient({ websocketFn, defaultTimeoutMs: timeoutMs });
const deadline = new Promise<CodexAppServerAuthStatus>((resolve) =>
setTimeout(() => resolve({ state: "unknown", reason: "auth probe timed out" }), timeoutMs)
);
const run = (async (): Promise<CodexAppServerAuthStatus> => {
try {
await client.connect(config.url, config.token);
await client.request(
"initialize",
{
clientInfo: { name: "omniroute-codex-app-server-health", title: null, version: "1.0" },
capabilities: null,
},
timeoutMs
);
// account/read: authenticated → { account: {...} }; logged out → no account.
const result = (await client.request("account/read", {}, timeoutMs)) as AccountReadResult;
const account = result?.account;
if (account && typeof account === "object") {
return {
state: "authenticated",
account: {
type: str(account.type),
email: str(account.email),
planType: str(account.planType),
},
};
}
return {
state: "logged_out",
reason: "app-server reachable but its Codex CLI is not signed in",
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
// A JSON-RPC error on account/read (e.g. AuthRequiredError) also means
// "up but not authenticated" — surface it as logged_out, not unknown, so
// the dashboard offers "Sign in with ChatGPT" rather than a scary error.
if (/auth|login|sign|unauthor|401/i.test(message)) {
return { state: "logged_out", reason: message };
}
return { state: "unknown", reason: message };
} finally {
client.close();
}
})();
return Promise.race([run, deadline]);
}