mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-15 19:32:20 +03:00
Landed with the design call resolved per the owner's pick — **option 1**: the synced store is now endpoint-agnostic (persistDiscoveredModels and managedModelImport no longer drop non-chat models at write time), and chat selectability moved to read time (auto-pool expansion in autoStrategy applies filterChatSelectableModels; the models-route projection already had its chatOnly filter). Your discovery test now passes end-to-end (3/3): /api/show capabilities persist per connection and image/embedding requests route through the advertising host. Reconciliation notes: conflicted areas merged onto the current tip (adobe discovery import, requestedModel preflight signature, resolvedProvider fast-path coexists with the synced-route override — explicit resolution wins); carried base-red drains (#10055 memoization, #11071 test variants) dropped as already-landed; the managed-model-import exclusion test was propagated to the new contract (image/video models persist; the read filter still hides them from chat pickers — pinned by a new assertion). Full battery: 205/206 focused (the one red is a confirmed periodic-timer timing flake on the loaded devbox — 20/20 isolated), autoCombo vitest 30/30, combo suites 46/46, gates + typecheck clean. Thank you @yourspraveen — the capability probe + routing design was right; it just needed the store contract opened up. Fixes #11087.
123 lines
4.1 KiB
TypeScript
123 lines
4.1 KiB
TypeScript
// @vitest-environment jsdom
|
|
import React from "react";
|
|
import { act } from "react";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
// Mock next-intl translations (the page imports useTranslations("auth")).
|
|
vi.mock("next-intl", () => ({
|
|
useTranslations: () => (key: string) => key,
|
|
}));
|
|
|
|
import CallbackPage from "@/app/callback/page";
|
|
|
|
/**
|
|
* Regression guard for ported upstream PR decolua/9router#998 (security):
|
|
* the OAuth callback page must never relay {code, state} to a wildcard
|
|
* postMessage target ("*"), as a hostile opener can read the code/state and
|
|
* complete the OAuth flow as the user. Trusted targets are the same-origin
|
|
* parent, the loopback hostname variants of the same port (localhost vs
|
|
* 127.0.0.1 — Zed native-app redirects may land on the other spelling than the
|
|
* dashboard the modal was opened from; same port means the same OmniRoute
|
|
* server), and Codex's fixed loopback helper (127.0.0.1:1455).
|
|
*/
|
|
describe("OAuth callback page — postMessage target origin scope (#998)", () => {
|
|
let container: HTMLDivElement;
|
|
let root: Root;
|
|
let postMessageSpy: ReturnType<typeof vi.fn>;
|
|
let originalOpener: typeof window.opener;
|
|
|
|
beforeEach(() => {
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
postMessageSpy = vi.fn();
|
|
originalOpener = window.opener;
|
|
|
|
// Set the callback URL with OAuth params (triggers the postMessage send).
|
|
window.history.replaceState({}, "", "/callback?code=test_code_abc123&state=test_state_xyz789");
|
|
|
|
// Stub window.opener as a CROSS-ORIGIN opener: same-origin probe must throw
|
|
// (mimics a real cross-origin window.opener), which means the page falls into
|
|
// the fallback path that previously used a wildcard "*" target origin.
|
|
Object.defineProperty(window, "opener", {
|
|
configurable: true,
|
|
writable: true,
|
|
value: {
|
|
postMessage: postMessageSpy,
|
|
get location(): never {
|
|
throw new Error("cross-origin access blocked");
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
act(() => root.unmount());
|
|
container.remove();
|
|
Object.defineProperty(window, "opener", {
|
|
configurable: true,
|
|
writable: true,
|
|
value: originalOpener,
|
|
});
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("never targets the wildcard '*' origin even when opener is cross-origin", async () => {
|
|
await act(async () => {
|
|
root.render(<CallbackPage />);
|
|
});
|
|
// Give useEffect a microtask to flush.
|
|
await act(async () => {
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const targetOrigins = postMessageSpy.mock.calls.map((call) => call[1]);
|
|
expect(targetOrigins).not.toContain("*");
|
|
});
|
|
|
|
it("only targets trusted origins (same-origin + Codex 127.0.0.1:1455)", async () => {
|
|
await act(async () => {
|
|
root.render(<CallbackPage />);
|
|
});
|
|
await act(async () => {
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const loopbackSamePort = window.location.port
|
|
? [`http://localhost:${window.location.port}`, `http://127.0.0.1:${window.location.port}`]
|
|
: [];
|
|
const trusted = new Set([
|
|
window.location.origin,
|
|
...loopbackSamePort,
|
|
"http://localhost:1455",
|
|
"http://127.0.0.1:1455",
|
|
]);
|
|
const targetOrigins = postMessageSpy.mock.calls.map((call) => call[1]);
|
|
expect(targetOrigins.length).toBeGreaterThan(0);
|
|
for (const origin of targetOrigins) {
|
|
expect(trusted.has(origin)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("delivers the OAuth code/state payload at least once via postMessage", async () => {
|
|
await act(async () => {
|
|
root.render(<CallbackPage />);
|
|
});
|
|
await act(async () => {
|
|
await Promise.resolve();
|
|
});
|
|
|
|
// Sanity: the scoped postMessage path still actually attempts delivery.
|
|
expect(postMessageSpy).toHaveBeenCalled();
|
|
const firstCall = postMessageSpy.mock.calls[0];
|
|
expect(firstCall[0]).toMatchObject({
|
|
type: "oauth_callback",
|
|
data: expect.objectContaining({
|
|
code: "test_code_abc123",
|
|
state: "test_state_xyz789",
|
|
}),
|
|
});
|
|
});
|
|
});
|