Skip to content

Feat/codex device flow - #3195

Merged
diegosouzapw merged 6 commits into
diegosouzapw:release/v3.8.11from
zhiru:feat/codex-device-flow
Jun 5, 2026
Merged

diegosouzapw merged 6 commits into
diegosouzapw:release/v3.8.11from
zhiru:feat/codex-device-flow

Conversation

@zhiru

@zhiru zhiru commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Codex login via OpenAI's browser-driven device authorization flow, exposed as a shareable "Adicionar Externo" link. A dashboard user generates a single-use public link (/connect/codex/{token}); a third party opens it and completes the OpenAI device login in their own browser — required because auth.openai.com blocks datacenter IPs (Cloudflare) but allows CORS, so the flow can't run server-side. The resulting tokens are persisted as a Codex connection, and the dashboard polls the ticket so it notifies and refreshes the connection list automatically on completion. Also re-enables the Codex "Import auth" button in the provider header, and includes an app-wide i18n fix so next-intl v4 accepts the existing flat dotted compliance.eventTypes keys.

Related Issues

Closes #
Related to #

Validation

  • npm run lint — 0 errors (pre-existing no-explicit-any warnings only)
  • npm run test:unit — 8395 passing; 3 failures are pre-existing WSL-only environment flakes in cli-environment-helpers.test.ts (detectRestrictedEnvironment returns wsl instead of gitpod/ci/replit because the local box is WSL). Unrelated to this change; green on CI Linux.
  • npm run test:coverage — to be confirmed by CI
  • Coverage is still >= 60% for statements, lines, functions, and branches — confirmed by CI. Note: the test:coverage script's enforced gate is currently 40%, not 60% — please confirm which threshold applies.
  • SonarQube PR analysis is green or any remaining issues are explicitly documented below — CI

Tests Added Or Updated

  • tests/unit/codex-device-flow.test.ts — client-side flow (usercode → poll → exchange; 404 gating, pending, timeout, abort, orchestrator).
  • tests/unit/codex-finalize-tokens.test.ts — finalizeTokens/Codex mapTokens (email + workspace from id_token, no-organizations fallback, missing id_token).
  • tests/unit/codex-device-flow-tickets.test.ts — ticket lifecycle (pending → claimed → completed, single-use, release/retry, provider mismatch, expired).
  • tests/unit/i18n-nest-dotted-keys.test.ts — flat→nested key normalization (preserves dots in values, guards prototype pollution).

Coverage Notes

This PR changes src/. Coverage by area:

  • New logic (device flow module, ticket store, finalizeTokens, i18n normalization) — covered by the unit tests above.
  • Routes (/api/codex/connect/[token], OAuth actions public-link / public-link-status / device-complete) — payload validation, gating, and the persistence/ticket logic they call are unit-tested; there is no native HTTP integration test for these routes (mocking @/models in the native runner is costly).
  • UI (providers/[id]/page.tsx, CodexConnectClient.tsx) — no component test; this is UI over already-covered endpoints, validated manually end-to-end.
  • Follow-up: add an integration test for the public route (GET validate + POST complete via ticket) to raise coverage on that surface.

Reviewer Notes

  • CORS dependency (highest risk): the device flow runs in the visitor's browser calling auth.openai.com directly; it depends on open CORS (validated manually E2E). If an environment blocks it, the fallback is a companion app.
  • Routing: the public page lives at /connect/codex/{token} on purpose — /codex/* is OmniRoute's responses-API alias (returned 405). /connect/* and /api/codex/connect/ are classified PUBLIC (no dashboard auth); security comes from the single-use, 15-min, in-memory ticket (globalThis, so it does not survive restarts or span multiple instances).
  • i18n: nestDottedKeys in src/i18n/request.ts runs per request for every locale — an app-wide fix for a pre-existing invalid-key bug (next-intl v4 rejects dotted keys), not Codex-specific.
  • Unused endpoint: the authenticated device-complete action is a reusable building block not wired to any UI button (the "Externo" flow uses the public endpoint). Keep or remove — TBD.
  • Tickets in memory: fine for an ephemeral link; migrate to SQLite if multi-instance/persistence is needed.

zhiru and others added 5 commits June 4, 2026 20:23
…istence

Add the foundation for Codex login via OpenAI's device authorization flow,
which must run in the user's browser: auth.openai.com blocks datacenter IPs
(Cloudflare) but allows CORS, so a server-side poll is not viable.

- src/lib/oauth/codexDeviceFlow.ts: client-side module implementing OpenAI's
  custom "deviceauth" flow (usercode -> poll -> authorization_code exchange),
  with admin-gating (404), pending (403/404), timeout and abort handling.
- providers.ts finalizeTokens(): run postExchange + mapTokens on tokens the
  browser already exchanged, without an HTTP token exchange.
- route.ts device-complete action + persistOAuthConnection helper: persist the
  final tokens via the same email+workspaceId upsert as the other OAuth flows.
- oauthDeviceCompleteSchema for request validation.
- Unit tests for the device flow module and finalizeTokens/mapTokens (incl. the
  no-organizations workspace fallback).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fase 6: a shareable single-use link lets a third party complete the Codex
device flow in their own browser (which also sidesteps the datacenter-IP block,
since the request originates from the visitor's residential IP).

- deviceFlowTickets.ts: in-memory, single-use, 15-min tickets (globalThis,
  mirroring the existing __codexCallbackState pattern).
- connectionPersistence.ts: shared persistOAuthConnection extracted from the
  OAuth route so both the authenticated device-complete action and the public
  endpoint reuse the same email+workspaceId upsert.
- OAuth route: new authenticated `public-link` action (generates the ticket +
  public URL) and `device-complete`; resolvePublicBaseUrl honors forwarded host.
- /api/codex/connect/[token]: public GET (validate) + POST (consume ticket →
  finalizeTokens → persist), NOT behind dashboard auth.
- /codex/connect/[token]: public page that runs the browser device flow, shows
  the user code + verification URL (open/copy), and auto-completes.
- Providers screen: "Adicionar Externo" button + popup with the link (open/copy).
- Ticket store unit tests (single-use, provider-mismatch, expiry).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mpletion

Move the public connect page out of the /codex/* namespace (which OmniRoute
aliases to the responses API, causing a 405) to /connect/codex/{token}, and
classify it + its completion endpoint as PUBLIC so a third party reaches them
without dashboard auth.

- authz: classify /connect/* as PUBLIC (new public_connect_page reason); add
  /api/codex/connect/ to the public API prefixes.
- deviceFlowTickets: pending -> claimed -> completed lifecycle with the resulting
  connection recorded; claim is single-use and reverts on persistence failure.
- public route: claim -> finalizeTokens -> persist -> complete (release on error);
  GET validate only succeeds while pending.
- OAuth route: authenticated `public-link-status` action for dashboard polling;
  public-link now points at /connect/codex/{token}.
- Providers screen: the "Adicionar Externo" popup polls the ticket status and,
  on completion, fires a success toast, refreshes the connection list and closes.
- Ticket store tests updated for the claim/complete/release/status lifecycle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
compliance.eventTypes ships flat keys containing "." (e.g. "apiKey.activate",
"auth.login.success"), which next-intl v4 rejects (INVALID_KEY) since "." denotes
nesting — crashing message loading. Normalize dotted keys into nested objects in
the request config before handing messages to next-intl. Idempotent, recursive,
applies to every locale, preserves dots in values, and guards prototype pollution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 5, 2026 03:48
@zhiru
zhiru requested a review from diegosouzapw as a code owner June 5, 2026 03:48
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Copilot AI left a comment

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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a public, ticket-gated Codex device-flow connect experience (browser-driven), plus i18n message normalization to support dotted keys under next-intl v4.

Changes:

  • Introduces Codex browser-driven device flow (request → poll → exchange) and public connect page + API endpoint.
  • Adds single-use in-memory device-flow tickets and shared OAuth connection persistence helper.
  • Normalizes i18n message objects by nesting dotted keys and adds unit tests for both i18n + device flow helpers.

Reviewed changes

Copilot reviewed 18 out of 20 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tsconfig.json Extends TS include globs for build-time Next types.
tests/unit/i18n-nest-dotted-keys.test.ts Adds unit coverage for dotted-key nesting + prototype pollution protection.
tests/unit/codex-finalize-tokens.test.ts Adds unit coverage for token finalization without server-side exchange.
tests/unit/codex-device-flow.test.ts Adds unit coverage for Codex device-flow steps via mocked fetch.
tests/unit/codex-device-flow-tickets.test.ts Adds unit coverage for single-use ticket lifecycle.
src/shared/validation/schemas.ts Adds schema for out-of-band device-flow token completion payload.
src/shared/constants/publicApiRoutes.ts Marks new public Codex connect API prefix as public.
src/server/authz/types.ts Adds route classification reason for public connect pages.
src/server/authz/classify.ts Treats /connect/* pages as PUBLIC routes.
src/lib/oauth/providers.ts Adds finalizeTokens to reuse map/persist tail without exchanging.
src/lib/oauth/deviceFlowTickets.ts Implements in-memory single-use tickets for public connect flow.
src/lib/oauth/connectionPersistence.ts Adds shared upsert + cloud sync for OAuth connections.
src/lib/oauth/codexDeviceFlow.ts Implements browser-only Codex “deviceauth” flow orchestration.
src/i18n/request.ts Adds nestDottedKeys and applies it to merged messages.
src/app/connect/codex/[token]/page.tsx Adds public connect page wrapper for tokenized route.
src/app/connect/codex/[token]/CodexConnectClient.tsx Adds client UI that runs device flow and POSTs tokens to backend.
src/app/api/oauth/[provider]/[action]/route.ts Adds actions for generating public links, polling ticket status, and device-complete persistence.
src/app/api/codex/connect/[token]/route.ts Adds public ticket-gated codex completion endpoint (GET validate, POST persist).
src/app/(dashboard)/dashboard/providers/[id]/page.tsx Adds dashboard UX to generate + poll external connect link.
.gitignore Ignores local data directories.

Comment thread src/i18n/request.ts Outdated
Comment on lines +48 to +69
const out: Record<string, unknown> = {};
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
const nested = nestDottedKeys(raw);
const parts = key.split(".");
let cursor = out;
let bail = false;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
// Guard against prototype pollution from a crafted message tree.
if (part === "__proto__" || part === "constructor" || part === "prototype") {
bail = true;
break;
}
const existing = cursor[part];
if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
cursor[part] = {};
}
cursor = cursor[part] as Record<string, unknown>;
}
if (bail) continue;
cursor[parts[parts.length - 1]] = nested;
}
Comment thread src/i18n/request.ts Outdated
Comment on lines +61 to +64
const existing = cursor[part];
if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
cursor[part] = {};
}
Comment on lines +18 to +28
/**
* Constant-time string comparison to prevent timing-oracle attacks (CWE-208).
* Handles null/undefined safely and different-length strings.
*/
function safeEqual(a: string | null | undefined, b: string | null | undefined): boolean {
if (a == null || b == null) return a === b;
const ba = Buffer.from(String(a));
const bb = Buffer.from(String(b));
if (ba.length !== bb.length) return false;
return timingSafeEqual(ba, bb);
}
Comment on lines +85 to +92
function resolvePublicBaseUrl(request: Request): string {
const env = process.env.NEXT_PUBLIC_BASE_URL || process.env.OMNIROUTE_PUBLIC_BASE_URL;
if (env && env.trim()) return env.trim().replace(/\/+$/, "");
const host = request.headers.get("x-forwarded-host") || request.headers.get("host");
const proto = request.headers.get("x-forwarded-proto") || "https";
if (host) return `${proto}://${host}`;
return new URL(request.url).origin;
}
/**
* Public Codex connect page (outside the dashboard auth gate).
*
* A third party opens the shared `/codex/connect/{token}` link and completes the
/**
* Public Codex device-flow completion endpoint (NOT behind dashboard auth).
*
* Reached by a third party who opened the shared `/codex/connect/{token}` link.
Comment on lines +72 to +77
export interface CodexDeviceTokens {
access_token: string;
refresh_token: string;
id_token: string;
expires_in: number;
}
const out = nestDottedKeys(input) as any;
assert.equal(out.compliance.eventTypes.apiKey.activate, "API Key Activated");
assert.equal(out.compliance.eventTypes.apiKey.scopes.grant, "API Key Scopes Granted");
assert.equal(out.compliance.eventTypes.apiKey.scopes.revoke, "API Key Scopes Revoke".concat("d"));
@diegosouzapw

Copy link
Copy Markdown
Owner

Thanks @zhiru — the browser-driven device flow with a shareable /connect/codex/{token} link is a clever way around auth.openai.com blocking datacenter IPs. This is a sizable feature (~1.5k lines, new public routes + authz changes), so it needs a focused security pass before it can merge — flagging the areas I want to verify:

  1. Public route surface. /connect/codex/{token} and /api/codex/connect/[token] are added to publicApiRoutes / classify.ts. I want to confirm: single-use token enforcement, expiry/TTL on the ticket (deviceFlowTickets.ts), rate-limiting, and that a leaked/guessed token can't bind an OAuth credential to someone else's instance.
  2. Token entropy + binding. How is the ticket token generated and scoped to the originating dashboard session/connection? Can it be replayed?
  3. CORS/CSRF on the public connect endpoints.
  4. Note it overlaps conceptually with the Codex auth import already merged in Fix/codex import auth #3185 (different mechanism) — worth confirming they coexist cleanly.

Tests are present which is great. Could you confirm the token lifecycle points above (or point me at where they're enforced)? I'll do a dedicated security review pass on this one rather than batch it. Leaving open.

@diegosouzapw
diegosouzapw changed the base branch from main to release/v3.8.11 June 5, 2026 11:54
# Conflicts:
#	src/app/(dashboard)/dashboard/providers/[id]/page.tsx
#	src/i18n/request.ts
@diegosouzapw

Copy link
Copy Markdown
Owner

Thanks @zhiru — this is a genuinely useful feature. 👏 The public ticket-gated device-flow neatly works around auth.openai.com blocking datacenter IPs by completing the OpenAI device login in the visitor's own browser.

I reviewed the security surface and it holds up: tickets are 256-bit (randomBytes(32).toString("base64url")), single-use, 15-min TTL, bound to a specific connectionId, and publicApiRoutes only exposes /api/codex/connect/ (the handler enforces its own ticket check). Nice test coverage too (17/17 across the three suites).

I resolved two conflicts with release/v3.8.11:

  • page.tsx: kept both codex actions — your "Adicionar Externo" button and the existing "Import auth" button (they're complementary).
  • i18n/request.ts: release already shipped the dotted-key nesting fix (refactor(i18n): restructure eventTypes to nested object format #3167's setNestedValue), so I kept that one — your inline strings don't need the extra nestDottedKeys copy.

typecheck:core + lint green. Merging into release/v3.8.11 — ships in the next release. 🚀

@diegosouzapw
diegosouzapw merged commit 2942ba8 into diegosouzapw:release/v3.8.11 Jun 5, 2026
1 check passed
diegosouzapw added a commit that referenced this pull request Jun 5, 2026
…s + add contributor hall

Consolidate the split [Unreleased]/[3.8.11] sections into one, add entries for
every merged contributor PR that was missing credit (#3170/#3171/#3172 @pizzav-xyz,
#3185/#3195 @zhiru, #3188 @xz-dev, #3189/#3203/#3204/#3241 @wilsonicdev, #3191 @bypanghu,
#3206 @juandisay, #3217 @oyi77, #3226 @miracuves, #3187/#3200 maintainer), drop stale
v3.8.8 leftovers (#2958/#2959 already shipped) and 3 empty v3.8.10 stub headers.
@diegosouzapw diegosouzapw mentioned this pull request Jun 5, 2026
wilsonicdev pushed a commit to wilsonicdev/OmniRoute that referenced this pull request Jun 6, 2026
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
Codex public device-flow connect link (ticket-gated) + dashboard CTA. Integrated into release/v3.8.11.
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
Codex public device-flow connect link (ticket-gated) + dashboard CTA. Integrated into release/v3.8.11.
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
Codex public device-flow connect link (ticket-gated) + dashboard CTA. Integrated into release/v3.8.11.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants