Skip to content

Commit 6246ed9

Browse files
committed
feat(http): serve OpenAPI spec and ChatGPT plugin manifest (no auth required)
1 parent ffac673 commit 6246ed9

1 file changed

Lines changed: 63 additions & 0 deletions

File tree

src/http.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,9 @@ pub fn build_router(state: ApiState) -> Router {
380380
.route("/api/migrate/preview", post(handle_migrate_preview))
381381
.route("/api/migrate/apply", post(handle_migrate_apply))
382382
.route("/api/migrate/undo", post(handle_migrate_undo))
383+
// OpenAPI / ChatGPT plugin discovery (no auth required)
384+
.route("/openapi.json", get(handle_openapi))
385+
.route("/.well-known/ai-plugin.json", get(handle_plugin_manifest))
383386
.layer(cors)
384387
.with_state(state)
385388
}
@@ -388,6 +391,30 @@ async fn health_check() -> &'static str {
388391
"ok"
389392
}
390393

394+
async fn handle_openapi(State(state): State<ApiState>) -> impl IntoResponse {
395+
let default_url = format!("http://{}:{}", state.http_config.host, state.http_config.port);
396+
let server_url = state
397+
.http_config
398+
.plugin
399+
.public_url
400+
.as_deref()
401+
.unwrap_or(&default_url);
402+
let spec = crate::openapi::build_openapi_spec(server_url);
403+
Json(spec)
404+
}
405+
406+
async fn handle_plugin_manifest(State(state): State<ApiState>) -> impl IntoResponse {
407+
let default_url = format!("http://{}:{}", state.http_config.host, state.http_config.port);
408+
let server_url = state
409+
.http_config
410+
.plugin
411+
.public_url
412+
.as_deref()
413+
.unwrap_or(&default_url);
414+
let manifest = crate::openapi::build_plugin_manifest(&state.http_config, server_url);
415+
Json(manifest)
416+
}
417+
391418
// ---------------------------------------------------------------------------
392419
// Read endpoint handlers
393420
// ---------------------------------------------------------------------------
@@ -1273,4 +1300,40 @@ mod tests {
12731300
assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
12741301
assert!(response.headers().get("retry-after").is_some());
12751302
}
1303+
1304+
// -----------------------------------------------------------------------
1305+
// OpenAPI / Plugin manifest tests (no auth required)
1306+
// -----------------------------------------------------------------------
1307+
1308+
#[tokio::test]
1309+
async fn test_openapi_no_auth_required() {
1310+
let state = test_api_state();
1311+
let app = build_router(state);
1312+
let response = app
1313+
.oneshot(
1314+
axum::http::Request::builder()
1315+
.uri("/openapi.json")
1316+
.body(Body::empty())
1317+
.unwrap(),
1318+
)
1319+
.await
1320+
.unwrap();
1321+
assert_eq!(response.status(), StatusCode::OK);
1322+
}
1323+
1324+
#[tokio::test]
1325+
async fn test_plugin_manifest_no_auth_required() {
1326+
let state = test_api_state();
1327+
let app = build_router(state);
1328+
let response = app
1329+
.oneshot(
1330+
axum::http::Request::builder()
1331+
.uri("/.well-known/ai-plugin.json")
1332+
.body(Body::empty())
1333+
.unwrap(),
1334+
)
1335+
.await
1336+
.unwrap();
1337+
assert_eq!(response.status(), StatusCode::OK);
1338+
}
12761339
}

0 commit comments

Comments
 (0)