mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 00:02:20 +03:00
This commit is contained in:
committed by
GitHub
parent
60d2f05d0e
commit
9ef4a61e9a
@@ -8,6 +8,7 @@
|
||||
- **fix(api):** `/v1/completions` now returns the legacy OpenAI Completions shape (`object:"text_completion"`, `choices[].text`) instead of chat payloads (`choices[].message|delta.content`) — the endpoint routes internally through the chat pipeline, so legacy Completion clients like TabbyML's `openai/completion` backend crashed with `missing field "text"`. The response (both non-streaming JSON and the SSE stream) is now translated back to the text-completion shape; `[DONE]` and error bodies pass through unchanged. ([#3571](https://github.com/diegosouzapw/OmniRoute/issues/3571))
|
||||
- **fix(usage):** the z.ai/GLM coding-plan quota card no longer shows "Monthly 0%" — coding plans have no monthly cap (only 5-hour windows), so the quota API reports the `TIME_LIMIT` ("Monthly") entry with `total=0`, and the `total>0 ? … : 0` fallback rendered a misleading 0% remaining (which can skew downstream model-choice). With no absolute cap the remaining percentage now falls back to the percentage-derived value (full/100% when 0% used). ([#3580](https://github.com/diegosouzapw/OmniRoute/issues/3580))
|
||||
- **docs(discovery):** mark `DISCOVERY_TOOL_DESIGN.md`'s API Endpoints table with an explicit "⚠️ Not yet implemented — Phase 2" banner — the discovery routes are a design proposal (Phase-1 stub only), and the banner makes clear the `KNOWN_STALE_DOC_REFS` gate suppression is intentional, not stale drift. ([#3498](https://github.com/diegosouzapw/OmniRoute/issues/3498))
|
||||
- **fix(agent-bridge):** add the missing `POST /api/tools/agent-bridge/upstream-ca/test` route — the UpstreamCaField "Test" button POSTed to it but it didn't exist (404). The new validate-only route checks the CA file exists and is a parseable PEM certificate (returns the subject/expiry) **without** persisting the path or activating it; it inherits the `/api/tools/agent-bridge/` LOCAL_ONLY classification. ([#3488](https://github.com/diegosouzapw/OmniRoute/issues/3488))
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ const KNOWN_MISSING = new Set([
|
||||
"/api/gamification/badges", // profile/page.tsx — idem
|
||||
"/api/gamification/badges/earned", // profile/page.tsx — idem
|
||||
"/api/settings/obsidian/webdav", // ObsidianSourceCard.tsx — só existe /api/settings/obsidian
|
||||
"/api/tools/agent-bridge/upstream-ca/test", // UpstreamCaField.tsx — rota inexistente
|
||||
]);
|
||||
|
||||
function walk(dir, acc = []) {
|
||||
|
||||
63
src/app/api/tools/agent-bridge/upstream-ca/test/route.ts
Normal file
63
src/app/api/tools/agent-bridge/upstream-ca/test/route.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* POST /api/tools/agent-bridge/upstream-ca/test
|
||||
*
|
||||
* Validate-only (dry-run) counterpart of POST /api/tools/agent-bridge/upstream-ca:
|
||||
* checks the upstream CA file exists and is a parseable PEM certificate, WITHOUT
|
||||
* persisting the path or activating it via configureUpstreamCa(). Backs the
|
||||
* UpstreamCaField "Test" button, which previously 404'd (#3488).
|
||||
*
|
||||
* LOCAL_ONLY: covered by the "/api/tools/agent-bridge/" prefix in routeGuard.ts.
|
||||
*/
|
||||
import crypto from "crypto";
|
||||
import fs from "fs";
|
||||
|
||||
import { AgentBridgeUpstreamCaPostSchema } from "@/shared/schemas/agentBridge";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
|
||||
export async function POST(request: Request): Promise<Response> {
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return createErrorResponse({ status: 400, message: "Invalid JSON body" });
|
||||
}
|
||||
|
||||
const parsed = AgentBridgeUpstreamCaPostSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return createErrorResponse({
|
||||
status: 400,
|
||||
message: "Invalid request body",
|
||||
details: parsed.error.flatten(),
|
||||
});
|
||||
}
|
||||
|
||||
const { path: caPath } = parsed.data;
|
||||
|
||||
if (!fs.existsSync(caPath)) {
|
||||
return createErrorResponse({ status: 400, message: `Upstream CA file not found: ${caPath}` });
|
||||
}
|
||||
|
||||
let pem: string;
|
||||
try {
|
||||
pem = fs.readFileSync(caPath, "utf8");
|
||||
} catch (err) {
|
||||
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
|
||||
return createErrorResponse({ status: 400, message: `Unable to read upstream CA file: ${msg}` });
|
||||
}
|
||||
|
||||
if (!pem.includes("-----BEGIN CERTIFICATE-----")) {
|
||||
return createErrorResponse({
|
||||
status: 400,
|
||||
message: "File is not a PEM certificate (missing a -----BEGIN CERTIFICATE----- block).",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const cert = new crypto.X509Certificate(pem);
|
||||
return Response.json({ ok: true, path: caPath, subject: cert.subject, validTo: cert.validTo });
|
||||
} catch (err) {
|
||||
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
|
||||
return createErrorResponse({ status: 400, message: `Invalid certificate: ${msg}` });
|
||||
}
|
||||
}
|
||||
94
tests/unit/upstream-ca-test-route-3488.test.ts
Normal file
94
tests/unit/upstream-ca-test-route-3488.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { POST } from "../../src/app/api/tools/agent-bridge/upstream-ca/test/route.ts";
|
||||
|
||||
// #3488 — UpstreamCaField's "Test" button POSTed to /api/tools/agent-bridge/upstream-ca/test,
|
||||
// which did not exist (404). The new validate-only route checks the CA file exists and is a
|
||||
// parseable PEM certificate WITHOUT persisting/activating it.
|
||||
|
||||
// A throwaway self-signed cert (CN=OmniRoute Test CA), valid to 2036.
|
||||
const TEST_CA_PEM = `-----BEGIN CERTIFICATE-----
|
||||
MIIDGTCCAgGgAwIBAgIUISgNKO/v/z0FdUIPoCD4dwgKbacwDQYJKoZIhvcNAQEL
|
||||
BQAwHDEaMBgGA1UEAwwRT21uaVJvdXRlIFRlc3QgQ0EwHhcNMjYwNjEwMjExNDMx
|
||||
WhcNMzYwNjA3MjExNDMxWjAcMRowGAYDVQQDDBFPbW5pUm91dGUgVGVzdCBDQTCC
|
||||
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALkubKCA7sgOph0nsKhQZNoH
|
||||
UaQo+mrodWJ+23yVnxPygBQQay6okO1w5U6yxweinyCC0jB87Y386q30cqYK6NCf
|
||||
HbAgkNRelhxeoU71DztIjIaKZCTlra5CCjVxVzvIOu4PoP8UgoLy/jOLM15XiM5B
|
||||
entgjw62qXGRGak2Thiac+dHRzKJAIPxRDnWDrgQFsduNtSb1sGbivjUjLLEabm0
|
||||
gokYlJpNCYJvS31qvL37aeV8igjt8hsReVEb5qm5RiiAepM9B3gvKvn0fsKvxT2a
|
||||
rmqgySF+o2aTi+PW3+ZySoWoUL6b7GSA/CpF6Mc3u2qM/DvU4Kr0K8Y5EE+PsYUC
|
||||
AwEAAaNTMFEwHQYDVR0OBBYEFD/qt9vsjOHNvlfT6Z4j9myR4GJJMB8GA1UdIwQY
|
||||
MBaAFD/qt9vsjOHNvlfT6Z4j9myR4GJJMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
|
||||
hvcNAQELBQADggEBAH0WP40mF66cqUxQjamHS2BScRkn5E+SvwoZD12ZvQO/kgj/
|
||||
wbWu5mZOf5cpuqHrOmfOC+6itIkZb7v6d0CRM19xcoAg1mVWFFSk0iroFCw0qltN
|
||||
kB5WPvKHI6JRkr7HdUTD1qW9ljveMPfZ/Fm9ZM6QhCOLifzNTP2GMTVyIMAvig6B
|
||||
TgmL/sn4dw4C2UTMQioMMXHSeJ90OD4Pv3mqX16JKrRSICoTExBoEIF23kWWJL6m
|
||||
RL5Jiv1pdbFujHL8l9KPI2xmsWtkKutxOL2O5zpdUxP4noNVInDqEmbriK6CKY4y
|
||||
hWHoQhtd4zf9H6+NIi38SPTCAmCjgU7iVq6mWoE=
|
||||
-----END CERTIFICATE-----
|
||||
`;
|
||||
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ca-test-"));
|
||||
const validCaPath = path.join(dir, "valid-ca.pem");
|
||||
const nonPemPath = path.join(dir, "not-a-cert.txt");
|
||||
fs.writeFileSync(validCaPath, TEST_CA_PEM);
|
||||
fs.writeFileSync(nonPemPath, "this is not a certificate");
|
||||
|
||||
test.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
|
||||
function postJson(body: unknown): Request {
|
||||
return new Request("http://localhost/api/tools/agent-bridge/upstream-ca/test", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
test("#3488 valid PEM cert → 200 ok with subject", async () => {
|
||||
const res = await POST(postJson({ path: validCaPath }));
|
||||
assert.equal(res.status, 200);
|
||||
const json = await res.json();
|
||||
assert.equal(json.ok, true);
|
||||
assert.match(json.subject, /OmniRoute Test CA/);
|
||||
});
|
||||
|
||||
test("#3488 does NOT persist the CA path (validate-only)", async () => {
|
||||
await POST(postJson({ path: validCaPath }));
|
||||
// The persisted path file used by the real POST route must not be created by /test.
|
||||
// We can't import the constant without side effects, so assert the dry-run returns a
|
||||
// shape with no activation marker and leaves no global state — the absence of an
|
||||
// `activated`/`persisted` field is the contract.
|
||||
const res = await POST(postJson({ path: validCaPath }));
|
||||
const json = await res.json();
|
||||
assert.equal(json.persisted, undefined);
|
||||
assert.equal(json.activated, undefined);
|
||||
});
|
||||
|
||||
test("#3488 non-existent path → 400", async () => {
|
||||
const res = await POST(postJson({ path: path.join(dir, "nope.pem") }));
|
||||
assert.equal(res.status, 400);
|
||||
});
|
||||
|
||||
test("#3488 file that is not a PEM cert → 400", async () => {
|
||||
const res = await POST(postJson({ path: nonPemPath }));
|
||||
assert.equal(res.status, 400);
|
||||
});
|
||||
|
||||
test("#3488 invalid body (missing path) → 400", async () => {
|
||||
const res = await POST(postJson({}));
|
||||
assert.equal(res.status, 400);
|
||||
});
|
||||
|
||||
test("#3488 invalid JSON body → 400", async () => {
|
||||
const req = new Request("http://localhost/api/tools/agent-bridge/upstream-ca/test", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{not json",
|
||||
});
|
||||
const res = await POST(req);
|
||||
assert.equal(res.status, 400);
|
||||
});
|
||||
Reference in New Issue
Block a user