Files
OmniRoute/tests/unit/authz/classify.test.ts
Jan Leon 6103fd7239 fix: Stabilize live dashboard WebSocket routing (#6335)
* fix(dashboard): allow anonymous WS handshake + public /api/health/ping

The live-dashboard WebSocket descriptor handshake (GET /api/v1/ws?handshake=1)
and the lightweight GET /api/health/ping liveness probe both 401'd for
unauthenticated callers, even though both are metadata-only reads intended
to be public. clientApiPolicy required a bearer/dashboard-session before the
WS route handler could even return its own wsAuth/protocol descriptor, and
/api/health/ping was never added to PUBLIC_READONLY_API_ROUTE_PREFIXES
despite its own docstring documenting it as "No auth required".

clientApiPolicy.evaluate() now allows an anonymous
{kind:"anonymous", id:"ws-handshake"} subject for GET/HEAD/OPTIONS on
/api/v1/ws?handshake=1 — the route handler still performs its own real
wsAuth/dashboard/API-key decision before opening the socket — and
/api/health/ping is now in PUBLIC_READONLY_API_ROUTE_PREFIXES.

Re-scoped from the original PR per review-group-prs analysis: the
overlapping hardcoded /live-ws path-derivation change (useLiveDashboard.ts,
ws/route.ts) is dropped here since it conflicts with #6072's different
(dynamic, env-derived) approach to the same problem; only the
non-overlapping auth-policy win ships in this PR.

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

* chore(changelog): resync CHANGELOG.md after merging release/v3.8.47

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

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: JxnLexn <JxnLexn@users.noreply.github.com>
2026-07-09 23:19:32 -03:00

263 lines
7.8 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import * as classifyPublicApi from "../../../src/server/authz/classify.ts";
import { classifyRoute } from "../../../src/server/authz/classify.ts";
import type { RouteClass } from "../../../src/server/authz/types.ts";
interface Case {
name: string;
path: string;
method?: string;
expectedClass: RouteClass;
expectedNormalized?: string;
}
const cases: Case[] = [
{ name: "root /", path: "/", expectedClass: "MANAGEMENT", expectedNormalized: "/" },
{ name: "dashboard root", path: "/dashboard", expectedClass: "MANAGEMENT" },
{ name: "dashboard nested", path: "/dashboard/settings", expectedClass: "MANAGEMENT" },
{ name: "dashboard onboarding", path: "/dashboard/onboarding", expectedClass: "PUBLIC" },
{
name: "/api/v1 base",
path: "/api/v1",
expectedClass: "CLIENT_API",
expectedNormalized: "/api/v1",
},
{
name: "/api/v1/chat/completions",
path: "/api/v1/chat/completions",
expectedClass: "CLIENT_API",
},
{ name: "/api/v1/responses", path: "/api/v1/responses", expectedClass: "CLIENT_API" },
{ name: "/api/v1/models", path: "/api/v1/models", expectedClass: "CLIENT_API" },
{ name: "/api/v1/embeddings", path: "/api/v1/embeddings", expectedClass: "CLIENT_API" },
{ name: "/api/v1/files", path: "/api/v1/files", expectedClass: "CLIENT_API" },
{ name: "/api/v1/batches", path: "/api/v1/batches", expectedClass: "CLIENT_API" },
{ name: "/api/v1/ws", path: "/api/v1/ws", expectedClass: "CLIENT_API" },
{ name: "/api/mcp/* stays management", path: "/api/mcp/status", expectedClass: "MANAGEMENT" },
{
name: "/v1 alias",
path: "/v1",
expectedClass: "CLIENT_API",
expectedNormalized: "/api/v1",
},
{
name: "/v1/chat/completions alias",
path: "/v1/chat/completions",
expectedClass: "CLIENT_API",
expectedNormalized: "/api/v1/chat/completions",
},
{
name: "/v1beta alias",
path: "/v1beta",
expectedClass: "CLIENT_API",
expectedNormalized: "/api/v1beta",
},
{
name: "/v1beta generateContent alias",
path: "/v1beta/models/gemini-pro:generateContent",
expectedClass: "CLIENT_API",
expectedNormalized: "/api/v1beta/models/gemini-pro:generateContent",
},
{
name: "/api/v1beta generateContent",
path: "/api/v1beta/models/gemini-pro:generateContent",
expectedClass: "CLIENT_API",
},
{
name: "/v1/v1 double-prefix",
path: "/v1/v1",
expectedClass: "CLIENT_API",
expectedNormalized: "/api/v1",
},
{
name: "/v1/v1/embeddings double-prefix",
path: "/v1/v1/embeddings",
expectedClass: "CLIENT_API",
expectedNormalized: "/api/v1/embeddings",
},
{
name: "/chat/completions alias",
path: "/chat/completions",
expectedClass: "CLIENT_API",
expectedNormalized: "/api/v1/chat/completions",
},
{
name: "/responses alias",
path: "/responses",
expectedClass: "CLIENT_API",
expectedNormalized: "/api/v1/responses",
},
{
name: "/responses/abc alias",
path: "/responses/abc",
expectedClass: "CLIENT_API",
expectedNormalized: "/api/v1/responses/abc",
},
{
name: "/codex/* alias collapses to /api/v1/responses",
path: "/codex/anything",
expectedClass: "CLIENT_API",
expectedNormalized: "/api/v1/responses",
},
{
name: "/models alias",
path: "/models",
expectedClass: "CLIENT_API",
expectedNormalized: "/api/v1/models",
},
{
name: "/api/auth/login is PUBLIC",
path: "/api/auth/login",
method: "POST",
expectedClass: "PUBLIC",
},
{
name: "/api/auth/logout is PUBLIC",
path: "/api/auth/logout",
method: "POST",
expectedClass: "PUBLIC",
},
{
name: "/api/auth/status is PUBLIC",
path: "/api/auth/status",
method: "GET",
expectedClass: "PUBLIC",
},
{ name: "/api/init is PUBLIC", path: "/api/init", method: "POST", expectedClass: "PUBLIC" },
{
name: "/api/monitoring/health is PUBLIC",
path: "/api/monitoring/health",
method: "GET",
expectedClass: "PUBLIC",
},
{
name: "/api/health/ping is PUBLIC",
path: "/api/health/ping",
method: "GET",
expectedClass: "PUBLIC",
},
{
name: "/api/cloud/auth POST is PUBLIC",
path: "/api/cloud/auth",
method: "POST",
expectedClass: "PUBLIC",
},
{
name: "/api/cloud/model/resolve POST is PUBLIC",
path: "/api/cloud/model/resolve",
method: "POST",
expectedClass: "PUBLIC",
},
{
name: "/api/cloud/models/alias GET is PUBLIC",
path: "/api/cloud/models/alias",
method: "GET",
expectedClass: "PUBLIC",
},
{
name: "/api/cloud/credentials/update PUT is MANAGEMENT",
path: "/api/cloud/credentials/update",
method: "PUT",
expectedClass: "MANAGEMENT",
},
{
name: "/api/cloud/models/alias PUT is MANAGEMENT",
path: "/api/cloud/models/alias",
method: "PUT",
expectedClass: "MANAGEMENT",
},
{
name: "/api/cloud/unknown GET is MANAGEMENT",
path: "/api/cloud/unknown",
method: "GET",
expectedClass: "MANAGEMENT",
},
{
name: "/api/oauth/* is PUBLIC",
path: "/api/oauth/callback",
method: "GET",
expectedClass: "PUBLIC",
},
{
name: "/api/sync/bundle is PUBLIC",
path: "/api/sync/bundle",
method: "POST",
expectedClass: "PUBLIC",
},
{
name: "/api/settings/require-login GET is PUBLIC readonly",
path: "/api/settings/require-login",
method: "GET",
expectedClass: "PUBLIC",
},
{
name: "/api/settings/require-login POST is MANAGEMENT",
path: "/api/settings/require-login",
method: "POST",
expectedClass: "MANAGEMENT",
},
{
name: "/api/providers/* MUST stay MANAGEMENT",
path: "/api/providers/openai",
expectedClass: "MANAGEMENT",
},
{ name: "/api/keys MANAGEMENT", path: "/api/keys", expectedClass: "MANAGEMENT" },
{ name: "/api/db/health MANAGEMENT", path: "/api/db/health", expectedClass: "MANAGEMENT" },
{ name: "/api/settings MANAGEMENT", path: "/api/settings", expectedClass: "MANAGEMENT" },
{ name: "/api/audit MANAGEMENT", path: "/api/audit", expectedClass: "MANAGEMENT" },
{
name: "/api/usage/om-usage is PUBLIC (handler enforces its own API key auth)",
path: "/api/usage/om-usage",
method: "GET",
expectedClass: "PUBLIC",
},
{
name: "Unknown top-level path defaults MANAGEMENT (fail-closed)",
path: "/totally-unknown",
expectedClass: "MANAGEMENT",
},
];
for (const c of cases) {
test(`classifyRoute: ${c.name}`, () => {
const r = classifyRoute(c.path, c.method ?? "GET");
assert.equal(r.routeClass, c.expectedClass, `routeClass for ${c.path}`);
if (c.expectedNormalized) {
assert.equal(r.normalizedPath, c.expectedNormalized, `normalizedPath for ${c.path}`);
}
});
}
test("classifyRoute returns deterministic result for trailing slash", () => {
const a = classifyRoute("/api/v1/chat/completions", "POST");
const b = classifyRoute("/api/v1/chat/completions/", "POST");
assert.equal(a.routeClass, b.routeClass);
assert.equal(a.normalizedPath, b.normalizedPath);
});
test("classifyRoute strips trailing slash on root only when not '/'", () => {
assert.equal(classifyRoute("/").normalizedPath, "/");
});
test("classifyRoute treats /api/v1 prefix exactly", () => {
assert.equal(classifyRoute("/api/v1abc").routeClass, "MANAGEMENT");
assert.equal(classifyRoute("/api/v1/x").routeClass, "CLIENT_API");
assert.equal(classifyRoute("/api/v1betamax").routeClass, "MANAGEMENT");
assert.equal(classifyRoute("/api/v1beta/models").routeClass, "CLIENT_API");
});
test("classify module public surface only exposes route classification", () => {
assert.equal("classifyRoute" in classifyPublicApi, true);
assert.equal("isClientApi" in classifyPublicApi, false);
assert.equal("isManagement" in classifyPublicApi, false);
assert.equal("isPublic" in classifyPublicApi, false);
});