Skip to content

feat(plugins): plugin UI-extensibility — admin pages, render routes, sidebar (split from #3668) - #3785

Closed
oyi77 wants to merge 3 commits into
cheaper-inference:release/v3.8.35from
oyi77:feat/plugin-ui-extensibility
Closed

oyi77 wants to merge 3 commits into
cheaper-inference:release/v3.8.35from
oyi77:feat/plugin-ui-extensibility

Conversation

@oyi77

@oyi77 oyi77 commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Description

Second slice of the original PR #3668 split into 3 independent PRs as requested.

Contains the WordPress-style plugin UI extensibility:

  • Plugin admin pages — dynamic route for rendered plugin pages
  • API route for plugin page rendering with sanitization (DOMPurify + XSS protection)
  • API endpoint for plugin UI extension listings
  • Sidebar integration — Plugins section that fetches and renders plugin admin pages
  • API key auth enforcement on render and ui-extensions routes
  • JSX syntax fix in Sidebar (unclosed .map() chain)
  • API route tests for plugin UI extensibility

Commits (3):

  1. Add plugin UI extensibility — admin pages, render routes, sidebar, dashboard widgets
  2. Harden plugin page rendering and API auth (DOMPurify sanitization, API key checks)
  3. Add API route tests for plugin UI extensibility

Related

Part of the split from original PR #3668

@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 introduces plugin UI extensibility, adding a dynamic plugin page component, API routes for rendering plugin pages and fetching UI extensions, and dynamic sidebar integration. The review feedback highlights a critical authentication mismatch where dashboard client-side fetches will fail with a 401 error if API key authentication is strictly required. Additionally, the reviewer recommends using an AbortController to prevent race conditions in the page component, wrapping request.json() in a try-catch block with Zod validation to prevent unhandled exceptions, refactoring duplicated authentication helpers, and correcting the unit tests to properly verify authentication behavior instead of just asserting function existence.

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 +20 to +29
async function checkAuth(request: Request): Promise<Response | null> {
const apiKeyRaw = extractApiKey(request);
if (isRequireApiKeyEnabled() && !apiKeyRaw) {
return errorResp(HTTP_STATUS.UNAUTHORIZED, "Authentication required");
}
if (apiKeyRaw && !(await isValidApiKey(apiKeyRaw))) {
return errorResp(HTTP_STATUS.UNAUTHORIZED, "Invalid API key");
}
return null;
}

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-high high

If isRequireApiKeyEnabled() is active, this route strictly requires an API key. However, the dashboard client-side fetches (such as the ones in Sidebar.tsx and the plugin page component) do not provide an API key in their headers. This will cause the plugin pages and sidebar integration to fail with a 401 Unauthorized error for authenticated dashboard users.

Consider allowing session-based authentication (e.g., validating the user's session cookie) as an alternative to API key authentication for these dashboard-facing routes, or retrieve the API key from the dashboard context and pass it in the fetch headers.

Comment on lines +53 to +70
useEffect(() => {
setLoading(true);
setError(null);
fetch(`/api/plugins/${name}/render?page=${slug}`)
.then((res) => {
if (!res.ok) throw new Error("Failed to load plugin page");
return res.json();
})
.then((data) => {
setContent(data.content);
})
.catch((err) => {
setError(err.message);
})
.finally(() => {
setLoading(false);
});
}, [name, slug]);

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

To prevent race conditions and memory leaks when the component unmounts or when the name or slug parameters change rapidly, use an AbortController to cancel any pending fetch requests.

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);
    setError(null);
    fetch(`/api/plugins/${name}/render?page=${slug}`, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error("Failed to load plugin page");
        return res.json();
      })
      .then((data) => {
        setContent(data.content);
      })
      .catch((err) => {
        if (err.name !== "AbortError") {
          setError(err.message);
        }
      })
      .finally(() => {
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      });
    return () => {
      controller.abort();
    };
  }, [name, slug]);

Comment on lines +76 to +77
const body = await request.json();
const { page, params: pageParams } = body;

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

Calling request.json() outside of a try-catch block will cause an unhandled exception and a 500 Internal Server Error if the request body is empty or contains invalid JSON. Additionally, the input should be validated using a Zod schema to comply with the repository style guide.

  let body;
  try {
    body = await request.json();
  } catch (err) {
    return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
  }
  const { page, params: pageParams } = body;
References
  1. Always validate inputs with Zod schemas from src/shared/validation/schemas.ts. (link)

Comment on lines +13 to +29
function errorResp(status: number, message: string): Response {
return new Response(JSON.stringify(buildErrorBody(status, sanitizeErrorMessage(message))), {
status,
headers: { "Content-Type": "application/json", ...CORS_HEADERS },
});
}

async function checkAuth(request: Request): Promise<Response | null> {
const apiKeyRaw = extractApiKey(request);
if (isRequireApiKeyEnabled() && !apiKeyRaw) {
return errorResp(HTTP_STATUS.UNAUTHORIZED, "Authentication required");
}
if (apiKeyRaw && !(await isValidApiKey(apiKeyRaw))) {
return errorResp(HTTP_STATUS.UNAUTHORIZED, "Invalid API key");
}
return null;
}

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

The checkAuth and errorResp helper functions are duplicated exactly between src/app/api/plugins/[name]/render/route.ts and src/app/api/plugins/ui-extensions/route.ts. Consider refactoring these helpers into a shared utility file to improve maintainability.

Comment on lines +57 to +65
it("render route GET calls checkAuth", async () => {
const mod = await import("../../src/app/api/plugins/[name]/render/route.ts");
assert.equal(typeof mod.GET, "function");
});

it("ui-extensions route GET calls checkAuth", async () => {
const mod = await import("../../src/app/api/plugins/ui-extensions/route.ts");
assert.equal(typeof mod.GET, "function");
});

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

These tests claim to verify that the render and ui-extensions routes call checkAuth, but they only assert that typeof mod.GET === "function". This is a duplicate assertion of the previous tests and does not actually verify any authentication behavior.

To properly test this, you should call the route handler with a mock Request (without an API key) and assert that it returns a 401 status code when isRequireApiKeyEnabled() is active.

@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.24 to release/v3.8.25 June 14, 2026 01:07
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.25 to release/v3.8.26 June 15, 2026 07:24
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.26 to release/v3.8.27 June 16, 2026 06:20
@oyi77
oyi77 force-pushed the feat/plugin-ui-extensibility branch from 074a818 to dc5ebfa Compare June 16, 2026 13:15
@oyi77

oyi77 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Force-pushed with clean commits — branch now contains only 3 plugin UI commits (was 33 commits with 30 unrelated changes).

@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.27 to release/v3.8.28 June 17, 2026 08:20
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.28 to release/v3.8.29 June 18, 2026 00:09
@oyi77
oyi77 force-pushed the feat/plugin-ui-extensibility branch from 51044a9 to 7422a53 Compare June 18, 2026 09:50
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.29 to release/v3.8.30 June 19, 2026 10:17
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thanks @oyi77 for the continued work on the plugin system (#3668 split — UI-extensibility).

This is conflicting against release/v3.8.30 and is a large surface (~190 files). Two things before it can move forward:

  1. Rebase onto the latest release/v3.8.30 and resolve conflicts.
  2. Landing the plugin epic is a maintainer product decision (which milestone it targets) — we'll confirm that separately.

Keeping it open and queued — thanks for the patience!

@oyi77

oyi77 commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Acknowledged — rebasing onto release/v3.8.30 is in progress. The plugin UI-extensibility is held pending the maintainer's product decision on milestone targeting. Keeping open as queued. 🙏

@oyi77
oyi77 force-pushed the feat/plugin-ui-extensibility branch from 7422a53 to 466bd40 Compare June 20, 2026 05:37
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.30 to release/v3.8.31 June 20, 2026 10:41
@oyi77
oyi77 force-pushed the feat/plugin-ui-extensibility branch from 466bd40 to 6e45e36 Compare June 20, 2026 12:39
@oyi77
oyi77 force-pushed the feat/plugin-ui-extensibility branch from 6e45e36 to 7fd345b Compare June 20, 2026 15:38
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.31 to release/v3.8.32 June 20, 2026 18:31
@oyi77
oyi77 force-pushed the feat/plugin-ui-extensibility branch from 7fd345b to f06597a Compare June 20, 2026 18:37
@oyi77
oyi77 force-pushed the feat/plugin-ui-extensibility branch from f06597a to 3d82447 Compare June 20, 2026 18:43
@oyi77
oyi77 force-pushed the feat/plugin-ui-extensibility branch from 3d82447 to 1ef2e7e Compare June 20, 2026 19:05
@oyi77
oyi77 force-pushed the feat/plugin-ui-extensibility branch from 1ef2e7e to a0ffe0a Compare June 20, 2026 23:05
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.32 to release/v3.8.33 June 21, 2026 14:03
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.33 to release/v3.8.34 June 22, 2026 07:40
diegosouzapw and others added 2 commits June 22, 2026 08:46
…C+D) (cheaper-inference#4622)

C — scripts/quality/validate-release-green.mjs (npm run check:release-green):
reproduces the release-equivalent validation (typecheck, eslint, db-rules,
public-creds, full unit, vitest, ratchets, optional --with-build package-artifact)
against the current working tree and classifies each red as HARD (real defect,
exit 1) vs DRIFT (ratchet — reported, never affects exit / never blocks). Pure
helpers exported + orchestration behind a direct-run guard; unit-tested.

D — .github/workflows/nightly-release-green.yml: runs C on the active release
branch nightly (and on workflow_dispatch) and opens/updates a single tracking
issue on HARD failures. Never a required check, never touches a contributor PR.

Closes the gap where the full gate (ci.yml) only ran on the release PR, so reds
accrued silently on release/** and surfaced in 40-min layers at release time.
Non-blocking by construction; drift is the maintainer's to rebaseline at release.

Co-authored-by: Diego Rodrigues de Sa e Souza <diego.souza@cdwasolutions.com.br>
…sidebar (split from cheaper-inference#3668)

- Plugin admin pages — dynamic route for rendered plugin pages
- API route for plugin page rendering with sanitization (DOMPurify + XSS)
- API endpoint for plugin UI extension listings
- Sidebar integration — Plugins section
- API key auth enforcement on render and ui-extensions routes
- Tests for plugin UI extensibility API routes
@oyi77
oyi77 force-pushed the feat/plugin-ui-extensibility branch from 9e85861 to 78f2fbc Compare June 22, 2026 18:46
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.34 to release/v3.8.35 June 23, 2026 06:51
@oyi77

oyi77 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Closing stale PR — conflicts with current release branch. Will reopen fresh if needed.

@oyi77 oyi77 closed this Jun 23, 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.

2 participants