Export Flow
Reconciliation note (re-verified against
src/ExportCenter, 2026-05-31). Export Center is a deterministic evidence-export engine built around tenant-scoped profiles that are run to produce signed, verifiable artifacts (canonical JSON, Trivy DB, mirror bundles, compliance dossiers) — not a Chromium-rendered “PDF/Excel report” service. The flow below is the profile → run → artifact model that the code defines (/v1/exports/*). Sections that describe behavior not present in the codebase are explicitly marked NOT IMPLEMENTED, ORPHANED, or Draft/roadmap.Key implementation-status caveat: the HTTP API (profiles, runs, artifacts, verify, SSE, manifest) and the typed export surfaces (attestation, NIS2 SoA, risk bundle, audit/exception/lineage) are implemented, but the deployed Worker does not execute the generic profile→run phase pipeline — see the “Worker reality” note under Actors. Treat the worker-driven phases (§4) and the diagram’s worker column as the intended design, not live behaviour.
Overview
Export Center generates and delivers deterministic evidence exports: canonical JSON archives, Trivy-compatible vulnerability databases, full/delta mirror bundles, and compliance dossiers (CRA, NIS2, standards mappings). Exports are defined as reusable profiles and executed as runs; every run produces hashed artifacts plus a signed manifest that can be independently verified offline.
The unit of work is a profile (the what/how/where), and a run (a single deterministic execution of a profile). Runs are tenant-scoped, can be triggered ad-hoc, on a cron schedule, by an event, or via API, and converge to identical bundle bytes for identical inputs.
Audience: operators and auditors who configure export profiles and download evidence, and integrators building tools that consume or verify Export Center artifacts.
Business Value: Deterministic, signed, offline-verifiable exports give auditors and downstream tools reproducible evidence without depending on a live StellaOps instance.
Actors
| Actor | Type | Role |
|---|---|---|
| User | Human | Creates/activates profiles, starts runs, downloads artifacts |
| Console | System | UI for profile and run management |
| Gateway / Router | Service | Routes export requests (Stella Router microservice mesh) |
| ExportCenter (WebService) | Service | Profile/run lifecycle, artifact + manifest serving, verification, SSE |
| ExportCenter (Worker) | Service | Background batch jobs (DevPortal offline bundle, risk bundle). See Worker reality note below — the generic profile→run phase pipeline is not driven by a deployed worker. |
| Adapters | Library | Trivy DB / Java DB, Mirror delta, OCI distribution, compliance dossiers |
| EvidenceLocker | Service | Stores export manifest snapshots + distribution transcripts (optional) |
| Distribution targets | Storage | FileSystem, OCI registry, Mirror, OfflineKit, S3/Azure/GCS (see note) |
NOTE —
RustFS,Scanner,Policyas direct flow actors. The original doc listed Scanner/Policy/RustFS as inline data sources queried during the flow. In code the run pipeline resolves a scope into items via theExportScopeResolver/ExportPlanner(Core/Planner/*) and the configured adapters; there is no hard-codedGET /internal/scans/{id}/GET /internal/verdicts/{scan_id}orchestration inExportCenter. RustFS is not referenced anywhere insrc/ExportCenter. Artifact persistence is to the configured distribution targets / local artifact store (IExportArtifactStore), not a fixed RustFS layout.
NOT IMPLEMENTED — worker-driven run execution (the generic phase pipeline). The deployed
StellaOps.ExportCenter.Workerhost registers exactly twoBackgroundServices (Worker/Program.cs):Worker(a one-shot DevPortal offline bundle job,DevPortalOfflineJob) andRiskBundleWorker(a one-shot risk bundle job). Each runs once if enabled, then idles onTask.Delay(Timeout.Infinite). There is no hosted service that polls theExportRuntable and drives a started run throughScopeResolution → … → Finalize. TheExportPlanner/ExportScopeResolver(Core/Planner/*) and theExportPhaseKindmodel exist as building blocks but are not invoked by any deployed run-executor loop. Concretely:POST …/profiles/{id}/runscreates anExportRunalready inRunningstate (ExportApiEndpoints.StartRunFromProfile) but nothing in the deployed services transitions it toCompleted, producesExportArtifacts, signs, or distributes for the generic profile pipeline. The worker-column steps in the flow diagram below and the §4 phase list describe the intended engine, not behaviour wired into the shipped Worker. The artifact/manifest/verify/download read paths (§5–§7) and the typed export surfaces (attestation, NIS2 SoA, risk bundle, audit/exception/lineage — see “Other typed export surfaces”) are separately implemented and do produce output.
Prerequisites
- Caller holds the required export scope (see Authorization below).
- An export profile exists and is in
Activestatus (runs can only start from anActiveprofile;Draft/Paused/Archivedprofiles are rejected). - Source data exists for the profile’s resolved scope.
Authorization (scopes)
Verified against StellaOpsScopes.cs and StellaOpsResourceServerPolicies.cs. Scope claim values are dot-form (no colon-form variant exists for Export Center):
| Scope (claim value) | Policy constant | Grants |
|---|---|---|
export.viewer | ExportViewer | Read-only: list/get profiles, runs, artifacts; download; verify; manifest; attestation status; SSE streams |
export.operator | ExportOperator | Create/update profiles; start runs; cancel runs |
export.admin | ExportAdmin | Archive (soft-delete) profiles; retention/encryption/scheduling administration |
The WebService applies a global fallback policy requiring authentication + export.viewer; write paths additionally require export.operator and archive requires export.admin. Health probes (/healthz, /readyz) are anonymous.
Supported Export Formats
Verified against ExportFormat (Core/Planner/ExportPlanModels.cs) and the profile catalogue (docs/modules/export-center/profiles.md).
| Format enum | Description |
|---|---|
JsonRaw | Canonical raw JSON / JSONL archives (advisories, VEX, SBOMs) |
JsonPolicy | JsonRaw plus policy snapshot + evaluated findings |
Ndjson | Newline-delimited JSON (streaming) |
Csv | CSV |
Mirror | Full mirror layout with indexes (full / delta variants) |
TrivyDb | Trivy vulnerability database (schema v2) |
TrivyJavaDb | Trivy Java DB supplement (Maven/Gradle/SBT) |
Compression is independent (CompressionFormat: None, Gzip, Zstd, Brotli).
NOT IMPLEMENTED — PDF, Excel/
.xlsx, SARIF, CycloneDX/SPDX as Export Center output formats. The original doc’s format table (PDF reports rendered via Chromium, Excel spreadsheets, SARIF,.cdx.json/.spdx.jsondirect outputs) does not correspond to anyExportFormatvalue or adapter insrc/ExportCenter. Export Center emits evidence/data bundles, not human-readable rendered reports. SBOMs are carried as inputs/records (ExportInputKind.Sbom) inside JSON/mirror bundles, not as a standalone CycloneDX/SPDX export format selectable here. (SBOM document generation lives in the SBOM flow — see SBOM Generation Flow.)
Draft/roadmap — compliance dossier “formats”. CRA technical-file / conformity-dossier, NIS2 SoA / effectiveness reports, and standards mappings are real profile variants (see
Adapters.Cra,Adapters.Nis2,Adapters.StandardsMappingandprofiles.md), exposed via dedicated typed endpoints rather than a genericformatselector.
Flow Diagram
The flow is profile-first: a profile is created and activated, then a run is started from that profile. The intended design is that the run is planned (scope resolution), executed by a worker (data fetch → transform → write → manifest → sign → distribute), and progress is streamed over SSE.
Read with the “Worker reality” note above. The ExportCenter Wkr column below (data-fetch → transform → write → manifest → sign → distribute) is the intended engine. In the shipped code the API creates the run in
Runningstate, but no deployed background service executes these phases for the generic profile pipeline, so therun.completed/artifact steps are not driven end-to-end today. The diagram is retained as the target design.
┌──────────────────────────────────────────────────────────────────────────────────┐
│ Export Flow (profile → run → artifact) │
└──────────────────────────────────────────────────────────────────────────────────┘
┌────────┐ ┌─────────┐ ┌──────────────────┐ ┌───────────────────┐ ┌──────────────┐
│ User │ │ Console │ │ ExportCenter API │ │ ExportCenter Wkr │ │ Distribution │
└───┬────┘ └────┬────┘ └────────┬─────────┘ └─────────┬─────────┘ └──────┬───────┘
│ │ │ │ │
│ Create & │ POST /v1/exports/profiles │ │
│ activate │ PUT …/{id} (status=Active) │ │
│ profile │───────────────>│ │ │
│ │ 201 / 200 │ │ │
│<───────────│<───────────────│ │ │
│ │ │ │ │
│ Start run │ POST /v1/exports/profiles/{id}/runs │ │
│───────────>│───────────────>│ │ │
│ │ │ (concurrency check; │ │
│ │ │ Running or Queued) │ │
│ │ 202 Accepted │ Location: │ │
│ │ {runId,status}│ …/runs/{runId} │ │
│<───────────│<───────────────│ │ │
│ │ │ │ │
│ Subscribe │ GET …/runs/{runId}/events (SSE) │ │
│───────────>│───────────────>│ plan: scope-resolve│ │
│ │ event:connected │ │
│ │ event:run.started │ │
│ │ │ data-fetch ────────>│ │
│ │ event:run.progress │ transform/redact │
│ │ │ │ write output │
│ │ │ │ generate manifest │
│ │ │ │ sign (optional) │
│ │ │ │ distribute ──────>│
│ │ event:run.completed │ │
│<───────────│<───────────────│ │ │
│ │ │ │ │
│ Download │ GET …/runs/{runId}/artifacts/{aid}/download │
│───────────>│───────────────>│ (file stream) │ │
│<───────────│<───────────────│ │ │
│ Verify │ POST …/runs/{runId}/verify │ │
│───────────>│───────────────>│ (manifest+sig+hashes)│ │
│<───────────│<───────────────│ │ │
Step-by-Step
1. Create / activate an export profile
Profiles carry the scope, format, signing options, and (optional) cron schedule. A newly created profile starts in Draft; a run can only be started once it is moved to Active (via PUT).
POST /v1/exports/profiles HTTP/1.1
Authorization: Bearer {jwt} # requires export.operator
Content-Type: application/json
{
"name": "weekly-evidence-json",
"description": "Canonical JSON evidence archive",
"kind": "Scheduled",
"scope": {
"targetKinds": ["sbom", "vex", "advisory"],
"namespaces": ["acme/payments"],
"dateRange": { "from": "2026-05-01T00:00:00Z", "to": "2026-05-30T00:00:00Z", "field": "CreatedAt" },
"maxItems": 5000
},
"format": {
"format": "JsonPolicy",
"compression": "Zstd",
"sortKeys": true,
"normalizeTimestamps": true
},
"signing": { "enabled": true, "algorithm": "ES256", "includeProvenance": true },
"schedule": "0 8 * * MON"
}
Tenant identity: the WebService derives the tenant from the
tenant_id(ortid) claim on the JWT, not from anX-Tenant-Idheader. Requests without a resolvable tenant claim return400. (The original doc’sX-Tenant-Id: acme-corpheader is not how tenancy is resolved here.)
ExportProfileKind values: AdHoc, Scheduled, EventDriven, Continuous. ExportProfileStatus values: Draft, Active, Paused, Archived.
2. Start a run from the profile
POST /v1/exports/profiles/{profileId}/runs HTTP/1.1
Authorization: Bearer {jwt} # requires export.operator
Content-Type: application/json
{
"scopeOverride": null,
"formatOverride": null,
"correlationId": "trace-abc123",
"dryRun": false
}
The API enforces per-tenant and per-profile concurrency limits (ExportConcurrencyOptions: default MaxConcurrentRunsPerTenant=4, MaxConcurrentRunsPerProfile=2, QueueExcessRuns=true, MaxQueueSizePerTenant=10). If limits are exceeded and queuing is disabled, the API returns 429 Too Many Requests.
On success it returns 202 Acceptedwith a Location header pointing at the run resource, and a body whose status is Running (or Queued if it had to be queued):
{
"runId": "9f1c…",
"profileId": "3a2b…",
"tenantId": "11…",
"status": "Running",
"trigger": "Api",
"correlationId": "trace-abc123",
"initiatedBy": "user:alice",
"progress": { "totalItems": 0, "processedItems": 0, "failedItems": 0, "totalSizeBytes": 0, "percentComplete": 0 },
"createdAt": "2026-05-30T10:30:00Z",
"startedAt": "2026-05-30T10:30:00Z",
"expiresAt": "2026-06-06T10:30:00Z"
}
ExportRunStatus: Queued, Running, Completed, PartiallyCompleted, Failed, Cancelled. ExportRunTrigger: Manual, Scheduled, Event, Api. Runs default to a 7-day expiresAt (now.AddDays(7)). The API-created run is written directly in Running (or Queued) state with empty progress; the Trigger for runs started through POST …/runs is always Api.
Caveat (see “Worker reality”). The run is created
Running, but for the generic profile pipeline no deployed worker subsequently advances it toCompleted/Failedor produces artifacts. The status/progress fields and the §3–§4 planning model exist, but the end-to-end run engine is not wired in the shipped Worker host today.
3. Scope resolution & planning
The planner (Core/Planner/ExportPlanner.cs, ExportScopeResolver.cs) turns the profile’s ExportScope into a concrete set of ResolvedExportItems and an ExportPlan of ordered phases. ExportScope filters include: targetKinds, sourceRefs, tags, namespaces, dateRange, runIds, excludePatterns, maxItems, and deterministic sampling (None/Random/First/Last/Stratified/Systematic).
Each input is tracked as an ExportInput with ExportInputKind (Sbom, Vex, Attestation, ScanReport, PolicyResult, Evidence, RiskBundle, Advisory) and an ExportInputStatus (Pending/Processing/Processed/Failed/Skipped).
NOT IMPLEMENTED (as a fixed data-gather contract): the original “Data Gathering” table with
GET /internal/scans/{id},GET /internal/verdicts/{scan_id},GET /internal/vex/applied/{scan_id},GET /internal/sboms/{digest}. No such fixed internal-call sequence exists insrc/ExportCenter; data is resolved via the scope resolver and adapters.
4. Execution phases
ExportPhaseKind (Core/Planner/ExportPlanModels.cs) defines the intended worker pipeline order. The ExportPlanner builds an ExportPlan of ExportPlanPhases in this order, but — per the “Worker reality” note above — no deployed background service currently executes the plan for the generic profile pipeline, so treat this list as the enumerated design, not a live runtime sequence:
1. ScopeResolution – resolve scope and collect items
2. DataFetch – fetch and transform source data
3. Transform – normalization / redaction
4. WriteOutput – write output files (JSON/Trivy/Mirror/…)
5. GenerateManifest – compute checksums + manifest
6. Sign – sign artifacts (when signing.enabled)
7. Distribute – push to distribution targets
8. Verify – verify distribution
9. ApplyRetention – apply retention policy
10. Finalize – cleanup / finalization
NOT IMPLEMENTED — Chromium PDF rendering. The original
chromium.pdf({...})snippet and “PDF generation uses Chromium for high-fidelity rendering” claim have no counterpart insrc/ExportCenter. Removed.
Draft/roadmap — named report templates.
compliance-executive,compliance-detailed,audit-evidence,developer-sarif,custom-{tenant}are not a configurable template registry in code. Profiles select a format and (for compliance) a typed adapter/variant; there is no free-form template name field beyond the profile configuration.
5. Artifacts & manifest
Each run produces one or more artifacts (ExportArtifact, Core/Persistence/IExportRunRepository.cs) with a Kind (free-form string), a content-addressed Checksum, a ChecksumAlgorithm string that defaults to "SHA-256" (it is a plain string field, not an enum), size, content type, and metadata. List/fetch/download:
GET /v1/exports/runs/{runId}/artifacts # list
GET /v1/exports/runs/{runId}/artifacts/{artifactId} # metadata
GET /v1/exports/runs/{runId}/artifacts/{artifactId}/download # file stream
Download streams the file directly (FileStreamHttpResult) with the artifact’s content type and filename, and emits an audit event + download metric. The run manifest is available separately:
GET /v1/exports/runs/{runId}/verify/manifest # manifest content + digest
NOT IMPLEMENTED — fixed RustFS path layout & signed download URLs. The original
blobs/exports/{tenant}/{yyyy}/{mm}/{exp-id}/…RustFS tree and theGET …/download → { "download_url": "https://storage…?sig=…" }signed-URL response are not what the code does. Download is a direct authenticated file stream from the artifact store; there is no signed-URL indirection and no hard-coded RustFS directory contract insrc/ExportCenter.
6. Evidence locker integration (optional)
When configured, Export Center pushes a manifest snapshot to the Evidence Locker (IExportEvidenceLockerClient.PushSnapshotAsync) and later records a distribution transcript (UpdateDistributionTranscriptAsync). The locker returns a bundle id, a Merkle root hash, and an optional DSSE signature; Export Center can re-verify the root hash (VerifyRootHashAsync).
// shape returned by PushSnapshotAsync (ExportEvidenceSnapshotResult)
{
"bundleId": "bnd-…",
"rootHash": "sha256:…",
"signature": { /* ExportDsseSignatureInfo: keyId, algorithm, signature, … */ } // optional
}
The original “Evidence Sealing” JSON (
bnd-789ghi,merkle_root, base64signature, fixedcontentsarray) was illustrative; the real contract is theIExportEvidenceLockerClientinterface above. Detailed evidence packaging is covered in Evidence Bundle Export Flow.
7. Verification
Export Center can verify a completed run’s manifest, signatures, and content hashes — including optional Rekor transparency-log checks — without a live data source:
POST /v1/exports/runs/{runId}/verify HTTP/1.1
Authorization: Bearer {jwt} # requires export.viewer
Content-Type: application/json
{
"verifyHashes": true,
"verifySignatures": true,
"verifyManifest": true,
"verifyEncryption": true,
"checkRekor": false,
"trustedKeys": []
}
Returns an ExportVerificationResponse with isValid, per-file hash results, manifest and signature results (algorithm, keyId, signer, optional Rekor index), errors, and warnings. A streaming variant is available at POST /v1/exports/runs/{runId}/verify/stream (SSE), and attestation status at GET /v1/exports/runs/{runId}/verify/attestation.
8. Distribution & delivery
Distribution is driven by ExportDistributionKind:
| Kind | Notes |
|---|---|
FileSystem | Local filesystem |
Mirror | Mirror-server distribution (full/delta) |
OfflineKit | Air-gap offline-kit distribution |
OciRegistry | OCI artifact push (feature-flagged; disable for air-gap) |
Webhook | Notification (metadata only) |
AmazonS3 / AzureBlob / GoogleCloudStorage | Object-store targets |
On-prem / air-gap posture. Per the module charter, OCI distribution must be disable-able for air-gap and tests must not reach the network. The cloud object-store kinds (
AmazonS3,AzureBlob,GoogleCloudStorage) are opt-in distribution targets a tenant may configure; they are not defaults and Export Center stores only references/transcripts, never managing those services itself. Offline/file-system/mirror/OCI-to-a-local-registry are the first-class paths.
Draft/roadmap — email delivery & per-recipient delivery config. Delivery by email attachment/link is not implemented as an Export Center distribution kind (
Webhookcarries metadata only; there is no
Export “Types” → profile kinds + input kinds
The original “Export Types” table (scan_report, sbom, vulnerability_report, policy_compliance, ai_code_guard_report, evidence_bundle, audit_log, …) does not map to a single type field on the request. In code:
- A profile has a
kind(AdHoc/Scheduled/EventDriven/Continuous) and a format (JsonRaw/JsonPolicy/Ndjson/Csv/Mirror/TrivyDb/TrivyJavaDb). - The content is expressed through
ExportScope.targetKindsandExportInputKind(Sbom,Vex,Attestation,ScanReport,PolicyResult,Evidence,RiskBundle,Advisory). - Compliance dossiers (CRA, NIS2 SoA/effectiveness, standards mapping) and audit/exception/lineage/risk bundles are produced by dedicated typed endpoints (see “Other typed export surfaces” below), not by a generic
typeenum.
ORPHANED —
ai_code_guard_reportexport type. No such export type/profile exists insrc/ExportCenter. Treat as NOT IMPLEMENTED.
Data Contracts
The original ExportRequest/ExportResponse TypeScript interfaces do not match the code. The actual contracts are (Api/ExportApiModels.cs):
Create profile (CreateExportProfileRequest)
interface CreateExportProfileRequest {
name: string; // required, unique per tenant
description?: string;
kind?: "AdHoc" | "Scheduled" | "EventDriven" | "Continuous"; // default AdHoc
scope?: ExportScope;
format?: ExportFormatOptions;
signing?: ExportSigningOptions;
schedule?: string; // cron
}
interface ExportFormatOptions {
format?: "JsonRaw" | "JsonPolicy" | "Ndjson" | "Csv" | "Mirror" | "TrivyDb" | "TrivyJavaDb";
compression?: "None" | "Gzip" | "Zstd" | "Brotli";
includeMetadata?: boolean; // default true
prettyPrint?: boolean;
redactFields?: string[];
normalizeTimestamps?: boolean; // default true
sortKeys?: boolean; // default true
}
interface ExportSigningOptions {
enabled?: boolean;
algorithm?: string; // default "ES256"
keyId?: string;
providerHint?: string;
includeProvenance?: boolean; // default true
}
Start run (StartExportRunRequest)
interface StartExportRunRequest {
scopeOverride?: ExportScope;
formatOverride?: ExportFormatOptions;
correlationId?: string;
dryRun?: boolean;
}
Run response (ExportRunResponse)
interface ExportRunResponse {
runId: string;
profileId: string;
tenantId: string;
status: "Queued" | "Running" | "Completed" | "PartiallyCompleted" | "Failed" | "Cancelled";
trigger: "Manual" | "Scheduled" | "Event" | "Api";
correlationId?: string;
initiatedBy?: string;
progress: { totalItems: number; processedItems: number; failedItems: number; totalSizeBytes: number; percentComplete: number };
error?: { code: string; message: string; details?: Record<string,string> };
createdAt: string;
startedAt?: string;
completedAt?: string;
expiresAt?: string;
artifacts?: Array<{ artifactId: string; name: string; kind: string; sizeBytes: number; contentType?: string; checksum?: string; downloadUrl?: string }>;
}
Endpoint reference (/v1/exports/*)
Verified against Api/ExportApiEndpoints.cs. The in-service route prefix is /v1/exports(a gateway may front it under /api). The legacy GET/POST/DELETE /exports endpoints are deprecated and return 410 Gone(Program.cs).
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /v1/exports/profiles | viewer | List profiles (filter by status/kind/search; paginated) |
| GET | /v1/exports/profiles/{profileId} | viewer | Get profile |
| POST | /v1/exports/profiles | operator | Create profile (201 Created) |
| PUT | /v1/exports/profiles/{profileId} | operator | Update profile |
| DELETE | /v1/exports/profiles/{profileId} | admin | Archive (soft-delete) profile (204) |
| POST | /v1/exports/profiles/{profileId}/runs | operator | Start run (202 Accepted; 429 if over limits) |
| GET | /v1/exports/runs | viewer | List runs (filter by profile/status/trigger/date/correlation) |
| GET | /v1/exports/runs/{runId} | viewer | Get run (+artifacts) |
| POST | /v1/exports/runs/{runId}/cancel | operator | Cancel queued/running run |
| GET | /v1/exports/runs/{runId}/artifacts | viewer | List artifacts |
| GET | /v1/exports/runs/{runId}/artifacts/{artifactId} | viewer | Artifact metadata |
| GET | /v1/exports/runs/{runId}/artifacts/{artifactId}/download | viewer | Download (file stream) |
| GET | /v1/exports/runs/{runId}/events | viewer | SSE run events |
| POST | /v1/exports/runs/{runId}/verify | viewer | Verify run |
| GET | /v1/exports/runs/{runId}/verify/manifest | viewer | Get manifest |
| GET | /v1/exports/runs/{runId}/verify/attestation | viewer | Attestation status |
| POST | /v1/exports/runs/{runId}/verify/stream | viewer | Stream verification (SSE) |
Other typed export surfaces (same service, separate endpoint groups)
Registered in Program.cs alongside the core API; covered in depth by the module docs (docs/modules/export-center/*):
- Attestation & promotion attestation (
MapAttestationEndpoints,MapPromotionAttestationEndpoints) - Risk bundles (
MapRiskBundleEndpoints) - Simulation export (
MapSimulationExportEndpoints) - Audit bundle (
MapAuditBundleEndpoints) - Exception report (
MapExceptionReportEndpoints) - Lineage export (
MapLineageExportEndpoints) - Assurance / compliance exports — CRA, NIS2 SoA (
MapAssuranceExportEndpoints,MapNis2SoaExportEndpoints; e.g.GET /v1/exports/nis2/soa/profile,POST /v1/exports/nis2/soa/runs) - Incident management (
MapIncidentEndpoints)
SSE run events
StreamRunEvents (GET /v1/exports/runs/{runId}/events) emits event: <type>\ndata: <json>\n\n. The ExportRunSseEventTypes constant class declares: run.started, run.progress, run.phase.started, run.phase.completed, run.artifact.created, run.completed, run.failed, run.cancelled.
Actually emitted by the handler (it polls the run row every 2s): the framing events connected and disconnected, plus run.progress (on ProcessedItems change) and a status-change event mapped to run.started (→Running), run.completed (→Completed/PartiallyCompleted), run.failed (→Failed), or run.cancelled (→Cancelled).
ORPHANED —
run.phase.started,run.phase.completed,run.artifact.created. These three constants are declared but never emitted anywhere insrc/ExportCenter(only defined inExportApiModels.cs; not referenced byStreamRunEventsor any worker). They are unwired, awaiting the worker engine described in the “Worker reality” note. NOT IMPLEMENTED as emitted events today.
The original doc’s single
WebSocket: export readymessage is replaced by the typed SSE event stream above. Export Center uses Server-Sent Events, not WebSockets.
Scheduled Exports
Scheduling is a profile-level concern: a profile carries a single cron schedule string and a kind of Scheduled. Scheduled runs are triggered with ExportRunTrigger.Scheduled.
// the schedule lives on the profile, not in a separate "schedules:" config block
{
"name": "weekly-evidence-json",
"kind": "Scheduled",
"schedule": "0 8 * * MON",
"format": { "format": "JsonPolicy" },
"scope": { "targetKinds": ["sbom", "vex", "advisory"] }
}
Draft/roadmap — declarative
schedules:/delivery:YAML block. The original multi-schedule YAML with relativedate_range(-7d/now),delivery.method: email, and templated subjects is not the implemented model. A profile has one cron string; relative date-range tokens and email delivery are not implemented (see formats/delivery notes above).
Error Handling
| Condition | Behavior (verified) |
|---|---|
| Tenant claim missing | 400 BadRequest (tenant_not_found_in_claims) |
| Profile/run/artifact not found (or cross-tenant) | 404 NotFound |
Start run on non-Active profile | 400 BadRequest (profile_not_active) |
| Duplicate profile name | 409 Conflict (profile_name_conflict) |
| Update an archived profile | 400 BadRequest (profile_archived) |
| Cancel run in non-cancellable state | 400 BadRequest (run_cancel_invalid_state) |
| Over concurrency / queue limits | 429 Too Many Requests (audited) |
| Run failure | The ExportRun model supports Failed/PartiallyCompleted with an ErrorJson (surfaced as error.code/message/details). For the generic profile pipeline these transitions are not driven today — no deployed worker advances or fails a started run (see the “Worker reality” note). The status enum + error contract are in place for when the engine is wired. |
The original “Recovery” column (fall back to default template, simplify report and retry, retry with exponential backoff, queue-for-retry email) describes behavior not visible at the API layer and not implemented: there is no template registry, no PDF path, and (per the “Worker reality” note) no deployed run-executor that could perform worker-side retry/idempotency or retention pruning for the generic profile pipeline. Those remain roadmap.
Observability
Metrics
The meter is StellaOps.ExportCenter (ExportTelemetry.Meter, version 1.0.0); every instrument name is prefixed export_ (WebService/Telemetry/ExportTelemetry.cs). The four instruments the core profile/run/artifact/SSE API actually increments at runtime — with the exact tags the handlers pass — are:
| Instrument name | C# field | Type | Tags emitted by the API |
|---|---|---|---|
export_runs_total | ExportRunsTotal | Counter | tenant_id, profile_id |
export_runs_in_progress | ExportRunsInProgress | UpDownCounter | tenant_id |
export_artifact_downloads_total | ArtifactDownloadsTotal | Counter | tenant_id, artifact_kind |
export_sse_connections_total | SseConnectionsTotal | Counter | tenant_id |
ExportTelemetry also declares many more instruments for the typed surfaces and the (not-yet-wired) worker engine — e.g. counters export_runs_success_total, export_runs_failed_total, export_artifacts_total, export_bytes_total, export_concurrency_limit_exceeded_total, export_audit_events_total, the export_incidents_*/export_risk_bundle_jobs_*/export_timeline_events_* families, and histograms export_run_duration_seconds, export_plan_duration_seconds, export_bundle_size_bytes — see docs/modules/export-center/architecture.md (Observability) for the full list.
Corrected. The original metric names (
export_requests_total,export_duration_seconds,export_size_bytes,export_failures_totalwithtype/format/reasonlabels) are NOT the actual instrument names — treat them as NOT IMPLEMENTED. A prior reconciliation note also claimed “the canonical tag set istenant/profile/adapter/result”; that is not what the code emits — the core API handlers tag withtenant_id/profile_id/artifact_kind(singular dimensions above). Theadapter/result/phasedimensions appear in structured log fields, not as tags on these counters.
Trace / phase context
Phases map to ExportPhaseKind (see §4): ScopeResolution → DataFetch → Transform → WriteOutput → GenerateManifest → Sign → Distribute → Verify → ApplyRetention → Finalize.
Related Flows
- SBOM Generation Flow - Source of SBOM records
- Policy Evaluation Flow - Source of policy snapshots/findings
- Evidence Bundle Export Flow - Detailed evidence packaging
- Dashboard Data Flow - Data aggregation patterns
Module references (source of truth)
- API:
src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService/Api/ExportApiEndpoints.cs,ExportApiModels.cs - Domain:
…/StellaOps.ExportCenter.Core/Domain/{ExportProfile,ExportRun,ExportInput,ExportDistribution}.cs - Planner / formats / scope:
…/StellaOps.ExportCenter.Core/Planner/{ExportPlanModels,ExportScopeModels}.cs - Scopes:
src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs(ExportViewer/ExportOperator/ExportAdmin) - Module dossiers:
docs/modules/export-center/{architecture,profiles,provenance-and-signing,trivy-adapter,mirror-bundles}.md
