OfflineKit Export Gateway Contract

Currency check 2026-10-07: owner paths/source placement verified against 3238bd511a37e199ae99d4f17be2f13eabec8dd7. The supported boundary is /api/offlinekit/v1/exports (src/OfflineKit/StellaOps.OfflineKit.WebService/Program.cs). Re-verify the host mount and owning endpoint extension together. This is source verification; deployment and broader audit coverage are separate evidence. Other dated findings retain their stated dates. Re-verify: rg -n "MapGroup|Map.*Endpoints|AddHostedService" src/OfflineKit/StellaOps.OfflineKit.WebService/Program.cs, then inspect the endpoint extension or library pointer cited at the operation.

Supported implementation

The export application is hosted by offlinekit-web, with background profile-run execution in offlinekit-worker. The supported base is /api/offlinekit/v1/exports; the application groups are mounted below /api/offlinekit by src/OfflineKit/StellaOps.OfflineKit.WebService/Program.cs. The retained StellaOps.ExportCenter.* library and DTO names identify application code, rather than a separate deployable.

OperationCurrent route
List/create profilesGET/POST /api/offlinekit/v1/exports/profiles
Read/update/archive profileGET/PUT/DELETE /api/offlinekit/v1/exports/profiles/{profileId}
Submit a profile runPOST /api/offlinekit/v1/exports/profiles/{profileId}/runs
List/read/cancel runsGET /api/offlinekit/v1/exports/runs, GET /api/offlinekit/v1/exports/runs/{runId}, POST /api/offlinekit/v1/exports/runs/{runId}/cancel
Run eventsGET /api/offlinekit/v1/exports/runs/{runId}/events
Artifact metadata/downloadGET /api/offlinekit/v1/exports/runs/{runId}/artifacts, GET /api/offlinekit/v1/exports/runs/{runId}/artifacts/{artifactId}, GET /api/offlinekit/v1/exports/runs/{runId}/artifacts/{artifactId}/download
Carrier descriptorGET /api/offlinekit/v1/exports/runs/{runId}/carrier
VerificationPOST /api/offlinekit/v1/exports/runs/{runId}/verify, GET /api/offlinekit/v1/exports/runs/{runId}/verify/manifest, GET /api/offlinekit/v1/exports/runs/{runId}/verify/attestation, POST /api/offlinekit/v1/exports/runs/{runId}/verify/stream
Assurance discovery/readinessGET /api/offlinekit/v1/exports/assurance/profiles, GET /api/offlinekit/v1/exports/assurance/profiles/{profileId}, GET /api/offlinekit/v1/exports/assurance/profiles/{profileId}/readiness
Assurance production/readback/downloadPOST /api/offlinekit/v1/exports/assurance/profiles/{profileId}/runs, GET /api/offlinekit/v1/exports/assurance/profiles/{profileId}/runs/{runId}, GET /api/offlinekit/v1/exports/assurance/runs/{runId}/bundle
NIS2 SoAGET /api/offlinekit/v1/exports/nis2/soa/profile, POST /api/offlinekit/v1/exports/nis2/soa/runs, GET /api/offlinekit/v1/exports/nis2/soa/runs/{runId}, GET /api/offlinekit/v1/exports/nis2/soa/runs/{runId}/bundle

Ordinary profiles and runs use GUID identifiers. Assurance profile identifiers are the named catalogue ids, such as cra.technical-file and dora.roi. Endpoint extensions and DTOs are in src/OfflineKit/__Libraries/StellaOps.ExportCenter.Application/Api/, AssuranceExports/ and Nis2Soa/. The ordinary export, Assurance and typed SoA groups apply export.viewer to every route. Writes/run submission add export.operator; ordinary profile archive adds export.admin. Inherited, route-specific and configured resource-server default scopes apply together; a mutation scope does not substitute for Viewer.

The web host authenticates the Router identity envelope before resource-server authentication. Export requests pass through tenant middleware backed by the local tenants catalogue replica. A disabled required replica is an error, not proof that a tenant does not exist. The custody/import plane is excluded from that middleware and resolves its authenticated tenant itself.

An active profile submission returns 202 Accepted with a Queued run and a current-prefix status URL. ExportRunWorker leases queued work only when Export:Runs:Worker:Enabled is enabled on the worker. The executor writes declared inputs or a mirror bundle, records artifacts through distributions, then records completion/failure. The web role does not execute ordinary queued profile runs. Declared local inputs are restricted by Export:Runs:DeclaredInputRoots; mirror sources are absolute local paths or file:// URIs. This is implemented execution.

Carrier production uses Export:Carrier:SigningKeyId and the configured crypto registry (CryptoRegistryCarrierSigner). Operator-mounted material is registered through the configured key; there is no carrier-signing Signer HTTP hop. Without a carrier key id, a run can complete without a carrier and records EXPORT_RUN_CARRIER_NOT_WRITTEN; carrier-writing failure fails the run. The descriptor lists the five carrier files and their artifact download URLs.

Both roles use their family-owned stellaops_offlinekit database. Startup registration and the embedded chain under src/OfflineKit/__Libraries/StellaOps.OfflineKit.Persistence/Migrations/ own schema convergence; the connection is STELLAOPS_POSTGRES_OFFLINEKIT_CONNECTION (configuration override Postgres:OfflineKit:ConnectionString).

Operator references

Historical ExportCenter gateway contract reconciled 2026-05-30 (superseded 2026-09-14)

The remainder records the predecessor implementation or unlanded design at the dates stated in its text. All routes, host names, source paths, DTOs, metrics and examples in this section are historical comparison, including sentences that originally said “current” or “today”. They are not instructions for the consolidated estate. The supported section and owner source above supersede them. Do not rebuild or deploy a predecessor from these examples.

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). Router publishes those service paths directly; the Console resolves API_PATH_PREFIXES.exportCenter to /v1/exports without a static gateway rewrite. This describes the RAR-6 source package; the deployed Console continues to use the old aliases until that build is deployed and verified. 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 published service base (/v1/exports). 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 (and further mirror/standards formats). TrivyDb/TrivyJavaDb still appear in the schema enum but are RETIRED 2026-08-09 (owner ruling: Stella Ops does not support trivy-db) — they are retained only so historical persisted plans deserialize, and no adapter declares either format in its SupportedFormats, so nothing can produce them. (Not “fails closed at adapter resolution” — verified 2026-08-14, GetAdapterForFormat has no production call site, only test ones.) Use the CLI-owned stella vuln-db export --from-hub <url> path for offline vulnerability feeds. 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