up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Symbols Server CI / symbols-smoke (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Symbols Server CI / symbols-smoke (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
This commit is contained in:
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
@@ -146,6 +147,304 @@ app.MapGet("/excititor/status", async (HttpContext context,
|
||||
|
||||
app.MapHealthChecks("/excititor/health");
|
||||
|
||||
// OpenAPI discovery (WEB-OAS-61-001)
|
||||
app.MapGet("/.well-known/openapi", () =>
|
||||
{
|
||||
var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0";
|
||||
|
||||
var payload = new
|
||||
{
|
||||
service = "excititor",
|
||||
specVersion = "3.1.0",
|
||||
version,
|
||||
format = "application/json",
|
||||
url = "/openapi/excititor.json",
|
||||
errorEnvelopeSchema = "#/components/schemas/Error"
|
||||
};
|
||||
|
||||
return Results.Json(payload);
|
||||
});
|
||||
|
||||
app.MapGet("/openapi/excititor.json", () =>
|
||||
{
|
||||
var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0";
|
||||
|
||||
var spec = new
|
||||
{
|
||||
openapi = "3.1.0",
|
||||
info = new
|
||||
{
|
||||
title = "StellaOps Excititor API",
|
||||
version,
|
||||
description = "Aggregation-only VEX observation, timeline, and attestation APIs"
|
||||
},
|
||||
paths = new Dictionary<string, object>
|
||||
{
|
||||
["/excititor/status"] = new
|
||||
{
|
||||
get = new
|
||||
{
|
||||
summary = "Service status (aggregation-only metadata)",
|
||||
responses = new
|
||||
{
|
||||
["200"] = new
|
||||
{
|
||||
description = "OK",
|
||||
content = new Dictionary<string, object>
|
||||
{
|
||||
["application/json"] = new
|
||||
{
|
||||
schema = new { @ref = "#/components/schemas/StatusResponse" },
|
||||
examples = new Dictionary<string, object>
|
||||
{
|
||||
["example"] = new
|
||||
{
|
||||
value = new
|
||||
{
|
||||
timeUtc = "2025-11-24T00:00:00Z",
|
||||
mongoBucket = "vex-raw",
|
||||
gridFsInlineThresholdBytes = 1048576,
|
||||
artifactStores = new[] { "S3ArtifactStore", "OfflineBundleArtifactStore" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
["/excititor/health"] = new
|
||||
{
|
||||
get = new
|
||||
{
|
||||
summary = "Health check",
|
||||
responses = new
|
||||
{
|
||||
["200"] = new
|
||||
{
|
||||
description = "Healthy",
|
||||
content = new Dictionary<string, object>
|
||||
{
|
||||
["application/json"] = new
|
||||
{
|
||||
examples = new Dictionary<string, object>
|
||||
{
|
||||
["example"] = new
|
||||
{
|
||||
value = new
|
||||
{
|
||||
status = "Healthy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
["/obs/excititor/timeline"] = new
|
||||
{
|
||||
get = new
|
||||
{
|
||||
summary = "VEX timeline stream (SSE)",
|
||||
parameters = new object[]
|
||||
{
|
||||
new { name = "cursor", @in = "query", schema = new { type = "string" }, required = false, description = "Numeric cursor or Last-Event-ID" },
|
||||
new { name = "limit", @in = "query", schema = new { type = "integer", minimum = 1, maximum = 100 }, required = false }
|
||||
},
|
||||
responses = new
|
||||
{
|
||||
["200"] = new
|
||||
{
|
||||
description = "Event stream",
|
||||
headers = new Dictionary<string, object>
|
||||
{
|
||||
["Deprecation"] = new
|
||||
{
|
||||
description = "Set to true when this route is superseded",
|
||||
schema = new { type = "string" }
|
||||
},
|
||||
["Link"] = new
|
||||
{
|
||||
description = "Link to OpenAPI description",
|
||||
schema = new { type = "string" },
|
||||
example = "</openapi/excititor.json>; rel=\"describedby\"; type=\"application/json\""
|
||||
}
|
||||
},
|
||||
content = new Dictionary<string, object>
|
||||
{
|
||||
["text/event-stream"] = new
|
||||
{
|
||||
examples = new Dictionary<string, object>
|
||||
{
|
||||
["event"] = new
|
||||
{
|
||||
value = "id: 123\nretry: 5000\nevent: timeline\ndata: {\"id\":123,\"tenant\":\"acme\",\"kind\":\"vex.status\",\"createdUtc\":\"2025-11-24T00:00:00Z\"}\n\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
["400"] = new
|
||||
{
|
||||
description = "Invalid cursor",
|
||||
content = new Dictionary<string, object>
|
||||
{
|
||||
["application/json"] = new
|
||||
{
|
||||
schema = new { @ref = "#/components/schemas/Error" },
|
||||
examples = new Dictionary<string, object>
|
||||
{
|
||||
["bad-cursor"] = new
|
||||
{
|
||||
value = new
|
||||
{
|
||||
error = new
|
||||
{
|
||||
code = "ERR_CURSOR",
|
||||
message = "cursor must be integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
["/airgap/v1/vex/import"] = new
|
||||
{
|
||||
post = new
|
||||
{
|
||||
summary = "Register sealed mirror bundle metadata",
|
||||
requestBody = new
|
||||
{
|
||||
required = true,
|
||||
content = new Dictionary<string, object>
|
||||
{
|
||||
["application/json"] = new
|
||||
{
|
||||
schema = new { @ref = "#/components/schemas/AirgapImportRequest" }
|
||||
}
|
||||
}
|
||||
},
|
||||
responses = new
|
||||
{
|
||||
["200"] = new { description = "Accepted" },
|
||||
["400"] = new
|
||||
{
|
||||
description = "Validation error",
|
||||
content = new Dictionary<string, object>
|
||||
{
|
||||
["application/json"] = new
|
||||
{
|
||||
schema = new { @ref = "#/components/schemas/Error" },
|
||||
examples = new Dictionary<string, object>
|
||||
{
|
||||
["validation-failed"] = new
|
||||
{
|
||||
value = new
|
||||
{
|
||||
error = new
|
||||
{
|
||||
code = "ERR_VALIDATION",
|
||||
message = "PayloadHash is required."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
["403"] = new
|
||||
{
|
||||
description = "Trust validation failed",
|
||||
content = new Dictionary<string, object>
|
||||
{
|
||||
["application/json"] = new
|
||||
{
|
||||
schema = new { @ref = "#/components/schemas/Error" },
|
||||
examples = new Dictionary<string, object>
|
||||
{
|
||||
["trust-failed"] = new
|
||||
{
|
||||
value = new
|
||||
{
|
||||
error = new
|
||||
{
|
||||
code = "ERR_TRUST",
|
||||
message = "Signature trust root not recognized."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
components = new
|
||||
{
|
||||
schemas = new Dictionary<string, object>
|
||||
{
|
||||
["Error"] = new
|
||||
{
|
||||
type = "object",
|
||||
required = new[] { "error" },
|
||||
properties = new Dictionary<string, object>
|
||||
{
|
||||
["error"] = new
|
||||
{
|
||||
type = "object",
|
||||
required = new[] { "code", "message" },
|
||||
properties = new Dictionary<string, object>
|
||||
{
|
||||
["code"] = new { type = "string", example = "ERR_EXAMPLE" },
|
||||
["message"] = new { type = "string", example = "Details about the error." }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
["StatusResponse"] = new
|
||||
{
|
||||
type = "object",
|
||||
required = new[] { "timeUtc", "mongoBucket", "artifactStores" },
|
||||
properties = new Dictionary<string, object>
|
||||
{
|
||||
["timeUtc"] = new { type = "string", format = "date-time" },
|
||||
["mongoBucket"] = new { type = "string" },
|
||||
["gridFsInlineThresholdBytes"] = new { type = "integer", format = "int64" },
|
||||
["artifactStores"] = new { type = "array", items = new { type = "string" } }
|
||||
}
|
||||
},
|
||||
["AirgapImportRequest"] = new
|
||||
{
|
||||
type = "object",
|
||||
required = new[] { "bundleId", "mirrorGeneration", "signedAt", "publisher", "payloadHash", "signature" },
|
||||
properties = new Dictionary<string, object>
|
||||
{
|
||||
["bundleId"] = new { type = "string", example = "mirror-2025-11-24" },
|
||||
["mirrorGeneration"] = new { type = "string", example = "g001" },
|
||||
["signedAt"] = new { type = "string", format = "date-time" },
|
||||
["publisher"] = new { type = "string", example = "acme" },
|
||||
["payloadHash"] = new { type = "string", example = "sha256:..." },
|
||||
["payloadUrl"] = new { type = "string", nullable = true },
|
||||
["signature"] = new { type = "string", example = "base64-signature" },
|
||||
["transparencyLog"] = new { type = "string", nullable = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Results.Json(spec);
|
||||
});
|
||||
|
||||
app.MapPost("/airgap/v1/vex/import", async (
|
||||
[FromServices] AirgapImportValidator validator,
|
||||
[FromServices] AirgapSignerTrustService trustService,
|
||||
|
||||
Reference in New Issue
Block a user