Skip to content

feat(auth): OIDC as optional dashboard admin login gate (password fallback preserved) - #6973

Merged
diegosouzapw merged 8 commits into
cheaper-inference:release/v3.8.49from
mikolaj92:feat/oidc-dashboard-login-gate
Jul 20, 2026
Merged

diegosouzapw merged 8 commits into
cheaper-inference:release/v3.8.49from
mikolaj92:feat/oidc-dashboard-login-gate

Conversation

@mikolaj92

Copy link
Copy Markdown
Contributor

Summary

Replace the web dashboard login gate with OIDC while keeping password login as a fully supported fallback.

Scope (minimal sensible):

  • Only the dashboard/MANAGEMENT gate (/dashboard, /api/auth/* for login).
  • No changes to /v1/* proxy traffic, API keys, managementPassword, managementPolicy, MCP, CLI, or proxy auth.
  • Single-admin model preserved.
  • Uses existing auth_token JWT (30 days, { authenticated: true }, same cookie attributes).

Changes

  • src/lib/db/settings.ts: OIDC config fields (oidcEnabled, oidcIssuer, oidcClientId, oidcClientSecret, oidcScopes, oidcRedirectPath, oidcAllowedSubjects).
  • src/shared/constants/publicApiRoutes.ts: /api/auth/oidc/* marked public.
  • src/shared/utils/apiAuth.ts: hasConfiguredOidc(); isAuthRequired treats full OIDC config as valid auth method (password still works).
  • src/app/api/settings/require-login/route.ts: exposes oidcEnabled.
  • src/app/login/page.tsx: conditional OIDC button when configured.
  • New routes:
    • GET /api/auth/oidc/login — builds authorize URL via discovery, sets oidc_state.
    • GET /api/auth/oidc/callback — code exchange, ID token validation via jose + createRemoteJWKSet, optional subject/email whitelist, mints identical auth_token, clears state, redirects to /dashboard.
  • src/app/api/auth/oidc/callback/route.ts: added test seam oidcCallbackInternals.getCookieStore, all redirects made absolute (originEarly), setupComplete: true on first successful OIDC login (bootstrap parity with password path).
  • Tests:
    • tests/unit/oidc-callback.test.ts — 8 focused integration tests (happy path + all major error paths using mocked fetch + real jose-signed tokens).
    • Updated assertions in public-api-routes.test.ts and api-auth.test.ts.

Constraints followed

  • No new dependencies (jose + fetch already present).
  • Password path untouched and remains available.
  • Bootstrap/misconfig behavior preserved (partial OIDC config does not satisfy isAuthRequired).
  • Same JWT/cookie shape as password login.
  • All NextResponse.redirect use absolute URLs (Next.js 16 compatibility).

Verification

  • Focused test run (53 tests across OIDC + auth surface): 53 pass / 0 fail.
  • No package/lock changes for the feature itself.
  • Exact same auth_token JWT payload and cookie attributes as password login.

Manual testing (recommended before merge)

  1. Configure OIDC in Settings (Issuer, Client ID, Secret, optional Allowed Subjects).
  2. Log in via OIDC button.
  3. Verify redirect to dashboard and auth_token cookie.
  4. Password login should still work as fallback.
  5. Test error cases (invalid state, subject not allowed, misconfigured, etc.).

Closes the dashboard OIDC login gate request while preserving the existing password mechanism.

…fallback)

- Settings: oidcEnabled + issuer/client/secret/scopes/redirect/allowedSubjects
- Public routes: /api/auth/oidc/ prefix (authorize + callback reachable)
- isAuthRequired: full OIDC config acts as auth method (gate requires login); partial does not (bootstrap preserved)
- New endpoints:
  - GET /api/auth/oidc/login — IdP redirect (absolute redirect_uri from request + discovery)
  - GET /api/auth/oidc/callback — code exchange, ID token validation (jose + JWKS), optional sub/email whitelist, mints identical 30d auth_token JWT + cookie as password login, redirects to /dashboard
- require-login endpoint now returns oidcEnabled
- Login UI: conditional OIDC button when enabled; password form untouched as fallback
- Tests:
  - public-api-routes: OIDC prefixes are public
  - api-auth: isAuthRequired true with full OIDC (no password); partial OIDC keeps bootstrap semantics

No new deps. No changes to proxy, keys, managementPassword, policies, MCP, CLI. Single-admin preserved.
- Add comprehensive integration test for /api/auth/oidc/callback
  (happy path + error paths: invalid_state, subject_not_allowed,
   not_configured, token_exchange, id_token_invalid, missing_code,
   server_misconfigured)
- Use static test seam (oidcCallbackInternals) for cookie store
- Mark setupComplete on first successful OIDC login (bootstrap parity)
- Ensure all redirects use absolute URLs (Next.js 16 compatibility)
- Verify identical auth_token JWT/cookie behavior as password path
- No new dependencies; reuses jose + fetch

Password login remains fully supported as fallback.
@mikolaj92
mikolaj92 requested a review from diegosouzapw as a code owner July 12, 2026 14:38

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request implements optional OpenID Connect (OIDC) authentication for the dashboard admin gate, adding login and callback API routes, updating the login UI, and extending settings and authentication utilities to support OIDC configuration. The review feedback focuses on enhancing the robustness, performance, and security of this new authentication flow. Key recommendations include caching the JWKS client globally to prevent redundant network requests, wrapping token exchange fetches in try-catch blocks with timeouts to avoid unhandled exceptions, performing case-insensitive email comparisons for the subject whitelist, and dynamically applying the secure attribute to the state cookie based on the request protocol.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +7 to +9
export const oidcCallbackInternals = {
getCookieStore: cookies,
};

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.

high

To avoid recreating the JWKS client on every request and to leverage jose's built-in caching of retrieved keys, we should define a module-level cache for JWKS clients. This prevents unnecessary network roundtrips to the OIDC provider's JWKS endpoint on every login attempt.

export const oidcCallbackInternals = {
  getCookieStore: cookies,
};

// Cache JWKS clients globally to reuse retrieved keys and avoid fetching JWKS on every login request.
const jwksClientsCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();

function getJwksClient(jwksUri: string) {
  let client = jwksClientsCache.get(jwksUri);
  if (!client) {
    client = createRemoteJWKSet(new URL(jwksUri));
    jwksClientsCache.set(jwksUri, client);
  }
  return client;
}

Comment thread src/app/api/auth/oidc/callback/route.ts Outdated

// Validate ID token
try {
const JWKS = createRemoteJWKSet(new URL(jwksUri));

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.

high

Use the globally cached JWKS client helper instead of instantiating a new one on every request.

Suggested change
const JWKS = createRemoteJWKSet(new URL(jwksUri));
const JWKS = getJwksClient(jwksUri);

Comment thread src/app/api/auth/oidc/callback/route.ts Outdated
Comment on lines +107 to +120
const tokenResp = await fetch(tokenEndpoint, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: tokenParams.toString(),
});

if (!tokenResp.ok) {
return NextResponse.redirect(`${originEarly}/login?oidc_error=token_exchange`);
}

const tokenData: unknown = await tokenResp.json();
if (!tokenData || typeof tokenData !== "object") {
return NextResponse.redirect(`${originEarly}/login?oidc_error=token_response`);
}

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.

high

The token exchange fetch and the subsequent tokenResp.json() call are not wrapped in a try-catch block. Any network error or malformed JSON response will throw an unhandled exception and crash the route with a 500 error. Wrapping these in try-catch blocks ensures graceful error handling and redirects the user back to the login page with a clean error code. Additionally, we should add a timeout to the fetch request to prevent hanging connections.

  let tokenResp: Response;
  try {
    tokenResp = await fetch(tokenEndpoint, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: tokenParams.toString(),
      signal: AbortSignal.timeout(10000),
    });
  } catch {
    return NextResponse.redirect(`${originEarly}/login?oidc_error=token_exchange`);
  }

  if (!tokenResp.ok) {
    return NextResponse.redirect(`${originEarly}/login?oidc_error=token_exchange`);
  }

  let tokenData: unknown;
  try {
    tokenData = await tokenResp.json();
  } catch {
    return NextResponse.redirect(`${originEarly}/login?oidc_error=token_response`);
  }

  if (!tokenData || typeof tokenData !== "object") {
    return NextResponse.redirect(`${originEarly}/login?oidc_error=token_response`);
  }

Comment thread src/app/api/auth/oidc/callback/route.ts Outdated
let tokenEndpoint = `${issuer}/token`;
let jwksUri = `${issuer}/jwks`;
try {
const wellKnownResp = await fetch(`${issuer}/.well-known/openid-configuration`);

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.

medium

Add a timeout to the discovery fetch request to prevent the connection from hanging indefinitely if the OIDC provider is unresponsive.

    const wellKnownResp = await fetch(`${issuer}/.well-known/openid-configuration`, {
      signal: AbortSignal.timeout(5000),
    });

Comment on lines +136 to +148
// Optional subject / email whitelist
const allowed = Array.isArray(settings.oidcAllowedSubjects) ? settings.oidcAllowedSubjects : [];
if (allowed.length > 0) {
const sub = typeof payload.sub === "string" ? payload.sub : "";
const email =
typeof (payload as Record<string, unknown>).email === "string"
? ((payload as Record<string, unknown>).email as string)
: "";
const ok = allowed.some((v: unknown) => typeof v === "string" && (v === sub || v === email));
if (!ok) {
return NextResponse.redirect(`${originEarly}/login?oidc_error=subject_not_allowed`);
}
}

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.

medium

Email addresses are case-insensitive by nature. Performing a strict case-sensitive comparison (v === email) can cause unexpected login failures if there is a casing mismatch between the OIDC provider's response and the configured allowed subjects. Comparing emails case-insensitively resolves this usability issue.

    // Optional subject / email whitelist
    const allowed = Array.isArray(settings.oidcAllowedSubjects) ? settings.oidcAllowedSubjects : [];
    if (allowed.length > 0) {
      const sub = typeof payload.sub === "string" ? payload.sub : "";
      const email = 
        typeof (payload as Record<string, unknown>).email === "string"
          ? ((payload as Record<string, unknown>).email as string).toLowerCase()
          : "";
      const ok = allowed.some((v: unknown) => {
        if (typeof v !== "string") return false;
        return v === sub || v.toLowerCase() === email;
      });
      if (!ok) {
        return NextResponse.redirect(`${originEarly}/login?oidc_error=subject_not_allowed`);
      }
    }

Comment thread src/app/api/auth/oidc/login/route.ts Outdated
// Discover authorization_endpoint
let authEndpoint = `${issuer}/authorize`;
try {
const wellKnownResp = await fetch(`${issuer}/.well-known/openid-configuration`);

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.

medium

Add a timeout to the discovery fetch request to prevent the connection from hanging indefinitely if the OIDC provider is unresponsive.

    const wellKnownResp = await fetch(`${issuer}/.well-known/openid-configuration`, {
      signal: AbortSignal.timeout(5000),
    });

Comment on lines +77 to +83
res.cookies.set("oidc_state", state, {
httpOnly: true,
sameSite: "lax",
path: "/",
maxAge: 60 * 10,
secure: process.env.AUTH_COOKIE_SECURE === "true",
});

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.

security-medium medium

To ensure consistent cookie security, the oidc_state cookie should be marked secure dynamically when running over HTTPS, matching the logic used for the auth_token cookie in the callback route.

  const isHttpsRequest = scheme === "https";
  const useSecureCookie = process.env.AUTH_COOKIE_SECURE === "true" || isHttpsRequest;

  res.cookies.set("oidc_state", state, {
    httpOnly: true,
    sameSite: "lax",
    path: "/",
    maxAge: 60 * 10,
    secure: useSecureCookie,
  });

…shboard login gate

- Add module-level JWKS client cache (Record) + getJwksClient helper
- Wrap token exchange fetch + .json() in try/catch with 10s timeout
- Add 5s timeout to discovery fetch in both /login and /callback routes
- Case-insensitive email comparison in oidcAllowedSubjects whitelist
- Make oidc_state cookie 'secure' dynamic based on request protocol (matches auth_token)
- Expose clearJwksCache on test seam for isolation

All reviewer suggestions applied (adjusted for project rules on Map/Record).
Tests: 53/53 pass.
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thanks for this — solid core implementation. jose-based JWKS/signature/issuer/audience validation, CSRF state cookie, sanitized error redirects, and the 49 unit tests hold up.

Two things need to land before merge:

  1. No way to configure it. updateSettingsSchema (src/shared/validation/settingsSchemas.ts) is .strict() and has no oidc* fields, and no settings UI or API route in this PR writes oidcEnabled/oidcIssuer/oidcClientId/oidcClientSecret/oidcAllowedSubjects. As shipped, an operator cannot turn this on short of raw SQLite writes.
  2. oidcClientSecret isn't encrypted at rest. It goes through updateSettings() → plain JSON.stringify, unlike provider credentials which run through encryptConnectionFields (AES-256-GCM) in src/lib/db/providers.ts.

Also worth addressing while wiring the settings path: gate oidcEnabled behind the existing SECURITY_IMPACTING_KEYS re-auth check, and consider requiring a non-empty oidcAllowedSubjects before allowing oidcEnabled: true (the empty default currently lets any subject from the configured IdP become dashboard admin). Once configurable, a live test against a real IdP completes the validation per the repo's testing protocol.

@diegosouzapw
diegosouzapw changed the base branch from main to release/v3.8.47 July 12, 2026 17:08
@mikolaj92

Copy link
Copy Markdown
Contributor Author

All Gemini Code Assist review comments addressed (JWKS cache, token exchange try/catch+timeout, discovery timeouts, case-insensitive email, dynamic secure cookie). Ready for re-review. Tests: 53/53 pass.

@mikolaj92

Copy link
Copy Markdown
Contributor Author

@diegosouzapw

All Gemini Code Assist review comments have been addressed:

  • Added module-level JWKS client cache + getJwksClient helper (avoids recreating client on every request)
  • Wrapped token exchange fetch + .json() in try/catch with 10s timeout
  • Added 5s timeout to discovery fetch in both /login and /callback routes
  • Made email comparison case-insensitive in oidcAllowedSubjects whitelist
  • Made oidc_state cookie secure flag dynamic based on request protocol (matches auth_token logic)

Tests: 53/53 pass.

Ready for re-review.

@mikolaj92

Copy link
Copy Markdown
Contributor Author

@diegosouzapw

All points from the Gemini Code Assist review have been addressed:

  • Module-level JWKS cache + getJwksClient helper (prevents recreating the client on every login)
  • Token exchange fetch and .json() wrapped in try/catch with 10s timeout
  • 5s timeout added to discovery fetch in both /login and /callback
  • Email comparison made case-insensitive in oidcAllowedSubjects whitelist
  • oidc_state cookie now sets secure dynamically based on request protocol (same logic as auth_token)

Tests: 53/53 pass.

Ready for re-review.

@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.47 to release/v3.8.48 July 13, 2026 05:04
…CTING_KEYS + encrypt/decrypt + non-empty subjects guard (address maintainer review)
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.48 to release/v3.8.49 July 13, 2026 21:57
@mikolaj92

Copy link
Copy Markdown
Contributor Author

I’ve addressed the points from your review in the latest commit. When you have a chance, could you please take another look? Thank you!

@diegosouzapw

Copy link
Copy Markdown
Collaborator

This PR is now behind release/v3.8.49 and shows conflicts — could you rebase onto the current tip? No competing OIDC work has landed, so the conflicts look like plain drift rather than anything superseding your change.

Preserve both the OIDC client-secret decrypt-on-read and the session
affinity legacy fallback introduced independently on the release branch.

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com>
diegosouzapw added a commit that referenced this pull request Jul 19, 2026
… (owner-approved)

The /fix-prs validation-train sweep surfaced a cluster of otherwise-clean
contributor feature PRs (#6973/#7683/#7662/#7672/#7633/#7767) whose per-PR
+1/+2 own-growth collectively exceeded the tip's 3-unit complexity slack
(2056 vs 2059). This was the 4th such block of the day (#7695/#7747/#7768
each needed helper extraction earlier). Owner approved raising both ceilings
to give new-feature PRs breathing room: complexity to 2072 (combined-cluster
2068 + 4 headroom), cognitive to 900 (combined 896 + 4). Structural shrink
stays debt (#3501); tighten via --update next cycle.
diegosouzapw and others added 2 commits July 19, 2026 21:19
…oidcEnabled

The security-impacting-keys re-auth gate in PATCH /api/settings assigned to
`storedPasswordHash` without ever declaring it (no `let`/`const`), so every
PATCH touching a SECURITY_IMPACTING_KEYS field (requireLogin, newPassword,
oidcEnabled, oidcClientSecret, the bypass toggles) threw a ReferenceError in
strict-mode ESM and fell through to the generic 500 handler. That masked the
expected 400/401/200 outcomes in settings-audit and settings-route-password
password-migration tests. Declare it as a block-scoped `const` where it's
first assigned.

Also updates the login-bootstrap-route contract tests: the public
/api/settings/require-login GET now legitimately includes `oidcEnabled` in
its response (the login page needs it to decide whether to render the OIDC
button) — the three closed-shape assertions are extended to expect
`oidcEnabled: false`, matching route.ts's existing behavior.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
diegosouzapw added a commit that referenced this pull request Jul 20, 2026
…cognitive 950

Tip was at 2069/2072 and 900/900 (zero slack) after the day's 17 merges; the
remaining queue (#6973, #7662, #7719, #7744, #7779 reworks) was collectively
blocked. Owner picked the wide margin in chat (2026-07-20).
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Validated in local merge-train on 192.168.0.113 @ 9d2b92a63acdf6915dbcb5d9ae7a4b18caf79d6d (FAST gates green: static + changed tests + vitest; today's full-suite parity ran in trains 4+5)

@diegosouzapw
diegosouzapw merged commit 2acc8e8 into cheaper-inference:release/v3.8.49 Jul 20, 2026
3 checks passed
diegosouzapw added a commit that referenced this pull request Jul 20, 2026
…wlist + safe error redirects

Two findings from automated security review of the just-merged #6973 OIDC login gate:
- HIGH: the allowlist matched the `email` claim without checking `email_verified`, so
  an attacker with an IdP account whose unverified email equals an allowlisted address
  could pass the gate. Now the email claim is only honored when email_verified === true.
- MEDIUM: error redirects built their target from the raw Host header (reflected
  open-redirect). Now resolved against the framework-parsed request URL.

Regression test added asserting an unverified allowlisted email is rejected.
@diegosouzapw diegosouzapw mentioned this pull request Jul 23, 2026
diegosouzapw added a commit that referenced this pull request Jul 28, 2026
Phase 1 step 6b. Swept the cycle's 284 New Features bullets against the existing
docs before writing anything: nearly every large theme (Kimi, xAI OAuth, session
affinity, bun:sqlite, Firecrawl, Opus 5, omniglyph, GCF v3.2, homologation suite)
was already covered. Six real gaps were left undocumented by the PRs that shipped
them, each verified in source before being written up:

- CredentialMaskerGuardrail (#7683) is registered in guardrails/registry.ts but the
  GUARDRAILS table listed only 3 of the 4 guardrails
- the cacheAffinity scoring factor and the cache-optimized combo strategy (#8008):
  the docs still said 12 factors / 18 strategies, the code has 13 / 19
- the optional dashboard OIDC login gate (#6973) — /api/auth/oidc/{login,callback}
  had no mention in AUTHZ_GUIDE
- GET /api/usage/cache-health (#8827) and GET /api/usage/model-latency-stats (#6873)
  were missing from the API reference

README "What's New" gains one bullet (routing transparency) and merges two others
rather than growing a second changelog. PROVIDER_REFERENCE regenerated with the
generator (Firecrawl reclassified to Search, Xiaomi MiMo added by #8861).

check:docs-all green: 134 docs, 813 internal links, no fabricated API/env/CLI
references. Known pre-existing drift left alone and reported: stale nominal counts
in ARCHITECTURE/CODEBASE_DOCUMENTATION (soft), the 9-factor mentions scattered in
AUTO-COMBO, and the auto-combo diagram SVG (the renderer needs a browser this
environment does not have — the .mmd source is updated and the .md says so).
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
… (owner-approved)

The /fix-prs validation-train sweep surfaced a cluster of otherwise-clean
contributor feature PRs (cheaper-inference#6973/cheaper-inference#7683/cheaper-inference#7662/cheaper-inference#7672/cheaper-inference#7633/cheaper-inference#7767) whose per-PR
+1/+2 own-growth collectively exceeded the tip's 3-unit complexity slack
(2056 vs 2059). This was the 4th such block of the day (cheaper-inference#7695/cheaper-inference#7747/cheaper-inference#7768
each needed helper extraction earlier). Owner approved raising both ceilings
to give new-feature PRs breathing room: complexity to 2072 (combined-cluster
2068 + 4 headroom), cognitive to 900 (combined 896 + 4). Structural shrink
stays debt (cheaper-inference#3501); tighten via --update next cycle.
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…cognitive 950

Tip was at 2069/2072 and 900/900 (zero slack) after the day's 17 merges; the
remaining queue (cheaper-inference#6973, cheaper-inference#7662, cheaper-inference#7719, cheaper-inference#7744, cheaper-inference#7779 reworks) was collectively
blocked. Owner picked the wide margin in chat (2026-07-20).
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…lback preserved) (cheaper-inference#6973)

* feat(auth): optional OIDC for dashboard admin gate (password remains fallback)

- Settings: oidcEnabled + issuer/client/secret/scopes/redirect/allowedSubjects
- Public routes: /api/auth/oidc/ prefix (authorize + callback reachable)
- isAuthRequired: full OIDC config acts as auth method (gate requires login); partial does not (bootstrap preserved)
- New endpoints:
  - GET /api/auth/oidc/login — IdP redirect (absolute redirect_uri from request + discovery)
  - GET /api/auth/oidc/callback — code exchange, ID token validation (jose + JWKS), optional sub/email whitelist, mints identical 30d auth_token JWT + cookie as password login, redirects to /dashboard
- require-login endpoint now returns oidcEnabled
- Login UI: conditional OIDC button when enabled; password form untouched as fallback
- Tests:
  - public-api-routes: OIDC prefixes are public
  - api-auth: isAuthRequired true with full OIDC (no password); partial OIDC keeps bootstrap semantics

No new deps. No changes to proxy, keys, managementPassword, policies, MCP, CLI. Single-admin preserved.

* feat(auth): add integration test and fixes for OIDC dashboard login gate

- Add comprehensive integration test for /api/auth/oidc/callback
  (happy path + error paths: invalid_state, subject_not_allowed,
   not_configured, token_exchange, id_token_invalid, missing_code,
   server_misconfigured)
- Use static test seam (oidcCallbackInternals) for cookie store
- Mark setupComplete on first successful OIDC login (bootstrap parity)
- Ensure all redirects use absolute URLs (Next.js 16 compatibility)
- Verify identical auth_token JWT/cookie behavior as password path
- No new dependencies; reuses jose + fetch

Password login remains fully supported as fallback.

* fix(auth): address all Gemini Code Assist review comments for OIDC dashboard login gate

- Add module-level JWKS client cache (Record) + getJwksClient helper
- Wrap token exchange fetch + .json() in try/catch with 10s timeout
- Add 5s timeout to discovery fetch in both /login and /callback routes
- Case-insensitive email comparison in oidcAllowedSubjects whitelist
- Make oidc_state cookie 'secure' dynamic based on request protocol (matches auth_token)
- Expose clearJwksCache on test seam for isolation

All reviewer suggestions applied (adjusted for project rules on Map/Record).
Tests: 53/53 pass.

* fix(auth): wire OIDC config into updateSettingsSchema + SECURITY_IMPACTING_KEYS + encrypt/decrypt + non-empty subjects guard (address maintainer review)

* fix(auth): declare storedPasswordHash + align bootstrap contract for oidcEnabled

The security-impacting-keys re-auth gate in PATCH /api/settings assigned to
`storedPasswordHash` without ever declaring it (no `let`/`const`), so every
PATCH touching a SECURITY_IMPACTING_KEYS field (requireLogin, newPassword,
oidcEnabled, oidcClientSecret, the bypass toggles) threw a ReferenceError in
strict-mode ESM and fell through to the generic 500 handler. That masked the
expected 400/401/200 outcomes in settings-audit and settings-route-password
password-migration tests. Declare it as a block-scoped `const` where it's
first assigned.

Also updates the login-bootstrap-route contract tests: the public
/api/settings/require-login GET now legitimately includes `oidcEnabled` in
its response (the login page needs it to decide whether to render the OIDC
button) — the three closed-shape assertions are extended to expect
`oidcEnabled: false`, matching route.ts's existing behavior.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: mikolaj92 <mikolaj92@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com>
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…wlist + safe error redirects

Two findings from automated security review of the just-merged cheaper-inference#6973 OIDC login gate:
- HIGH: the allowlist matched the `email` claim without checking `email_verified`, so
  an attacker with an IdP account whose unverified email equals an allowlisted address
  could pass the gate. Now the email claim is only honored when email_verified === true.
- MEDIUM: error redirects built their target from the raw Host header (reflected
  open-redirect). Now resolved against the framework-parsed request URL.

Regression test added asserting an unverified allowlisted email is rejected.
diegosouzapw added a commit that referenced this pull request Aug 11, 2026
* chore(changelog): v3.8.49 reconciliation — 200 missing bullets + 22 restored credits

Phase 0a of /generate-release. Measured commit<->CHANGELOG coverage over the real
cycle range (2c62333..HEAD, 933 non-merge commits) instead of the last tag: 180
merged PRs had no bullet at all (they landed without a changelog.d fragment) and a
further 19 were invisible because the merge-train landed them under a generic
'Train 1D: merge via --admin' subject that carries no PR reference.

- +200 bullets, all with PR back-reference and author attribution (1179 -> 1379)
- 🙌 Contributors 156 -> 178; credits @terrafirmbot-source for #7904, which shipped
  through the conflict-resolved #8685 without any attribution
- closed-PR credit audit over the 32 human PRs closed unmerged this cycle: 12 had
  already landed under the author's own follow-up PR and were verified credited
- rollup bullet for the direct release-branch maintenance (merge-train landings,
  ratchet re-pins, base-red sweeps) that carries no PR of its own
- [3.8.49] header dated 2026-07-28 (was TBD) in the root file and the 42 i18n mirrors

Coverage after: 0 commits uncovered.

* chore(quality): v3.8.49 pre-flight — clear 4 base-reds, absorb cycle drift

Pre-flight sweep (Phase 0). Test suites ran on the dedicated 32-core box so the
self-inflicted load of `node --test` could not fabricate timing flakes.

Base-reds fixed (all real, all from merged cycle PRs that did not update their
characterization tests):

- providers-constants-split / quota-plan-registry / provider-translate-path GOLDEN:
  #8861 added the Xiaomi MiMo Token Plan provider, so APIKEY_PROVIDERS is 195 (was
  194), knownProviders() is 12 (was 11) and the translate-path snapshot gains one
  purely additive entry. Counts aligned to the shipped catalog, never relaxed.
- agent-skills-content: skills/config-codex-cli/ was added by #8709 with a custom
  block, so the custom-block set is 13, not 12.
- chatcore-compression-integration: #8595/#8560 deliberately decoupled REACTIVE
  context compaction from the `enabled` master switch, so a body above 70% of the
  window is pruned even with compression off. The test was sized above that
  threshold, which made it assert against intended behavior; it now stays below it
  and keeps testing the invariant it was written for (resolveBasePlan short-circuits
  to "off" before reading comboOverrides).

Static gates:

- 3 shellcheck directives were malformed (`# shellcheck disable=SC2086 — text`; the
  em-dash makes shellcheck reject the whole directive as SC1125) in ci.yml and
  nightly-release-green.yml — the comment now sits on its own line.
- gitleaks: 2 new generic-api-key false positives allowlisted with justification —
  a localStorage key for the sponsor banner (#8723) and the PUBLIC Adobe Firefly
  web x-api-key, whose only literals are in JSDoc (the runtime reads it through
  resolvePublicCred, per Hard Rule #11). secretFindings back to 0.
- zizmor 176 -> 189 and bundleSize 6762 -> 7666 rebaselined with the measurement and
  the reason; both are ordinary cycle drift absorbed at release.

Environment-dependent failures classified out, not silenced: the two tproxy tests
assert the native addon is unavailable/unprivileged and therefore fail when the
suite runs as root on the build box (they pass as a normal user), and the
consoleInterceptor rate-limit test is a 4s-timing flake under load (6/6 isolated).

* test(codex): align the Responses HTTP e2e to the #8507 input-item contract

Fifth and last base-red of the v3.8.49 pre-flight. #8507 (#8083) deliberately sets
`status: "completed"` on Responses input items so strict upstream validators accept
them; codex-chat-reasoning-http-e2e still asserted the pre-#8507 shape, so it failed
against intended behavior. Expectation updated with the reason inline — the assertion
is not relaxed, it now pins the current contract.

The test was never reached in the first pre-flight sweep (the run was interrupted
during the integration phase, and this file sorts after the one that failed).

* docs(release): v3.8.49 feature-documentation sync

Phase 1 step 6b. Swept the cycle's 284 New Features bullets against the existing
docs before writing anything: nearly every large theme (Kimi, xAI OAuth, session
affinity, bun:sqlite, Firecrawl, Opus 5, omniglyph, GCF v3.2, homologation suite)
was already covered. Six real gaps were left undocumented by the PRs that shipped
them, each verified in source before being written up:

- CredentialMaskerGuardrail (#7683) is registered in guardrails/registry.ts but the
  GUARDRAILS table listed only 3 of the 4 guardrails
- the cacheAffinity scoring factor and the cache-optimized combo strategy (#8008):
  the docs still said 12 factors / 18 strategies, the code has 13 / 19
- the optional dashboard OIDC login gate (#6973) — /api/auth/oidc/{login,callback}
  had no mention in AUTHZ_GUIDE
- GET /api/usage/cache-health (#8827) and GET /api/usage/model-latency-stats (#6873)
  were missing from the API reference

README "What's New" gains one bullet (routing transparency) and merges two others
rather than growing a second changelog. PROVIDER_REFERENCE regenerated with the
generator (Firecrawl reclassified to Search, Xiaomi MiMo added by #8861).

check:docs-all green: 134 docs, 813 internal links, no fabricated API/env/CLI
references. Known pre-existing drift left alone and reported: stale nominal counts
in ARCHITECTURE/CODEBASE_DOCUMENTATION (soft), the 9-factor mentions scattered in
AUTO-COMBO, and the auto-combo diagram SVG (the renderer needs a browser this
environment does not have — the .mmd source is updated and the .md says so).

* chore(release): v3.8.49 — clear the release-PR CI in one pass

Every finding from the first full ci.yml run on the release PR, fixed or justified
together so a single re-push clears the board.

Lint / check:route-validation:t06 — three routes read request.json() with no visible
Zod validation. The two proxy-subscriptions routes validated with a hand-rolled
parsePayload(); they now use real Zod schemas (src/lib/proxySubscription/schema.ts)
reproducing the same acceptance rules, error strings and status codes. chat/completions
is the proxy's hottest path and parses the body ONCE on purpose (#4380 OOM crash-loop),
so it now safeParses the ALREADY-PARSED object against a deliberately permissive
structural schema — proven not to change behavior: absent model and model:null still
pass through, role "developer" still reaches 200, a ~300 KB payload is accepted, and
the body is still read exactly once. 25 new tests.

i18n UI value drift — 13 English strings rewritten during the cycle left stale
translations in up to 41 locales (317 pairs). Eleven are genuine rewrites and now carry
the pipeline's __MISSING__:<english> marker so the runtime serves corrected English until
translation catches up; vi forbids that marker by test, so it got a real translation.

PR Test Policy — 33 files flagged. Each was verified against the SOURCE, not the diff:
26 assert reductions are legitimate (mostly the #7866 Qwen OAuth provider removal and the
#8013 Antigravity refactor deleting the surface under test) and are allowlisted with the
PR and the evidence; 5 deleted files have verified replacements. One was NOT legitimate:
#7528's GraphQL->WebSocket migration dropped four muse-spark continuation scenarios whose
logic is still live — connection isolation, cache eviction after a failed turn (the commit
itself says "was missing"), parallel-chat cache collision, and the empty-content guard.
All four are restored against the new transport and each was verified to fail when the
corresponding production mechanism is broken.

Quality Ratchet / openapiCoverage — 36.6% against a baseline of 38: the cycle added routes
faster than the spec. Eight real endpoints are now documented from their route.ts
(usage cache-health and model-latency-stats, the two OIDC endpoints, and the five
proxy-subscriptions paths), bringing it to 38.1%.

Quality Gates (Extended) / zizmor — the runner measures 190 where the devbox measures 189
on the same commit, a delta already recorded in this baseline's history. Baselined to the
runner's number.

Also: the driverFactory better-sqlite3 guard moved from a mid-body t.skip() to a declared
{ skip: <condition> } test option. Same behavior for the optional native dependency, but
the skip now shows up in the report and is distinguishable from a test.skip() that silences
a test outright. Verified under both runners: 15/15 on Node, 14/14 on Bun.

SonarCloud Code Analysis stays red and is not a blocker: sonar.qualitygate.wait=false since
#7038 makes the job informative, the built-in gate cannot be swapped on the FREE plan, and
main has no branch protection.

* chore(quality): close the last two release-PR reds

test-masking — I had missed one of the 34 flagged files: my first pass grepped only
paths under tests/, so open-sse/services/__tests__/tierResolver.test.ts was invisible.
Same #7866 cause as the other eight qwen-driven reductions: the "classifies Qwen as
free" case and qwen's entry in the batch list went with the removed provider, and the
batch indices dropped from 10 to 9 (61→59). Allowlisted with that evidence.

dast-smoke — all four Schemathesis findings are on the two OIDC endpoints documented
in the previous commit, and none is a defect. /api/auth/oidc/* is a BROWSER redirect
flow: it answers 302 to the IdP and 302 back to /login?oidc_error=... on every failure,
which Schemathesis reads as "accepted a schema-violating request", and it answers 400
when OIDC is not configured, which it reads as "rejected a schema-compliant request".
Keeping the endpoints in the spec is right — operators need them, and they are what
brought openapi coverage back over the baseline — so the flow is excluded from the fuzz
instead, with the reason inline in the workflow. The rest of /api/auth and /api/keys
stays in scope.

* test(db): reword the driverFactory skip comment so the gate stops counting it

The anti-test-masking gate greps text, not code: my explanation of WHY the
better-sqlite3 guard moved out of the test body spelled the runner API out
literally, and those two mentions inside a comment were counted as two new skip
markers — the exact signal the previous commit set out to clear. Same explanation,
phrased without the call syntax.

Verified with the gate's own exported helpers against the merge-base: 0 modified-file
violations, 0 deletion violations. Test still 15/15.

* fix(dashboard): unbreak the vitest:ui gate — 2 real production bugs + the i18n test seam

The Vitest job is a BLOCKING gate that had not run to completion once in this whole
release: rounds 1-3 cancelled it via cancel-in-progress on each successive fix push,
so its red was indistinguishable from green. Round 4 finally ran it and the suite was
broken cycle-wide.

Root cause of the suite: #7935 instrumented ~180 shared/dashboard components with
next-intl's useTranslations/useLocale without updating the tests that mount them, so
every one of them threw "context from NextIntlClientProvider was not found". Fixed at
the shared seam (tests/_setup/vitestUiPolyfills.ts) rather than per file: a translator
built from the REAL en.json via next-intl's own createTranslator, memoized per
namespace — the naive version returns a fresh function each call and any component
whose useCallback/useEffect depends on t spins forever, which reads as a hang, not a
failure. A local mock still wins over the default. 22 files fixed by the seam alone,
15 realigned to the real strings; no assert removed or weakened.

Two production bugs the suite was hiding, both pre-existing and both with a failing
regression test already in the tree:

- RequestLoggerDetail crashed on a structured error object. #7920 gave the component
  formatErrorForDisplay for exactly this case, then #8213's combo-503 / cooldown
  checks went to the raw field and called .toLowerCase() on it. Both paths now use
  the helper.
- The logs detail modal reopened on first close again. #6830 fixed that by reading the
  deep-link id ONCE; the #8354 page rewrite regressed it by reading the live
  searchParams every render, so the prop flips mid-session and re-fires the child's
  deep-link effect exactly as the modal closes. Frozen at mount again.

Also tightens i18nUiCoverage 75.5 -> 99, which the ratchet demanded under
--require-tighten: the metric genuinely improved as the async translation workflow
paid off the debt that the v3.8.39/.44/.47 rebaselines had been recording. The
collector subtracts placeholders, so this release's 317 __MISSING__ markers are
already netted out of the 99.

Two UI files still fail locally under 20-worker concurrency (combos-page-smoke,
evals-tab-smoke) — cold-import flakes that pass isolated and with a larger timeout.

* test(e2e): repair the four shards the first green Build finally exercised

test-e2e has `needs: [build]`, and the release PR's Build died on every round
until now — so the 9-shard matrix produced ZERO signal for this whole cycle
while ~200 PRs merged. The first successful Build surfaced four independent
breakages, each traced to the commit that caused it:

- providers-management (#7361): the single-connection delete moved from
  window.confirm() to a ConfirmModal, so page.once("dialog") never fired and
  the DELETE was never sent (deleteCalls stayed 0). Click the modal instead.
- providers-bailian-coding-plan (#7882): the free-text Base URL field was
  deliberately replaced by a region step whose choice resolves the endpoint
  (global-sg -> coding-intl.dashscope, china-beijing -> coding.dashscope).
  Both cases rewritten against the region step; the invalid-URL case is
  unreachable from this modal now, so it covers the CN choice instead.
- group-b-activity-feed: the stack-trace guard ran against page.content(),
  which embeds the serialized i18n payload — zenmux's "endpoint at
  /api/v1/chat/completions" is prose, not a leak. Assert on rendered
  innerText and require the :line:col every real stack frame carries.
- navigation (#8292): APP_ROUTE_PATTERN accepted only /login and /dashboard,
  but the new prefetch spec is the sole caller passing /home, so waitForURL
  never resolved and the retry loop burned the full 180s timeout.

E2E is green on main (9/9 on 07-22 and 07-23), so all four are cycle
regressions, not pre-existing debt. Tests only — no production code touched.

* fix(dashboard): stop the /home quick-start cards from prefetching too

#8292 fixed half the RSC prefetch storm: it added prefetch={false} to the
sidebar's navigation and logo links, but /home — the landing route, and the
one its own e2e guard visits — renders five more internal Links in the
quick-start cards. First paint still fired 12 speculative RSC requests for
/dashboard/{analytics,logs,providers,api-manager} and /docs.

That PR shipped the test that would have caught this, but the test never got
to its assertion: gotoDashboardRoute("/home") hung because APP_ROUTE_PATTERN
accepted only /login and /dashboard, so the retry loop burned the whole 180s
timeout with no assertion error. With that helper repaired in the previous
commit, navigation.spec.ts finally ran and reported the 12 requests.

Validated both ways, per Hard Rule #18:
- tests/unit/sidebar-prefetch-policy-8281.test.ts extended to /home — red on
  the parent commit (5 internal Links, 5 without prefetch={false}), green here.
- the e2e assertion expect(speculativeRequests).toEqual([]) is the end-to-end
  guard; it is what surfaced the defect in the first place.

* refactor(dashboard): shrink HomePageClient back under the size gate

The prefetch fix in the parent commit tripped check:file-size — the frozen
budget for this file is 1377 lines and a naive fix measured 1391, because
`href` + `prefetch={false}` + `className` no longer fits Prettier's 100-column
budget, so three one-line <Link> elements each expanded to five.

Followed the gate's own first suggestion (extract/DRY) before touching the
baseline: the quick-start links repeated the same className literal four
times, and the docs link carried a 180-char one inline. Hoisting both into
INLINE_LINK / DOCS_LINK collapses five wrapped <Link> blocks back to a single
line each and removes the duplication — 1391 -> 1381.

The remaining +4 over the frozen budget is the five prefetch attributes
themselves, which cannot be expressed in fewer lines. Rebaselined to 1381
with the rationale recorded in file-size-baseline.json under
_rebaseline_2026_07_29_8281_home_quickstart_prefetch.

tests/unit/sidebar-prefetch-policy-8281.test.ts still passes (2/2): it matches
whole <Link ...> blocks, so it is indifferent to the wrapping and only checks
that every internal link opts out of prefetch.

* fix(bun): use native fetch for direct outbound requests

* test(bun): cover native direct fetch path

* fix(bun): preload polyfill for next build workers

* fix(bun): expose AsyncLocalStorage globally

* fix(bun): filter non-page Fumadocs metadata

* fix(bun): defer docs-only route dependencies

* chore(skills): sync generated OmniRoute agent skill docs

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
Phase 1 step 6b. Swept the cycle's 284 New Features bullets against the existing
docs before writing anything: nearly every large theme (Kimi, xAI OAuth, session
affinity, bun:sqlite, Firecrawl, Opus 5, omniglyph, GCF v3.2, homologation suite)
was already covered. Six real gaps were left undocumented by the PRs that shipped
them, each verified in source before being written up:

- CredentialMaskerGuardrail (cheaper-inference#7683) is registered in guardrails/registry.ts but the
  GUARDRAILS table listed only 3 of the 4 guardrails
- the cacheAffinity scoring factor and the cache-optimized combo strategy (cheaper-inference#8008):
  the docs still said 12 factors / 18 strategies, the code has 13 / 19
- the optional dashboard OIDC login gate (cheaper-inference#6973) — /api/auth/oidc/{login,callback}
  had no mention in AUTHZ_GUIDE
- GET /api/usage/cache-health (cheaper-inference#8827) and GET /api/usage/model-latency-stats (cheaper-inference#6873)
  were missing from the API reference

README "What's New" gains one bullet (routing transparency) and merges two others
rather than growing a second changelog. PROVIDER_REFERENCE regenerated with the
generator (Firecrawl reclassified to Search, Xiaomi MiMo added by cheaper-inference#8861).

check:docs-all green: 134 docs, 813 internal links, no fabricated API/env/CLI
references. Known pre-existing drift left alone and reported: stale nominal counts
in ARCHITECTURE/CODEBASE_DOCUMENTATION (soft), the 9-factor mentions scattered in
AUTO-COMBO, and the auto-combo diagram SVG (the renderer needs a browser this
environment does not have — the .mmd source is updated and the .md says so).
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
… (owner-approved)

The /fix-prs validation-train sweep surfaced a cluster of otherwise-clean
contributor feature PRs (cheaper-inference#6973/cheaper-inference#7683/cheaper-inference#7662/cheaper-inference#7672/cheaper-inference#7633/cheaper-inference#7767) whose per-PR
+1/+2 own-growth collectively exceeded the tip's 3-unit complexity slack
(2056 vs 2059). This was the 4th such block of the day (cheaper-inference#7695/cheaper-inference#7747/cheaper-inference#7768
each needed helper extraction earlier). Owner approved raising both ceilings
to give new-feature PRs breathing room: complexity to 2072 (combined-cluster
2068 + 4 headroom), cognitive to 900 (combined 896 + 4). Structural shrink
stays debt (cheaper-inference#3501); tighten via --update next cycle.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…cognitive 950

Tip was at 2069/2072 and 900/900 (zero slack) after the day's 17 merges; the
remaining queue (cheaper-inference#6973, cheaper-inference#7662, cheaper-inference#7719, cheaper-inference#7744, cheaper-inference#7779 reworks) was collectively
blocked. Owner picked the wide margin in chat (2026-07-20).
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…lback preserved) (cheaper-inference#6973)

* feat(auth): optional OIDC for dashboard admin gate (password remains fallback)

- Settings: oidcEnabled + issuer/client/secret/scopes/redirect/allowedSubjects
- Public routes: /api/auth/oidc/ prefix (authorize + callback reachable)
- isAuthRequired: full OIDC config acts as auth method (gate requires login); partial does not (bootstrap preserved)
- New endpoints:
  - GET /api/auth/oidc/login — IdP redirect (absolute redirect_uri from request + discovery)
  - GET /api/auth/oidc/callback — code exchange, ID token validation (jose + JWKS), optional sub/email whitelist, mints identical 30d auth_token JWT + cookie as password login, redirects to /dashboard
- require-login endpoint now returns oidcEnabled
- Login UI: conditional OIDC button when enabled; password form untouched as fallback
- Tests:
  - public-api-routes: OIDC prefixes are public
  - api-auth: isAuthRequired true with full OIDC (no password); partial OIDC keeps bootstrap semantics

No new deps. No changes to proxy, keys, managementPassword, policies, MCP, CLI. Single-admin preserved.

* feat(auth): add integration test and fixes for OIDC dashboard login gate

- Add comprehensive integration test for /api/auth/oidc/callback
  (happy path + error paths: invalid_state, subject_not_allowed,
   not_configured, token_exchange, id_token_invalid, missing_code,
   server_misconfigured)
- Use static test seam (oidcCallbackInternals) for cookie store
- Mark setupComplete on first successful OIDC login (bootstrap parity)
- Ensure all redirects use absolute URLs (Next.js 16 compatibility)
- Verify identical auth_token JWT/cookie behavior as password path
- No new dependencies; reuses jose + fetch

Password login remains fully supported as fallback.

* fix(auth): address all Gemini Code Assist review comments for OIDC dashboard login gate

- Add module-level JWKS client cache (Record) + getJwksClient helper
- Wrap token exchange fetch + .json() in try/catch with 10s timeout
- Add 5s timeout to discovery fetch in both /login and /callback routes
- Case-insensitive email comparison in oidcAllowedSubjects whitelist
- Make oidc_state cookie 'secure' dynamic based on request protocol (matches auth_token)
- Expose clearJwksCache on test seam for isolation

All reviewer suggestions applied (adjusted for project rules on Map/Record).
Tests: 53/53 pass.

* fix(auth): wire OIDC config into updateSettingsSchema + SECURITY_IMPACTING_KEYS + encrypt/decrypt + non-empty subjects guard (address maintainer review)

* fix(auth): declare storedPasswordHash + align bootstrap contract for oidcEnabled

The security-impacting-keys re-auth gate in PATCH /api/settings assigned to
`storedPasswordHash` without ever declaring it (no `let`/`const`), so every
PATCH touching a SECURITY_IMPACTING_KEYS field (requireLogin, newPassword,
oidcEnabled, oidcClientSecret, the bypass toggles) threw a ReferenceError in
strict-mode ESM and fell through to the generic 500 handler. That masked the
expected 400/401/200 outcomes in settings-audit and settings-route-password
password-migration tests. Declare it as a block-scoped `const` where it's
first assigned.

Also updates the login-bootstrap-route contract tests: the public
/api/settings/require-login GET now legitimately includes `oidcEnabled` in
its response (the login page needs it to decide whether to render the OIDC
button) — the three closed-shape assertions are extended to expect
`oidcEnabled: false`, matching route.ts's existing behavior.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: mikolaj92 <mikolaj92@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…wlist + safe error redirects

Two findings from automated security review of the just-merged cheaper-inference#6973 OIDC login gate:
- HIGH: the allowlist matched the `email` claim without checking `email_verified`, so
  an attacker with an IdP account whose unverified email equals an allowlisted address
  could pass the gate. Now the email claim is only honored when email_verified === true.
- MEDIUM: error redirects built their target from the raw Host header (reflected
  open-redirect). Now resolved against the framework-parsed request URL.

Regression test added asserting an unverified allowlisted email is rejected.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…rence#9761)

* chore(changelog): v3.8.49 reconciliation — 200 missing bullets + 22 restored credits

Phase 0a of /generate-release. Measured commit<->CHANGELOG coverage over the real
cycle range (a48ae22..HEAD, 933 non-merge commits) instead of the last tag: 180
merged PRs had no bullet at all (they landed without a changelog.d fragment) and a
further 19 were invisible because the merge-train landed them under a generic
'Train 1D: merge via --admin' subject that carries no PR reference.

- +200 bullets, all with PR back-reference and author attribution (1179 -> 1379)
- 🙌 Contributors 156 -> 178; credits @terrafirmbot-source for cheaper-inference#7904, which shipped
  through the conflict-resolved cheaper-inference#8685 without any attribution
- closed-PR credit audit over the 32 human PRs closed unmerged this cycle: 12 had
  already landed under the author's own follow-up PR and were verified credited
- rollup bullet for the direct release-branch maintenance (merge-train landings,
  ratchet re-pins, base-red sweeps) that carries no PR of its own
- [3.8.49] header dated 2026-07-28 (was TBD) in the root file and the 42 i18n mirrors

Coverage after: 0 commits uncovered.

* chore(quality): v3.8.49 pre-flight — clear 4 base-reds, absorb cycle drift

Pre-flight sweep (Phase 0). Test suites ran on the dedicated 32-core box so the
self-inflicted load of `node --test` could not fabricate timing flakes.

Base-reds fixed (all real, all from merged cycle PRs that did not update their
characterization tests):

- providers-constants-split / quota-plan-registry / provider-translate-path GOLDEN:
  cheaper-inference#8861 added the Xiaomi MiMo Token Plan provider, so APIKEY_PROVIDERS is 195 (was
  194), knownProviders() is 12 (was 11) and the translate-path snapshot gains one
  purely additive entry. Counts aligned to the shipped catalog, never relaxed.
- agent-skills-content: skills/config-codex-cli/ was added by cheaper-inference#8709 with a custom
  block, so the custom-block set is 13, not 12.
- chatcore-compression-integration: cheaper-inference#8595/cheaper-inference#8560 deliberately decoupled REACTIVE
  context compaction from the `enabled` master switch, so a body above 70% of the
  window is pruned even with compression off. The test was sized above that
  threshold, which made it assert against intended behavior; it now stays below it
  and keeps testing the invariant it was written for (resolveBasePlan short-circuits
  to "off" before reading comboOverrides).

Static gates:

- 3 shellcheck directives were malformed (`# shellcheck disable=SC2086 — text`; the
  em-dash makes shellcheck reject the whole directive as SC1125) in ci.yml and
  nightly-release-green.yml — the comment now sits on its own line.
- gitleaks: 2 new generic-api-key false positives allowlisted with justification —
  a localStorage key for the sponsor banner (cheaper-inference#8723) and the PUBLIC Adobe Firefly
  web x-api-key, whose only literals are in JSDoc (the runtime reads it through
  resolvePublicCred, per Hard Rule cheaper-inference#11). secretFindings back to 0.
- zizmor 176 -> 189 and bundleSize 6762 -> 7666 rebaselined with the measurement and
  the reason; both are ordinary cycle drift absorbed at release.

Environment-dependent failures classified out, not silenced: the two tproxy tests
assert the native addon is unavailable/unprivileged and therefore fail when the
suite runs as root on the build box (they pass as a normal user), and the
consoleInterceptor rate-limit test is a 4s-timing flake under load (6/6 isolated).

* test(codex): align the Responses HTTP e2e to the cheaper-inference#8507 input-item contract

Fifth and last base-red of the v3.8.49 pre-flight. cheaper-inference#8507 (cheaper-inference#8083) deliberately sets
`status: "completed"` on Responses input items so strict upstream validators accept
them; codex-chat-reasoning-http-e2e still asserted the pre-cheaper-inference#8507 shape, so it failed
against intended behavior. Expectation updated with the reason inline — the assertion
is not relaxed, it now pins the current contract.

The test was never reached in the first pre-flight sweep (the run was interrupted
during the integration phase, and this file sorts after the one that failed).

* docs(release): v3.8.49 feature-documentation sync

Phase 1 step 6b. Swept the cycle's 284 New Features bullets against the existing
docs before writing anything: nearly every large theme (Kimi, xAI OAuth, session
affinity, bun:sqlite, Firecrawl, Opus 5, omniglyph, GCF v3.2, homologation suite)
was already covered. Six real gaps were left undocumented by the PRs that shipped
them, each verified in source before being written up:

- CredentialMaskerGuardrail (cheaper-inference#7683) is registered in guardrails/registry.ts but the
  GUARDRAILS table listed only 3 of the 4 guardrails
- the cacheAffinity scoring factor and the cache-optimized combo strategy (cheaper-inference#8008):
  the docs still said 12 factors / 18 strategies, the code has 13 / 19
- the optional dashboard OIDC login gate (cheaper-inference#6973) — /api/auth/oidc/{login,callback}
  had no mention in AUTHZ_GUIDE
- GET /api/usage/cache-health (cheaper-inference#8827) and GET /api/usage/model-latency-stats (cheaper-inference#6873)
  were missing from the API reference

README "What's New" gains one bullet (routing transparency) and merges two others
rather than growing a second changelog. PROVIDER_REFERENCE regenerated with the
generator (Firecrawl reclassified to Search, Xiaomi MiMo added by cheaper-inference#8861).

check:docs-all green: 134 docs, 813 internal links, no fabricated API/env/CLI
references. Known pre-existing drift left alone and reported: stale nominal counts
in ARCHITECTURE/CODEBASE_DOCUMENTATION (soft), the 9-factor mentions scattered in
AUTO-COMBO, and the auto-combo diagram SVG (the renderer needs a browser this
environment does not have — the .mmd source is updated and the .md says so).

* chore(release): v3.8.49 — clear the release-PR CI in one pass

Every finding from the first full ci.yml run on the release PR, fixed or justified
together so a single re-push clears the board.

Lint / check:route-validation:t06 — three routes read request.json() with no visible
Zod validation. The two proxy-subscriptions routes validated with a hand-rolled
parsePayload(); they now use real Zod schemas (src/lib/proxySubscription/schema.ts)
reproducing the same acceptance rules, error strings and status codes. chat/completions
is the proxy's hottest path and parses the body ONCE on purpose (cheaper-inference#4380 OOM crash-loop),
so it now safeParses the ALREADY-PARSED object against a deliberately permissive
structural schema — proven not to change behavior: absent model and model:null still
pass through, role "developer" still reaches 200, a ~300 KB payload is accepted, and
the body is still read exactly once. 25 new tests.

i18n UI value drift — 13 English strings rewritten during the cycle left stale
translations in up to 41 locales (317 pairs). Eleven are genuine rewrites and now carry
the pipeline's __MISSING__:<english> marker so the runtime serves corrected English until
translation catches up; vi forbids that marker by test, so it got a real translation.

PR Test Policy — 33 files flagged. Each was verified against the SOURCE, not the diff:
26 assert reductions are legitimate (mostly the cheaper-inference#7866 Qwen OAuth provider removal and the
cheaper-inference#8013 Antigravity refactor deleting the surface under test) and are allowlisted with the
PR and the evidence; 5 deleted files have verified replacements. One was NOT legitimate:
cheaper-inference#7528's GraphQL->WebSocket migration dropped four muse-spark continuation scenarios whose
logic is still live — connection isolation, cache eviction after a failed turn (the commit
itself says "was missing"), parallel-chat cache collision, and the empty-content guard.
All four are restored against the new transport and each was verified to fail when the
corresponding production mechanism is broken.

Quality Ratchet / openapiCoverage — 36.6% against a baseline of 38: the cycle added routes
faster than the spec. Eight real endpoints are now documented from their route.ts
(usage cache-health and model-latency-stats, the two OIDC endpoints, and the five
proxy-subscriptions paths), bringing it to 38.1%.

Quality Gates (Extended) / zizmor — the runner measures 190 where the devbox measures 189
on the same commit, a delta already recorded in this baseline's history. Baselined to the
runner's number.

Also: the driverFactory better-sqlite3 guard moved from a mid-body t.skip() to a declared
{ skip: <condition> } test option. Same behavior for the optional native dependency, but
the skip now shows up in the report and is distinguishable from a test.skip() that silences
a test outright. Verified under both runners: 15/15 on Node, 14/14 on Bun.

SonarCloud Code Analysis stays red and is not a blocker: sonar.qualitygate.wait=false since
cheaper-inference#7038 makes the job informative, the built-in gate cannot be swapped on the FREE plan, and
main has no branch protection.

* chore(quality): close the last two release-PR reds

test-masking — I had missed one of the 34 flagged files: my first pass grepped only
paths under tests/, so open-sse/services/__tests__/tierResolver.test.ts was invisible.
Same cheaper-inference#7866 cause as the other eight qwen-driven reductions: the "classifies Qwen as
free" case and qwen's entry in the batch list went with the removed provider, and the
batch indices dropped from 10 to 9 (61→59). Allowlisted with that evidence.

dast-smoke — all four Schemathesis findings are on the two OIDC endpoints documented
in the previous commit, and none is a defect. /api/auth/oidc/* is a BROWSER redirect
flow: it answers 302 to the IdP and 302 back to /login?oidc_error=... on every failure,
which Schemathesis reads as "accepted a schema-violating request", and it answers 400
when OIDC is not configured, which it reads as "rejected a schema-compliant request".
Keeping the endpoints in the spec is right — operators need them, and they are what
brought openapi coverage back over the baseline — so the flow is excluded from the fuzz
instead, with the reason inline in the workflow. The rest of /api/auth and /api/keys
stays in scope.

* test(db): reword the driverFactory skip comment so the gate stops counting it

The anti-test-masking gate greps text, not code: my explanation of WHY the
better-sqlite3 guard moved out of the test body spelled the runner API out
literally, and those two mentions inside a comment were counted as two new skip
markers — the exact signal the previous commit set out to clear. Same explanation,
phrased without the call syntax.

Verified with the gate's own exported helpers against the merge-base: 0 modified-file
violations, 0 deletion violations. Test still 15/15.

* fix(dashboard): unbreak the vitest:ui gate — 2 real production bugs + the i18n test seam

The Vitest job is a BLOCKING gate that had not run to completion once in this whole
release: rounds 1-3 cancelled it via cancel-in-progress on each successive fix push,
so its red was indistinguishable from green. Round 4 finally ran it and the suite was
broken cycle-wide.

Root cause of the suite: cheaper-inference#7935 instrumented ~180 shared/dashboard components with
next-intl's useTranslations/useLocale without updating the tests that mount them, so
every one of them threw "context from NextIntlClientProvider was not found". Fixed at
the shared seam (tests/_setup/vitestUiPolyfills.ts) rather than per file: a translator
built from the REAL en.json via next-intl's own createTranslator, memoized per
namespace — the naive version returns a fresh function each call and any component
whose useCallback/useEffect depends on t spins forever, which reads as a hang, not a
failure. A local mock still wins over the default. 22 files fixed by the seam alone,
15 realigned to the real strings; no assert removed or weakened.

Two production bugs the suite was hiding, both pre-existing and both with a failing
regression test already in the tree:

- RequestLoggerDetail crashed on a structured error object. cheaper-inference#7920 gave the component
  formatErrorForDisplay for exactly this case, then cheaper-inference#8213's combo-503 / cooldown
  checks went to the raw field and called .toLowerCase() on it. Both paths now use
  the helper.
- The logs detail modal reopened on first close again. cheaper-inference#6830 fixed that by reading the
  deep-link id ONCE; the cheaper-inference#8354 page rewrite regressed it by reading the live
  searchParams every render, so the prop flips mid-session and re-fires the child's
  deep-link effect exactly as the modal closes. Frozen at mount again.

Also tightens i18nUiCoverage 75.5 -> 99, which the ratchet demanded under
--require-tighten: the metric genuinely improved as the async translation workflow
paid off the debt that the v3.8.39/.44/.47 rebaselines had been recording. The
collector subtracts placeholders, so this release's 317 __MISSING__ markers are
already netted out of the 99.

Two UI files still fail locally under 20-worker concurrency (combos-page-smoke,
evals-tab-smoke) — cold-import flakes that pass isolated and with a larger timeout.

* test(e2e): repair the four shards the first green Build finally exercised

test-e2e has `needs: [build]`, and the release PR's Build died on every round
until now — so the 9-shard matrix produced ZERO signal for this whole cycle
while ~200 PRs merged. The first successful Build surfaced four independent
breakages, each traced to the commit that caused it:

- providers-management (cheaper-inference#7361): the single-connection delete moved from
  window.confirm() to a ConfirmModal, so page.once("dialog") never fired and
  the DELETE was never sent (deleteCalls stayed 0). Click the modal instead.
- providers-bailian-coding-plan (cheaper-inference#7882): the free-text Base URL field was
  deliberately replaced by a region step whose choice resolves the endpoint
  (global-sg -> coding-intl.dashscope, china-beijing -> coding.dashscope).
  Both cases rewritten against the region step; the invalid-URL case is
  unreachable from this modal now, so it covers the CN choice instead.
- group-b-activity-feed: the stack-trace guard ran against page.content(),
  which embeds the serialized i18n payload — zenmux's "endpoint at
  /api/v1/chat/completions" is prose, not a leak. Assert on rendered
  innerText and require the :line:col every real stack frame carries.
- navigation (cheaper-inference#8292): APP_ROUTE_PATTERN accepted only /login and /dashboard,
  but the new prefetch spec is the sole caller passing /home, so waitForURL
  never resolved and the retry loop burned the full 180s timeout.

E2E is green on main (9/9 on 07-22 and 07-23), so all four are cycle
regressions, not pre-existing debt. Tests only — no production code touched.

* fix(dashboard): stop the /home quick-start cards from prefetching too

cheaper-inference#8292 fixed half the RSC prefetch storm: it added prefetch={false} to the
sidebar's navigation and logo links, but /home — the landing route, and the
one its own e2e guard visits — renders five more internal Links in the
quick-start cards. First paint still fired 12 speculative RSC requests for
/dashboard/{analytics,logs,providers,api-manager} and /docs.

That PR shipped the test that would have caught this, but the test never got
to its assertion: gotoDashboardRoute("/home") hung because APP_ROUTE_PATTERN
accepted only /login and /dashboard, so the retry loop burned the whole 180s
timeout with no assertion error. With that helper repaired in the previous
commit, navigation.spec.ts finally ran and reported the 12 requests.

Validated both ways, per Hard Rule cheaper-inference#18:
- tests/unit/sidebar-prefetch-policy-8281.test.ts extended to /home — red on
  the parent commit (5 internal Links, 5 without prefetch={false}), green here.
- the e2e assertion expect(speculativeRequests).toEqual([]) is the end-to-end
  guard; it is what surfaced the defect in the first place.

* refactor(dashboard): shrink HomePageClient back under the size gate

The prefetch fix in the parent commit tripped check:file-size — the frozen
budget for this file is 1377 lines and a naive fix measured 1391, because
`href` + `prefetch={false}` + `className` no longer fits Prettier's 100-column
budget, so three one-line <Link> elements each expanded to five.

Followed the gate's own first suggestion (extract/DRY) before touching the
baseline: the quick-start links repeated the same className literal four
times, and the docs link carried a 180-char one inline. Hoisting both into
INLINE_LINK / DOCS_LINK collapses five wrapped <Link> blocks back to a single
line each and removes the duplication — 1391 -> 1381.

The remaining +4 over the frozen budget is the five prefetch attributes
themselves, which cannot be expressed in fewer lines. Rebaselined to 1381
with the rationale recorded in file-size-baseline.json under
_rebaseline_2026_07_29_8281_home_quickstart_prefetch.

tests/unit/sidebar-prefetch-policy-8281.test.ts still passes (2/2): it matches
whole <Link ...> blocks, so it is indifferent to the wrapping and only checks
that every internal link opts out of prefetch.

* fix(bun): use native fetch for direct outbound requests

* test(bun): cover native direct fetch path

* fix(bun): preload polyfill for next build workers

* fix(bun): expose AsyncLocalStorage globally

* fix(bun): filter non-page Fumadocs metadata

* fix(bun): defer docs-only route dependencies

* chore(skills): sync generated OmniRoute agent skill docs

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
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.

2 participants