Export Center Gateway Contract (draft v0.9)

This contract describes how the Stella Ops Web gateway proxies the Export Center service: tenant-scoped exports, deterministic responses, sealed-mode readiness, and offline-friendly signed-URL handling. It is the integration surface for Console and SDK clients that create export profiles, start runs, and download or verify the resulting artifacts.

Status: reconciled against implementation (2026-05-30). The Export Center service (src/ExportCenter) exposes a typed REST surface under /v1/exports/* (ExportApiEndpoints.MapExportApiEndpoints). The Web gateway proxies it under the public prefix /api/export-center (Angular app.config.ts, resolveApiBaseUrl(gatewayBase, '/api/export-center')). Identifiers are GUIDs, not the export-run::tenant::… composite strings used in earlier drafts. Scopes have been corrected to the canonical catalog (StellaOps.Auth.Abstractions/StellaOpsScopes.cs): the export:read / export:write scopes in earlier drafts do not exist. Sections still marked (forward design — not implemented) describe a planned distribution/OCI surface that the service does not yet expose as HTTP endpoints.

Security / headers

Endpoints

All paths are relative to the service base (/v1/exports), proxied at the gateway under /api/export-center. Route IDs ({profileId}, {runId}, {artifactId}) are GUIDs.

Profiles:

Runs:

Artifacts:

Verification:

Specialised export families (mapped alongside the core API in Program.cs): /v1/exports/assurance/*, /v1/exports/nis2/soa/*, plus attestation, promotion-attestation, incident, risk-bundle, simulation, audit-bundle, exception-report, and lineage endpoints. These are out of scope for this contract; see the Export Center module dossier.

Deprecated (return 410 Gone): GET /exports, POST /exports, DELETE /exports/{id} — use the /v1/exports/* routes above.

(forward design — not implemented) GET /export-center/distributions/{id} for signed OCI/object-storage distribution URLs is not an HTTP endpoint in the current service. Distribution is handled internally (Distribution/IExportDistributionLifecycle, Distribution/Oci/*) and artifacts are retrieved via the runs/{runId}/artifacts/{artifactId}/download endpoint.

POST /v1/exports/profiles/{profileId}/runs (request)

Body shape: StartExportRunRequest (Api/ExportApiModels.cs). Run inputs (targets, formats, signing, retention, encryption, distribution) are defined on the profile, not the run-start request; the run may override scope/format only.

{
  "scopeOverride": {
    "targetKinds": ["sbom", "vex", "attestation"],
    "namespaces": ["tenant-default"],
    "maxItems": 1000
  },
  "formatOverride": {
    "format": "Ndjson",
    "compression": "None",
    "sortKeys": true,
    "normalizeTimestamps": true
  },
  "correlationId": "01HXYZ...",
  "dryRun": false
}

All fields are optional. format values: JsonRaw, JsonPolicy, Ndjson, Csv, Mirror, TrivyDb, TrivyJavaDb (and further mirror/standards formats). A 429 is returned when tenant/profile concurrency limits are exceeded and queueing is disabled.

202 Accepted

Body: ExportRunResponse. The Location header is /v1/exports/runs/{runId}.

{
  "runId": "8f3c1d2e-4b5a-6c7d-8e9f-0a1b2c3d4e5f",
  "profileId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "tenantId": "0c9d8e7f-6a5b-4c3d-2e1f-0a9b8c7d6e5f",
  "status": "Queued",
  "trigger": "Api",
  "correlationId": "01HXYZ...",
  "initiatedBy": "user-123",
  "progress": {
    "totalItems": 0,
    "processedItems": 0,
    "failedItems": 0,
    "totalSizeBytes": 0,
    "percentComplete": 0
  },
  "createdAt": "2026-05-30T10:00:00Z",
  "startedAt": null,
  "expiresAt": "2026-06-06T10:00:00Z"
}

status is an enum string: Queued, Running, Completed, PartiallyCompleted, Failed, Cancelled. trigger is Manual, Scheduled, Event, or Api (API-initiated runs are Api). Runs are created with a default 7-day expiresAt.

GET /v1/exports/runs/{runId}

Body: ExportRunResponse with artifacts (an array of ExportArtifactSummary).

{
  "runId": "8f3c1d2e-4b5a-6c7d-8e9f-0a1b2c3d4e5f",
  "profileId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "tenantId": "0c9d8e7f-6a5b-4c3d-2e1f-0a9b8c7d6e5f",
  "status": "Running",
  "trigger": "Api",
  "correlationId": "01HXYZ...",
  "initiatedBy": "user-123",
  "progress": {
    "totalItems": 200,
    "processedItems": 70,
    "failedItems": 0,
    "totalSizeBytes": 1048576,
    "percentComplete": 35.0
  },
  "error": null,
  "createdAt": "2026-05-30T10:00:00Z",
  "startedAt": "2026-05-30T10:00:01Z",
  "completedAt": null,
  "expiresAt": "2026-06-06T10:00:00Z",
  "artifacts": [
    {
      "artifactId": "c0ffee00-1111-2222-3333-444455556666",
      "name": "manifest.json",
      "kind": "manifest",
      "sizeBytes": 4096,
      "contentType": "application/json",
      "checksum": "sha256:c0ffee...",
      "downloadUrl": "/v1/exports/runs/8f3c1d2e-4b5a-6c7d-8e9f-0a1b2c3d4e5f/artifacts/c0ffee00-1111-2222-3333-444455556666/download"
    }
  ]
}

Full artifact metadata (path, checksumAlgorithm default SHA-256, metadata, expiresAt) is available from GET …/artifacts/{artifactId} (ExportArtifactResponse).

SSE events (GET /v1/exports/runs/{runId}/events)

Each event is emitted as event: <type>\ndata: <ExportRunSseEvent JSON>\n\n. The envelope is { eventType, runId, timestamp, data }; data is an ExportRunResponse for lifecycle events and an ExportRunProgress for progress events. The stream polls run state every ~2s while Queued/Running. Event types (ExportRunSseEventTypes):

Additional event-type constants run.phase.started, run.phase.completed, and run.artifact.created are defined but not yet emitted by the API streamer.

Determinism & limits

(forward design — not implemented) The fixed request-body, target-count, bundle-size, and SSE idle-timeout caps in earlier drafts (256 KiB body, 50 targets, 500 MiB bundle, 60-minute job timeout, 60s SSE idle, 1s,2s,4s,8s client backoff) are not enforced as documented constants in the current service. Concurrency and pagination are the enforced limits above.

Error handling

The service maps failures to RFC 7807 problem responses (UseExportCenterProblemExceptionMapping). Observed responses:

(forward design — not implemented) The ERR_EXPORT_* codes in earlier drafts (ERR_EXPORT_PROFILE_NOT_FOUND, ERR_EXPORT_REQUEST_INVALID, ERR_EXPORT_TOO_LARGE, ERR_EXPORT_RATE_LIMIT, ERR_EXPORT_DISTRIBUTION_FAILED, ERR_EXPORT_EXPIRED) are not emitted by the current service; errors are returned as ProblemDetails with localized messages (resource keys such as exportcenter.error.profile_name_conflict).

Persistence (reference)

PostgreSQL schema export_center (Infrastructure/Db/Migrations/001_initial_schema.sql), tenant-isolated via row-level security keyed on app.current_tenant:

A local-only EXPORTCENTER_ALLOW_INMEMORY_REPOSITORIES=true switch boots in-memory repositories for first-run/dev; it is rejected in Production by the runtime posture validator.

Telemetry

Meter StellaOps.ExportCenter (ExportTelemetry). Selected instruments relevant to this surface:

Common tags: tenant / tenant_id, profile / profile_id, type, artifact_kind.

Samples

Outstanding for sign-off