Login with ChatGPT
Reference

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

OptionTypeDefaultNotes
basePathstring"/api/chatgpt"Mount path; leading / added, trailing / stripped.
secretstringephemeral randomSigns cookies (HMAC-SHA256) and encrypts tokens (AES-GCM). Logs a warning when omitted. Always set it outside local dev.
sessionStoreKeyValueStore<StoredSession>new MemoryStore()Use a shared store in production.
cookieNamestring"lwc_session"Session cookie name.
cookiePartial<CookieOptions>{}Overrides: path, domain, maxAge, secure, httpOnly, sameSite.
sessionTtlMsnumber2_592_000_000 (30 days)Session and cookie lifetime.
defaultModelstring"gpt-5.5"Injected when a /responses body omits model.
enableResponsesProxybooleantruefalse disables the /responses and /models routes.
responsesProxyResponsesProxyPolicy{}Guardrails below.
allowedOriginsreadonly string[][]Extra origins allowed on cookie-authenticated non-GET routes.
dangerouslyAllowTokenExportbooleanfalseEnables dangerouslyGetTokens() / deprecated getTokens(). Only use when your server code intentionally accepts custody of raw bearer tokens.
dangerouslyAllowRefreshTokenExportbooleanfalseAlso allows { includeRefreshToken: true }. Requires dangerouslyAllowTokenExport; use only for deliberate migration/export flows.

responsesProxy

OptionTypeDefaultNotes
allowedModelsreadonly string[] | (model: string) => booleanall allowedDisallowed models get 403 model_not_allowed.
maxRequestBytesnumber41_943_040 (40 MiB)Maximum raw JSON body size, including base64 image-editing inputs.
rateLimitfalse | { 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.

Codex request defaults

Applied to proxied /responses bodies when the request does not set them (see request normalization):

OptionTypeEffective defaultNotes
instructionsstringa generic assistant promptSent when the body has no instructions.
reasoningEffort"none" | "low" | "medium" | "high" | "xhigh""medium"Overridden per request by the reasoning header.
reasoningSummarystring"auto"Merged into reasoning.
textVerbosity"low" | "medium" | "high""medium"Merged into text.
serviceTier"auto" | "default" | "flex" | "priority" | "fast"unsetApplied when the body has no service_tier.

Upstream configuration

Inherited from ChatGPTConfig. Override only when you know why (core reference): clientId, issuer, scope, codexBaseUrl, 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:

app/api/chat/route.ts
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.

On this page