Export Center API Contract
Contract ID: CONTRACT-EXPORT-BUNDLE-009 Version: 1.0 Status: Published Last Updated: 2026-05-30
Overview
This contract is the integration reference for the Stella Ops Export Center API. It is written for engineers building clients against Export Center and for operators configuring exports. It defines export profiles (scope and configuration), export runs (executions), produced artifacts, and run verification (manifest, signature, and content-hash checks). It also covers the supported output formats, persistence schema, and telemetry.
Model note. The implemented API is organized around profiles → runs → artifacts, not around standalone “jobs” and “bundles”. A profile holds the reusable scope/format/signing/schedule configuration; a run is a single execution of a profile; artifacts are the files a run produces. There is no separate
/jobsresource — scheduling is a property of a profile (kind = Scheduledplus a cronschedule).
Implementation References
- Export Center service:
src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/- Endpoints:
Api/ExportApiEndpoints.cs - DTOs:
Api/ExportApiModels.cs - Telemetry:
Telemetry/ExportTelemetry.cs
- Endpoints:
- Core domain:
src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Core/Domain/ExportProfile.cs,Domain/ExportRun.cs,Domain/ExportDistribution.csPlanner/ExportPlanModels.cs,Planner/ExportScopeModels.csPersistence/IExportRunRepository.cs(ExportArtifactrecord)Verification/ExportVerificationModels.cs
- Persistence schema:
src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.Infrastructure/Db/Migrations/001_initial_schema.sql - Scope catalog:
src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs - API stub (separate surface):
src/Api/StellaOps.Api.OpenApi/export-center/openapi.yaml— a v0.0.2 stub covering only/health,/healthz, and a legacy/bundlesread surface. It does not describe the/v1/exports/*API below and should not be treated as authoritative for it.
Authorization Scopes
All Export Center endpoints require one of the following scopes (StellaOpsScopes):
| Scope | Constant | Granted operations |
|---|---|---|
export.viewer | StellaOpsScopes.ExportViewer | Read profiles, runs, artifacts; download artifacts; verify; read manifest/attestation; SSE streams |
export.operator | StellaOpsScopes.ExportOperator | Create/update profiles; start runs; cancel runs |
export.admin | StellaOpsScopes.ExportAdmin | Archive (soft-delete) profiles; retention/encryption/scheduling policy |
The endpoint group is registered with RequireAuthorization(ExportViewer); write operations additionally require ExportOperator, and archive requires ExportAdmin (see ExportApiEndpoints.MapExportApiEndpoints).
Tenant is resolved from the tenant_id (or tid) claim on the caller’s principal; requests without a resolvable tenant claim return 400 Bad Request.
Data Models
ExportProfile
Reusable definition of an export’s scope, format, signing, and (optional) schedule. Source: Core/Domain/ExportProfile.cs, response DTO ExportProfileResponse in Api/ExportApiModels.cs.
{
"profileId": "8d2c0b6e-0000-0000-0000-000000000001",
"tenantId": "0a1b2c3d-0000-0000-0000-000000000001",
"name": "daily-vex-export",
"description": "Daily VEX advisory export",
"kind": "Scheduled",
"status": "Active",
"scope": {
"targetKinds": ["vex"],
"tags": ["critical", "high"],
"namespaces": ["github", "redhat"],
"maxItems": 5000
},
"format": {
"format": "Ndjson",
"compression": "Gzip",
"includeMetadata": true,
"prettyPrint": false,
"normalizeTimestamps": true,
"sortKeys": true
},
"signing": {
"enabled": true,
"algorithm": "ES256",
"keyId": "signing-key-001",
"includeProvenance": true
},
"schedule": "0 0 * * *",
"createdAt": "2026-05-30T10:00:00Z",
"updatedAt": "2026-05-30T10:00:00Z",
"archivedAt": null
}
Field notes:
profileId,tenantId— GUIDs (profile_id,tenant_idcolumns).kind—ExportProfileKind(see below). Defaults toAdHocon create.status—ExportProfileStatus(see below). A newly created profile starts inDraft; runs may only be started from anActiveprofile.scope,format,signing— persisted as JSON columns (scope_json,format_json,signing_json) and surfaced as structured objects in the response.schedule— cron expression string forScheduledprofiles (nullable).
ExportProfileKind enum
Core/Domain/ExportProfile.cs. Serialized by name.
| Value | Numeric | Description |
|---|---|---|
AdHoc | 1 | Manually triggered, one-off (default on create) |
Scheduled | 2 | Runs on a cron schedule |
EventDriven | 3 | Triggered by webhooks/events |
Continuous | 4 | Near-real-time mirror updates |
ExportProfileStatus enum
| Value | Numeric | Description |
|---|---|---|
Draft | 1 | Being set up (default on create) |
Active | 2 | Can start runs |
Paused | 3 | Will not run scheduled exports |
Archived | 4 | Read-only (soft-deleted) |
ExportRun
A single execution of a profile. Source: Core/Domain/ExportRun.cs, response DTO ExportRunResponse.
{
"runId": "1f7e9a40-0000-0000-0000-000000000001",
"profileId": "8d2c0b6e-0000-0000-0000-000000000001",
"tenantId": "0a1b2c3d-0000-0000-0000-000000000001",
"status": "Running",
"trigger": "Api",
"correlationId": "corr-001",
"initiatedBy": "user-123",
"progress": {
"totalItems": 150,
"processedItems": 42,
"failedItems": 0,
"totalSizeBytes": 1048576,
"percentComplete": 28.0
},
"error": null,
"createdAt": "2026-05-30T00:00:00Z",
"startedAt": "2026-05-30T00:00:00Z",
"completedAt": null,
"expiresAt": "2026-06-06T00:00:00Z",
"artifacts": [
{
"artifactId": "2a3b4c5d-0000-0000-0000-000000000001",
"name": "vex-export.ndjson.gz",
"kind": "vex",
"sizeBytes": 1048576,
"contentType": "application/x-ndjson",
"checksum": "sha256:abc123...",
"downloadUrl": "/v1/exports/runs/1f7e9a40-.../artifacts/2a3b4c5d-.../download"
}
]
}
Field notes:
progress.percentCompleteis computed (processedItems / totalItems * 100, rounded to 2 dp).expiresAtdefaults tocreatedAt + 7 dayswhen a run is created via the API.artifactsis only populated when fetching a single run (GET /v1/exports/runs/{runId}); list responses omit it.
ExportRunStatus enum
Core/Domain/ExportRun.cs. DB column status smallint CHECK (status BETWEEN 1 AND 6).
| Value | Numeric | Description |
|---|---|---|
Queued | 1 | Waiting to start (concurrency-limited) |
Running | 2 | Actively processing |
Completed | 3 | Finished successfully |
PartiallyCompleted | 4 | Completed with some item failures |
Failed | 5 | Failed |
Cancelled | 6 | Cancelled |
ExportRunTrigger enum
DB column trigger smallint CHECK (trigger BETWEEN 1 AND 4).
| Value | Numeric | Description |
|---|---|---|
Manual | 1 | Manually triggered by a user |
Scheduled | 2 | Triggered by a cron schedule |
Event | 3 | Triggered by an external event |
Api | 4 | Triggered via API (StartRunFromProfile uses this) |
ExportArtifact
A file produced by a run. Source: Core/Persistence/IExportRunRepository.cs (ExportArtifact record), response DTO ExportArtifactResponse.
{
"artifactId": "2a3b4c5d-0000-0000-0000-000000000001",
"runId": "1f7e9a40-0000-0000-0000-000000000001",
"tenantId": "0a1b2c3d-0000-0000-0000-000000000001",
"name": "vex-export.ndjson.gz",
"kind": "vex",
"path": "/var/lib/exports/.../vex-export.ndjson.gz",
"sizeBytes": 1048576,
"contentType": "application/x-ndjson",
"checksum": "sha256:abc123...",
"checksumAlgorithm": "SHA-256",
"metadata": { "format": "ndjson" },
"createdAt": "2026-05-30T00:00:05Z",
"expiresAt": "2026-06-06T00:00:05Z"
}
kind is a free-form string (e.g. sbom, vex, attestation, policy, evidence, manifest); checksumAlgorithm defaults to SHA-256.
Export Formats
ExportFormatOptions.Format (Core/Planner/ExportPlanModels.cs). Serialized by name.
| Format | Numeric | Description |
|---|---|---|
JsonRaw | 1 | Raw JSON, one object per file (default) |
JsonPolicy | 2 | JSON with policy metadata included |
Ndjson | 3 | Newline-delimited JSON (streaming) |
Csv | 4 | CSV |
Mirror | 5 | Full mirror layout with indexes |
TrivyDb | 6 | Trivy vulnerability DB format (schema v2) |
TrivyJavaDb | 7 | Trivy Java DB format (Maven/Gradle/SBT supplement) |
Not implemented as
ExportFormatvalues. OpenVEX, CSAF, CycloneDX, and SPDX are content schemas produced by their owning modules (Excititor / Scanner), not Export Center output-format options. Export Center carries those payloads as items withinJsonRaw/Ndjson/Mirroroutputs.
ExportFormatOptions
| Field | Type | Default | Description |
|---|---|---|---|
format | ExportFormat | JsonRaw | Output format |
compression | CompressionFormat | None | None (0), Gzip (1), Zstd (2), Brotli (3) |
includeMetadata | bool | true | Include item metadata |
prettyPrint | bool | false | Pretty-print JSON |
redactFields | string[] | [] | Field paths to redact |
normalizeTimestamps | bool | true | Normalize timestamps for determinism |
sortKeys | bool | true | Sort object keys for determinism |
ExportScope
Core/Planner/ExportScopeModels.cs. Stored as scope_json.
| Field | Type | Description |
|---|---|---|
targetKinds | string[] | Kind filter (e.g. sbom, vex, attestation) |
sourceRefs | string[] | Specific source references to include |
tags | string[] | Items must have all specified tags |
namespaces | string[] | Namespace/project filter |
dateRange | object | { from, to, field }; field ∈ CreatedAt/ModifiedAt/ProcessedAt |
maxItems | int? | Maximum number of items |
sampling | object | { strategy, size, seed, stratifyBy }; strategy ∈ None/Random/First/Last/Stratified/Systematic |
runIds | GUID[] | Include items from specific runs |
excludePatterns | string[] | Exclude items matching patterns |
Schedule Format
Cron expressions stored in ExportProfile.schedule (used by Scheduled profiles). Format:
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *
Examples:
| Schedule | Description |
|---|---|
0 0 * * * | Daily at midnight |
0 */6 * * * | Every 6 hours |
0 0 * * 0 | Weekly on Sunday |
0 0 1 * * | Monthly on the 1st |
Distribution Targets
Distribution targets are part of the planner/run pipeline, identified by ExportDistributionKind (Core/Domain/ExportDistribution.cs). They are specified on the export plan (ExportDistributionTargetSpec), not on the profile create/update DTOs above.
| Kind | Numeric | Description |
|---|---|---|
FileSystem | 1 | Local file system |
AmazonS3 | 2 | Amazon S3 |
Mirror | 3 | Mirror server |
OfflineKit | 4 | Air-gap offline kit |
Webhook | 5 | Webhook notification (metadata only) |
OciRegistry | 6 | OCI registry (artifact push) |
The DB
export_distributions.kindcheck constraint isBETWEEN 1 AND 5; theOciRegistry(6) kind is defined in the domain enum and is exercised via the OCI distribution publishers (Distribution/Oci/).
API Endpoints
Base group: /v1/exports (registered via app.MapExportApiEndpoints(); no api path-base prefix on the service itself). Legacy /exports, POST /exports, DELETE /exports/{id} endpoints return 410 Gone and point callers at /v1/exports/*.
Profile Management
List profiles
GET /v1/exports/profiles?status=Active&kind=Scheduled&search=vex&offset=0&limit=50
Scope: export.viewer
Response: 200 OK
{
"items": [ { ...ExportProfileResponse } ],
"totalCount": 5,
"offset": 0,
"limit": 50
}
limit defaults to 50 and is clamped to 1..100; offset is clamped to >= 0.
Get profile
GET /v1/exports/profiles/{profileId} (profileId: GUID)
Scope: export.viewer
Response: 200 OK | 404 Not Found
Create profile
POST /v1/exports/profiles
Scope: export.operator
Content-Type: application/json
{
"name": "daily-vex-export",
"description": "Daily VEX advisory export",
"kind": "Scheduled",
"scope": { ...ExportScope },
"format": { ...ExportFormatOptions },
"signing": { ...ExportSigningOptions },
"schedule": "0 0 * * *"
}
Response: 201 Created (Location: /v1/exports/profiles/{profileId})
| 409 Conflict (profile name not unique within tenant)
| 400 Bad Request
Created profiles start in status = Draft.
Update profile
PUT /v1/exports/profiles/{profileId}
Scope: export.operator
{ "name?": "...", "description?": "...", "status?": "Active",
"scope?": {...}, "format?": {...}, "signing?": {...}, "schedule?": "..." }
Response: 200 OK | 404 Not Found | 409 Conflict | 400 Bad Request
Updating an Archived profile returns 400. Setting status to Active / Paused emits the corresponding audit operation.
Archive profile
DELETE /v1/exports/profiles/{profileId}
Scope: export.admin
Response: 204 No Content | 404 Not Found
This is a soft delete (sets archivedAt / status = Archived).
Start run from profile
POST /v1/exports/profiles/{profileId}/runs
Scope: export.operator
{ "scopeOverride?": {...}, "formatOverride?": {...},
"correlationId?": "corr-001", "dryRun?": false }
Response: 202 Accepted (Location: /v1/exports/runs/{runId})
| 404 Not Found (profile missing)
| 400 Bad Request (profile not Active)
| 429 Too Many Requests (concurrency limit; see below)
The created run uses trigger = Api. If concurrency limits are reached and queueing is enabled, the run is created in status = Queued; otherwise it starts Running.
Run Management
List runs
GET /v1/exports/runs?profileId=&status=&trigger=&createdAfter=&createdBefore=&correlationId=&offset=0&limit=50
Scope: export.viewer
Response: 200 OK
{ "items": [ { ...ExportRunResponse } ], "totalCount": 1, "offset": 0, "limit": 50 }
Get run
GET /v1/exports/runs/{runId}
Scope: export.viewer
Response: 200 OK (includes "artifacts" list) | 404 Not Found
Cancel run
POST /v1/exports/runs/{runId}/cancel
Scope: export.operator
Response: 200 OK (updated ExportRunResponse)
| 404 Not Found
| 400 Bad Request (run not in a cancellable state)
Artifact Retrieval
List artifacts for a run
GET /v1/exports/runs/{runId}/artifacts
Scope: export.viewer
Response: 200 OK
{ "items": [ { ...ExportArtifactResponse } ], "totalCount": 1 }
| 404 Not Found
Get artifact metadata
GET /v1/exports/runs/{runId}/artifacts/{artifactId}
Scope: export.viewer
Response: 200 OK (ExportArtifactResponse) | 404 Not Found
Download artifact
GET /v1/exports/runs/{runId}/artifacts/{artifactId}/download
Scope: export.viewer
Response: 200 OK
Content-Type: <artifact.contentType, default application/octet-stream>
(file stream; filename = artifact.name)
| 404 Not Found (run, artifact, or backing file missing)
Downloads are audited and increment export_artifact_downloads_total.
Run Events (Server-Sent Events)
GET /v1/exports/runs/{runId}/events
Scope: export.viewer
Content-Type: text/event-stream
event: connected
data: { ...ExportRunResponse }
event: run.progress
data: { totalItems, processedItems, failedItems, totalSizeBytes }
event: run.completed
data: { ...ExportRunResponse }
The stream emits connected, progress updates (run.progress), status-change events, then a final disconnected event. SSE event-type constants (ExportRunSseEventTypes):
| Event type | Constant |
|---|---|
run.started | RunStarted |
run.progress | RunProgress |
run.phase.started | RunPhaseStarted |
run.phase.completed | RunPhaseCompleted |
run.artifact.created | RunArtifactCreated |
run.completed | RunCompleted |
run.failed | RunFailed |
run.cancelled | RunCancelled |
Verification
Verify a run
POST /v1/exports/runs/{runId}/verify
Scope: export.viewer
{ "verifyHashes": true, "verifySignatures": true, "verifyManifest": true,
"verifyEncryption": true, "checkRekor": false, "trustedKeys": [] }
Response: 200 OK
{
"runId": "...",
"status": "Valid",
"isValid": true,
"verifiedAt": "2026-05-30T00:00:10Z",
"manifest": { "isValid": true, "entryCount": 3, "digest": "sha256:...", "errors": [] },
"signature": { "isValid": true, "algorithm": "ES256", "keyId": "...",
"signer": "...", "rekorVerified": null, "rekorLogIndex": null, "errors": [] },
"fileHashes": [ { "path": "...", "isValid": true, "expectedHash": "...",
"computedHash": "...", "algorithm": "SHA-256", "error": null } ],
"errors": [],
"warnings": []
}
| 404 Not Found
status is the VerificationStatus enum serialized to string: Valid, Invalid, Partial, Error, Pending. isValid is true only when status == Valid.
Get run manifest
GET /v1/exports/runs/{runId}/verify/manifest
Scope: export.viewer
Response: 200 OK
{ "runId": "...", "manifestContent": "<raw manifest>", "digest": "sha256:..." }
| 404 Not Found
Get attestation status
GET /v1/exports/runs/{runId}/verify/attestation
Scope: export.viewer
Response: 200 OK
{ "runId": "...", "hasAttestation": true, "attestationType": "DSSE",
"manifestDigest": "sha256:...", "verifiedAt": null }
| 404 Not Found
Stream verification progress (SSE)
POST /v1/exports/runs/{runId}/verify/stream
Scope: export.viewer
Content-Type: text/event-stream
event: progress
data: { ...verification progress }
Signing Configuration
ExportSigningOptions (Api/ExportApiModels.cs). Stored as signing_json.
{
"enabled": true,
"algorithm": "ES256",
"keyId": "signing-key-001",
"providerHint": "vault",
"includeProvenance": true
}
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable signing |
algorithm | string | ES256 | Signature algorithm |
keyId | string? | — | Signing key identifier |
providerHint | string? | — | Free-form hint selecting the signing provider/backend (e.g. vault for the HashiCorp Vault KEK backend). |
includeProvenance | bool | true | Embed provenance in the attestation |
Attestations are emitted as DSSE envelopes (see GET .../verify/attestation, which reports attestationType: "DSSE" when a signature is present). Rekor inclusion is optional and only checked when checkRekor is requested at verify time.
Concurrency Control
ExportConcurrencyOptions (Api/ExportApiModels.cs) governs run admission in StartRunFromProfile:
| Option | Default | Description |
|---|---|---|
MaxConcurrentRunsPerTenant | 4 | Max active runs per tenant |
MaxConcurrentRunsPerProfile | 2 | Max active runs per profile |
QueueExcessRuns | true | Queue runs that exceed limits instead of rejecting |
MaxQueueSizePerTenant | 10 | Max queued runs per tenant |
When limits are exceeded and queueing is disabled (or the queue is full), the start request returns 429 Too Many Requests and increments export_concurrency_limit_exceeded_total.
Persistence
Schema export_center (plus export_center_app for tenant helpers). Created and migrated from 001_initial_schema.sql. All tenant-scoped tables enable PostgreSQL Row-Level Security keyed on app.current_tenant.
| Table | Key | Notes |
|---|---|---|
export_center.export_profiles | profile_id | kind/status smallints; unique (tenant_id, name) where archived_at IS NULL |
export_center.export_runs | run_id | FK → export_profiles; status 1–6, trigger 1–4 |
export_center.export_inputs | input_id | Items per run; FK → export_runs (cascade); content_hash is ^[0-9a-f]{64}$ |
export_center.export_distributions | distribution_id | Distribution to targets; FK → export_runs (cascade); kind 1–5 |
Telemetry
Meter StellaOps.ExportCenter (Telemetry/ExportTelemetry.cs). Selected metrics:
| Metric | Type | Notes |
|---|---|---|
export_runs_total | counter | Runs initiated; tags: profile/tenant/type |
export_runs_success_total | counter | Successful runs |
export_runs_failed_total | counter | Failed runs; tag error_code |
export_artifacts_total | counter | Artifacts exported; tag artifact_type |
export_bytes_total | counter | Bytes exported |
export_artifact_downloads_total | counter | Artifact downloads; tags tenant_id, artifact_kind |
export_sse_connections_total | counter | SSE connections |
export_concurrency_limit_exceeded_total | counter | Concurrency rejections; tag limit_type |
export_audit_events_total | counter | Audit events; tags operation, resource_type |
export_run_duration_seconds | histogram | Run duration |
export_bundle_size_bytes | histogram | Bundle size |
export_runs_in_progress | up/down counter | Active runs gauge |
Error Behavior
The API uses HTTP status codes rather than a custom error-code catalog:
| Condition | Status |
|---|---|
| Missing/invalid tenant claim | 400 Bad Request |
| Profile/run/artifact not found | 404 Not Found |
| Duplicate profile name within tenant | 409 Conflict |
Start run on non-Active profile | 400 Bad Request |
Update on Archived profile | 400 Bad Request |
| Cancel run in non-cancellable state | 400 Bad Request |
| Concurrency / queue limit exceeded | 429 Too Many Requests |
Legacy /exports endpoints | 410 Gone |
Validation/error message strings are produced by the localization layer (
StellaOps.Localization), e.g.exportcenter.error.profile_name_conflict.
Related Contracts
- Mirror Bundle Contract - Bundle format for air-gap (consumed by
Mirror/OfflineKitdistribution kinds) - Risk Scoring Contract - Risk bundle exports (
export_risk_bundle_jobs_*telemetry)
