mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-24 16:12:23 +03:00
Compare commits
2 Commits
fix/v3850-
...
fix/v3850-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c80526f904 | ||
|
|
c5dda9953f |
@@ -0,0 +1 @@
|
||||
- **docs(openapi):** document the conditionally management-authenticated, same-origin `POST /api/openapi/try` proxy contract and restore the release branch's operation-coverage ratchet ([#11363](https://github.com/diegosouzapw/OmniRoute/pull/11363))
|
||||
@@ -6931,6 +6931,104 @@ paths:
|
||||
"500":
|
||||
description: Failed to parse OpenAPI spec
|
||||
|
||||
/api/openapi/try:
|
||||
post:
|
||||
tags: [System]
|
||||
summary: Proxy an API Explorer request to an OmniRoute endpoint
|
||||
description: >-
|
||||
Executes an API Explorer request through a server-side, same-origin proxy. The target
|
||||
must start with `/api/`, `/v1/`, `/v1beta/`, `/a2a`, or
|
||||
`/.well-known/agent.json`; protocol-relative and cross-origin targets are rejected.
|
||||
Hop-by-hop, proxy, host, cookie, and forwarding headers supplied in `headers` are
|
||||
stripped, while any dashboard cookie on the original request is forwarded separately.
|
||||
When `requireLogin` is disabled, the management-auth bypass mirrors the runtime setting;
|
||||
otherwise a management Bearer credential or dashboard session is required. Failures
|
||||
caught after authentication, including request JSON parsing, fetch, and response-body
|
||||
parsing failures, are returned in the normal HTTP 200 result envelope so the Explorer
|
||||
can display them; `status: 0` identifies that caught-failure path.
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- ManagementSessionAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [path]
|
||||
properties:
|
||||
method:
|
||||
type: string
|
||||
enum: [GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS]
|
||||
default: GET
|
||||
path:
|
||||
type: string
|
||||
minLength: 1
|
||||
pattern: "^/(?:api/|v1/|v1beta/|a2a|\\.well-known/agent\\.json)"
|
||||
description: Same-origin OmniRoute API path, optionally including a query string.
|
||||
headers:
|
||||
type: object
|
||||
default: {}
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: >-
|
||||
Headers to forward after removing connection, content-length, cookie, host,
|
||||
keep-alive, proxy-authenticate, proxy-authorization, te, trailer,
|
||||
transfer-encoding, upgrade, x-forwarded-for, x-forwarded-host, and
|
||||
x-forwarded-proto headers.
|
||||
body:
|
||||
description: >-
|
||||
Optional JSON value. A truthy value is serialized unless it is already a
|
||||
string, and is not forwarded when `method` is `GET`.
|
||||
responses:
|
||||
"200":
|
||||
description: Upstream response or displayable caught-failure envelope
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [status, statusText, headers, body, latencyMs, contentType]
|
||||
properties:
|
||||
status:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: Upstream HTTP status, or 0 when request processing throws.
|
||||
statusText:
|
||||
type: string
|
||||
headers:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
body:
|
||||
description: >-
|
||||
Parsed JSON, response text truncated after 10,000 characters, or a sanitized
|
||||
caught-error object.
|
||||
latencyMs:
|
||||
type: integer
|
||||
minimum: 0
|
||||
contentType:
|
||||
type: string
|
||||
"400":
|
||||
description: Invalid request body or non-same-origin path
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/ValidationErrorResponse"
|
||||
- type: object
|
||||
required: [error]
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
example: Path must be same-origin
|
||||
"401":
|
||||
$ref: "#/components/responses/ManagementAuthenticationRequired"
|
||||
"403":
|
||||
$ref: "#/components/responses/ManagementInvalidToken"
|
||||
"503":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
|
||||
# ─── Agent Skills Catalog ────────────────────────────────────────────────────
|
||||
|
||||
/api/agent-skills:
|
||||
|
||||
@@ -49,6 +49,76 @@ test("GET /api/openapi/spec documents its conditional management auth contract",
|
||||
);
|
||||
});
|
||||
|
||||
test("POST /api/openapi/try documents its bounded management proxy contract", () => {
|
||||
const operation = paths["/api/openapi/try"]?.post;
|
||||
|
||||
assert.ok(operation, "POST /api/openapi/try must be present in docs/openapi.yaml");
|
||||
assert.deepEqual(operation.security, [{ BearerAuth: [] }, { ManagementSessionAuth: [] }]);
|
||||
assert.match(operation.description ?? "", /same-origin/);
|
||||
assert.match(operation.description ?? "", /When `requireLogin` is disabled/);
|
||||
|
||||
const requestBody = operation.requestBody;
|
||||
const requestSchema = requestBody?.content?.["application/json"]?.schema;
|
||||
assert.equal(requestBody?.required, true);
|
||||
assert.equal(requestSchema?.type, "object");
|
||||
assert.deepEqual(requestSchema?.required, ["path"]);
|
||||
assert.deepEqual(requestSchema?.properties?.method?.enum, [
|
||||
"GET",
|
||||
"POST",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"DELETE",
|
||||
"HEAD",
|
||||
"OPTIONS",
|
||||
]);
|
||||
assert.equal(requestSchema?.properties?.method?.default, "GET");
|
||||
assert.equal(requestSchema?.properties?.path?.minLength, 1);
|
||||
assert.equal(
|
||||
requestSchema?.properties?.path?.pattern,
|
||||
"^/(?:api/|v1/|v1beta/|a2a|\\.well-known/agent\\.json)"
|
||||
);
|
||||
assert.equal(requestSchema?.properties?.headers?.type, "object");
|
||||
assert.deepEqual(requestSchema?.properties?.headers?.additionalProperties, {
|
||||
type: "string",
|
||||
});
|
||||
assert.deepEqual(requestSchema?.properties?.headers?.default, {});
|
||||
assert.ok("body" in requestSchema.properties);
|
||||
|
||||
const successSchema = operation.responses?.["200"]?.content?.["application/json"]?.schema;
|
||||
assert.equal(successSchema?.type, "object");
|
||||
assert.equal(successSchema?.additionalProperties, false);
|
||||
assert.deepEqual(successSchema?.required, [
|
||||
"status",
|
||||
"statusText",
|
||||
"headers",
|
||||
"body",
|
||||
"latencyMs",
|
||||
"contentType",
|
||||
]);
|
||||
assert.equal(successSchema?.properties?.status?.type, "integer");
|
||||
assert.equal(successSchema?.properties?.status?.minimum, 0);
|
||||
assert.equal(successSchema?.properties?.statusText?.type, "string");
|
||||
assert.equal(successSchema?.properties?.headers?.type, "object");
|
||||
assert.deepEqual(successSchema?.properties?.headers?.additionalProperties, {
|
||||
type: "string",
|
||||
});
|
||||
assert.match(successSchema?.properties?.body?.description ?? "", /10,000 characters/);
|
||||
assert.equal(successSchema?.properties?.latencyMs?.type, "integer");
|
||||
assert.equal(successSchema?.properties?.latencyMs?.minimum, 0);
|
||||
assert.equal(successSchema?.properties?.contentType?.type, "string");
|
||||
|
||||
const badRequestSchema = operation.responses?.["400"]?.content?.["application/json"]?.schema;
|
||||
assert.equal(badRequestSchema?.oneOf?.length, 2);
|
||||
assert.equal(badRequestSchema?.oneOf?.[0]?.$ref, "#/components/schemas/ValidationErrorResponse");
|
||||
assert.equal(badRequestSchema?.oneOf?.[1]?.properties?.error?.type, "string");
|
||||
assert.equal(
|
||||
operation.responses?.["401"]?.$ref,
|
||||
"#/components/responses/ManagementAuthenticationRequired"
|
||||
);
|
||||
assert.equal(operation.responses?.["403"]?.$ref, "#/components/responses/ManagementInvalidToken");
|
||||
assert.equal(operation.responses?.["503"]?.$ref, "#/components/responses/InternalError");
|
||||
});
|
||||
|
||||
test("every x-always-protected path matches ALWAYS_PROTECTED_API_PATHS in routeGuard.ts", () => {
|
||||
for (const [pathStr, methods] of Object.entries(paths)) {
|
||||
if (!methods || typeof methods !== "object") continue;
|
||||
|
||||
Reference in New Issue
Block a user