Server handler
createChatGPTHandler() options, defaults, and the getSession / proxyFetch / getModels helpers.
import { createChatGPTHandler } from "@opencoredev/loginwithchatgpt-server";
const auth = createChatGPTHandler(options);Returns a ChatGPTHandler:
interface ChatGPTHandler {
basePath: string;
handler: (request: Request) => Promise<Response>;
fetch: (request: Request) => Promise<Response>; // alias of handler
proxyFetch: (request: Request) => typeof fetch;
getSession: (request: Request) => Promise<PublicSession>;
getModels: (request: Request) => Promise<string[] | undefined>;
dangerouslyGetTokens: (request: Request, options?: { includeRefreshToken?: boolean }) => Promise<ChatGPTTokens | undefined>;
/** @deprecated Prefer proxyFetch(), /responses, or /models. */
getTokens: (request: Request, options?: { includeRefreshToken?: boolean }) => Promise<ChatGPTTokens | undefined>;
}Mount handler at ${basePath}/*. It only requires Web-standard Request,
Response, fetch, and crypto.subtle, so use it in runtimes that provide
those APIs. The routes it serves are documented in HTTP routes.
Options
Server options
| Option | Type | Default | Notes |
|---|---|---|---|
basePath | string | "/api/chatgpt" | Mount path; leading / added, trailing / stripped. |
secret | string | ephemeral random | Signs cookies (HMAC-SHA256) and encrypts tokens (AES-GCM). Logs a warning when omitted. Always set it outside local dev. |
sessionStore | KeyValueStore<StoredSession> | new MemoryStore() | Use a shared store in production. |
cookieName | string | "lwc_session" | Session cookie name. |
cookie | Partial<CookieOptions> | {} | Overrides: path, domain, maxAge, secure, httpOnly, sameSite. |
sessionTtlMs | number | 2_592_000_000 (30 days) | Session and cookie lifetime. |
defaultModel | string | "gpt-5.5" | Injected when a /responses body omits model. |
enableResponsesProxy | boolean | true | false disables the /responses and /models routes. |
enableRealtime | boolean | true | false disables both direct and app-server Realtime routes. |
realtime | RealtimeProxyPolicy | {} | Experimental private voice signaling policy. |
responsesProxy | ResponsesProxyPolicy | {} | Guardrails below. |
allowedOrigins | readonly string[] | [] | Extra origins allowed on cookie-authenticated non-GET routes. |
dangerouslyAllowTokenExport | boolean | false | Enables dangerouslyGetTokens() / deprecated getTokens(). Only use when your server code intentionally accepts custody of raw bearer tokens. |
dangerouslyAllowRefreshTokenExport | boolean | false | Also allows { includeRefreshToken: true }. Requires dangerouslyAllowTokenExport; use only for deliberate migration/export flows. |
responsesProxy
| Option | Type | Default | Notes |
|---|---|---|---|
allowedModels | readonly string[] | (model: string) => boolean | all allowed | Disallowed models get 403 model_not_allowed. |
maxRequestBytes | number | 41_943_040 (40 MiB) | Maximum raw JSON body size, including base64 image-editing inputs. |
rateLimit | false | { limit?, windowMs?, store? } | { limit: 30, windowMs: 60_000 } | Per-session limit; false disables. Default store is per-instance memory; pass a shared store across instances. |
realtime
| Option | Type | Default | Notes |
|---|---|---|---|
maxRequestBytes | number | 262_144 | Maximum JSON SDP + session body. |
allowedTransports | readonly ("wm" | "vp" | "vps")[] | ["wm"] | Undocumented compatibility transports require explicit opt-in. |
allowedModes | readonly ("wingman" | "advanced" | "standard")[] | unrestricted | Disallowed requests get 403 realtime_mode_not_allowed. |
sessionDefaults | ChatGPTRealtimeSessionOptions | {} | Defaults merged before browser options. |
getAuth | callback | unset | Supplies server-held ChatGPT web auth for direct /wm; Codex login tokens are intentionally not reused for that route. |
appServer | RealtimeAppServerPolicy | unset | Enables desktop-style native handoffs and arbitrary server-side tools. Requires Node/Bun and the codex executable. |
realtime.appServer
| Option | Type | Default | Notes |
|---|---|---|---|
tools | readonly RealtimeDynamicTool[] | required | Server-owned function names, descriptions, and JSON input schemas. |
executeTool | callback | required | Revalidate arguments and execute under loginSessionId / the authenticated request snapshot. |
confirmTool | callback | unset | Required when any tool returns pendingConfirmation; called only by the authenticated confirmation route. |
allowedModels | readonly string[] | (model: string) => boolean | all allowed | Restricts the delegated execution model selected by the browser. |
defaultModel | string | unset | Delegated execution model when the browser omits one. |
reasoningEffort | ReasoningEffort | "low" | App-server execution effort; low keeps voice-tool turns responsive. |
sessionTtlMs | number | 1_800_000 | Maximum Live process lifetime. A new call also closes the previous call for that login session. |
command | readonly string[] | codex --enable realtime_conversation app-server --stdio | App-server executable override. |
executionInstructions | string | built-in tool-use instructions | Instructions for delegated execution turns. |
realtimePrompt | string | built-in native voice prompt | Prompt for the interruptible voice surface. |
sessionFactory | (options) => RealtimeAppServerSessionHandle | built-in session | Advanced process-hosting or test override using the minimal lifecycle contract. |
The handler adds request, loginSessionId, and liveSessionId to tool and
confirmation callbacks. Tokens stay inside the SDK. Tool results must be
JSON-serializable. If executeTool returns pendingConfirmation, the bridge
immediately completes the model-side tool request so the call remains
responsive; the consequential action stays pending until the application calls
the confirmation route.
Codex request defaults
Applied to proxied /responses bodies when the request does not set them
(see request normalization):
| Option | Type | Effective default | Notes |
|---|---|---|---|
instructions | string | a generic assistant prompt | Sent when the body has no instructions. |
reasoningEffort | "none" | "low" | "medium" | "high" | "xhigh" | "medium" | Overridden per request by the reasoning header. |
reasoningSummary | string | "auto" | Merged into reasoning. |
textVerbosity | "low" | "medium" | "high" | "medium" | Merged into text. |
serviceTier | "auto" | "default" | "flex" | "priority" | "fast" | unset | Applied when the body has no service_tier. |
Upstream configuration
Inherited from ChatGPTConfig. Override only when you know why
(core reference): clientId, issuer, scope,
codexBaseUrl, realtimeBaseUrl, Realtime client identifiers, originator,
clientVersion, fetch.
Helpers
All three read the session cookie from the request and are safe in any server context (route handlers, server components, middleware).
getSession(request)
Returns public state without touching upstream:
const session = await auth.getSession(request);
// { status: "authenticated", user: { accountId, email, name, plan } }
// or { status: "unauthenticated" | "pending" | "expired" | "error" }proxyFetch(request)
Creates a request-scoped fetch that carries the user's session cookie back through the mounted handler. Use it when you want a custom server route with AI SDK control, but you do not want application code to receive bearer tokens:
import { createChatGPTProxyProvider } from "@opencoredev/loginwithchatgpt-ai";
import { streamText } from "ai";
export async function POST(request: Request) {
const chatgpt = createChatGPTProxyProvider({
fetch: auth.proxyFetch(request),
});
const { prompt } = await request.json();
const result = streamText({ model: chatgpt(), prompt });
return result.toUIMessageStreamResponse();
}The provider still calls /api/chatgpt/responses and /api/chatgpt/models;
the handler loads, refreshes, and injects credentials internally.
getModels(request)
Returns the account's model slugs, or undefined when unauthenticated.
Refreshes tokens before calling upstream. Throws ChatGPTAuthError with code
models_request_failed on upstream failure.
dangerouslyGetTokens(request, options?)
Disabled unless createChatGPTHandler({ dangerouslyAllowTokenExport: true })
is set. When enabled, returns a fresh access token plus account id, with the
refresh token redacted by default.
Exporting bearer tokens moves trust from the SDK boundary into your application
code: server code can log, copy, or forward them. Prefer proxyFetch(request)
for request handling. Only enable this for advanced integrations that
intentionally accept that custody model.
const tokens = await auth.dangerouslyGetTokens(request);
// { accessToken, accountId, expiresAt } with no refreshToken
const exported = await auth.dangerouslyGetTokens(request, {
includeRefreshToken: true,
});
// Requires dangerouslyAllowRefreshTokenExport: true. Use only for migration/export flows.Session storage
Sessions are stored under the cookie's session id. When secret is set, token
JSON is AES-GCM encrypted before writing. The store contract:
interface KeyValueStore<T> {
get(key: string): Promise<T | undefined> | T | undefined;
set(key: string, value: T, options?: { ttlMs?: number }): Promise<void> | void;
delete(key: string): Promise<void> | void;
}MemoryStore (exported for tests and local dev) enforces TTL lazily at read
time. How the cookie, store, and refresh cycle fit together is covered in
Sessions & tokens.