From e49236f6302341be80b4523a7ef19098223c4867 Mon Sep 17 00:00:00 2001 From: master <> Date: Tue, 10 Mar 2026 16:37:59 +0200 Subject: [PATCH] Stabilize web context propagation and header constants --- ...obal_context_propagation_header_cleanup.md | 410 ++++++++++++++++++ src/Web/StellaOps.Web/src/app/app.config.ts | 6 +- .../src/app/core/api/abac-overlay.client.ts | 5 +- .../src/app/core/api/advisories.client.ts | 11 +- .../src/app/core/api/advisory-ai.client.ts | 7 +- .../src/app/core/api/ai-runs.client.ts | 7 +- .../src/app/core/api/analytics.client.ts | 7 +- .../src/app/core/api/aoc.client.ts | 11 +- .../app/core/api/attestation-chain.client.ts | 6 +- .../src/app/core/api/audit-bundles.client.ts | 9 +- .../app/core/api/authority-admin.client.ts | 3 +- .../app/core/api/authority-console.client.ts | 5 +- .../src/app/core/api/console-export.client.ts | 7 +- .../src/app/core/api/console-search.client.ts | 9 +- .../src/app/core/api/console-status.client.ts | 7 +- .../src/app/core/api/console-vex.client.ts | 7 +- .../src/app/core/api/console-vuln.client.ts | 7 +- .../src/app/core/api/cvss.client.ts | 3 +- .../src/app/core/api/evidence-pack.client.ts | 7 +- .../src/app/core/api/evidence.client.ts | 4 +- .../src/app/core/api/exception.client.ts | 10 +- .../src/app/core/api/export-center.client.ts | 7 +- .../app/core/api/findings-ledger.client.ts | 9 +- .../src/app/core/api/first-signal.client.ts | 11 +- .../core/api/gateway-observability.client.ts | 7 +- .../app/core/api/gateway-openapi.client.ts | 7 +- .../src/app/core/api/graph-platform.client.ts | 7 +- .../app/core/api/identity-provider.client.ts | 7 +- .../app/core/api/jobengine-control.client.ts | 13 +- .../src/app/core/api/jobengine-jobs.client.ts | 11 +- .../src/app/core/api/jobengine.client.ts | 11 +- .../src/app/core/api/noise-gating.client.ts | 7 +- .../src/app/core/api/notify.client.ts | 9 +- .../src/app/core/api/policy-engine.client.ts | 7 +- .../app/core/api/policy-exceptions.client.ts | 10 +- .../src/app/core/api/policy-gates.client.ts | 7 +- .../app/core/api/policy-governance.client.ts | 12 +- .../app/core/api/policy-registry.client.ts | 7 +- .../app/core/api/policy-simulation.client.ts | 7 +- .../app/core/api/policy-streaming.client.ts | 5 +- .../src/app/core/api/risk-http.client.ts | 9 +- .../src/app/core/api/scheduler.client.ts | 4 +- .../app/core/api/security-findings.client.ts | 4 +- .../app/core/api/security-overview.client.ts | 4 +- .../app/core/api/triage-evidence.client.ts | 6 +- .../src/app/core/api/vex-consensus.client.ts | 9 +- .../src/app/core/api/vex-decisions.client.ts | 9 +- .../src/app/core/api/vex-evidence.client.ts | 11 +- .../src/app/core/api/vex-hub.client.ts | 7 +- .../app/core/api/vulnerability-http.client.ts | 11 +- .../app/core/auth/auth-http.interceptor.ts | 3 +- .../StellaOps.Web/src/app/core/auth/index.ts | 5 +- .../core/auth/tenant-http.interceptor.spec.ts | 37 +- .../app/core/auth/tenant-http.interceptor.ts | 77 +--- .../global-context-http.interceptor.ts | 54 +-- .../openapi-context-param-map.service.spec.ts | 80 ++++ .../openapi-context-param-map.service.ts | 197 +++++++++ .../src/app/core/http/stella-ops-headers.ts | 35 ++ .../operator-metadata.interceptor.ts | 18 +- .../global-context-http.interceptor.spec.ts | 22 +- .../services/evidence-ribbon.service.ts | 6 +- .../sbom-diff/services/sbom-diff.service.ts | 6 +- .../security-risk/advisory-sources.api.ts | 5 +- .../symbol-sources/symbol-sources.api.ts | 5 +- .../services/vex-timeline.service.ts | 6 +- 65 files changed, 1058 insertions(+), 281 deletions(-) create mode 100644 docs/implplan/SPRINT_20260310_026_Platform_global_context_propagation_header_cleanup.md create mode 100644 src/Web/StellaOps.Web/src/app/core/context/openapi-context-param-map.service.spec.ts create mode 100644 src/Web/StellaOps.Web/src/app/core/context/openapi-context-param-map.service.ts create mode 100644 src/Web/StellaOps.Web/src/app/core/http/stella-ops-headers.ts diff --git a/docs/implplan/SPRINT_20260310_026_Platform_global_context_propagation_header_cleanup.md b/docs/implplan/SPRINT_20260310_026_Platform_global_context_propagation_header_cleanup.md new file mode 100644 index 000000000..40018d917 --- /dev/null +++ b/docs/implplan/SPRINT_20260310_026_Platform_global_context_propagation_header_cleanup.md @@ -0,0 +1,410 @@ +# Sprint 026 — Global Context Propagation & Header Naming Cleanup + +## Topic & Scope + +- **Goal**: Replace the hardcoded Pack 2.2 route list in `GlobalContextHttpInterceptor` with an OpenAPI-driven parameter map, so context (region, environment, timeWindow, stage) is injected as query params precisely where each endpoint declares support. +- **Goal**: Unify all HTTP header naming to a single canonical prefix `X-Stella-Ops-*`, removing legacy `X-Stella-*`, `X-StellaOps-*`, `X-Tenant-Id`, `X-Scopes`, `X-Project-Id` traces. +- **Why now**: The topbar context controls (tenant, region, env, window, stage) are now global singletons. The propagation layer must match — precise, automatic, and consistently named. +- Working directory: repo root (cross-module sprint). +- Explicitly allows cross-module edits in: `src/Web/`, `src/Router/`, `src/Platform/`, `src/Authority/`, `src/Scanner/`, `src/Policy/`, `src/Graph/`, `src/Concelier/`, `src/Findings/`, `src/JobEngine/`, `src/Notifier/`, `src/Telemetry/`, `src/EvidenceLocker/`, `src/AdvisoryAI/`, `src/BinaryIndex/`, `src/Replay/`. +- Expected evidence: interceptor tests, OpenAPI spec diff showing params, grep confirming zero legacy header references. + +## Dependencies & Concurrency + +- No upstream sprint dependencies. +- Phase 1 (OpenAPI param declarations) and Phase 3 (header cleanup) can run in parallel. +- Phase 2 (interceptor rewrite) depends on Phase 1 completion. +- Phase 3 is independent of Phase 1 and Phase 2 — pure rename. +- Header cleanup (Phase 3) should be done in a single atomic commit per module to avoid partial renames. + +## Documentation Prerequisites + +- Read before starting: + - `docs/modules/platform/architecture-overview.md` (context propagation) + - `src/Router/__Libraries/StellaOps.Router.Gateway/OpenApi/OpenApiDocumentGenerator.cs` (aggregation logic) + - `src/Web/StellaOps.Web/src/app/core/context/global-context-http.interceptor.ts` (current interceptor) + - `src/Web/StellaOps.Web/src/app/core/auth/tenant-http.interceptor.ts` (current tenant headers) + - `src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsHttpHeaderNames.cs` (master constants) + +--- + +## Current State (research findings) + +### Context propagation today + +1. **`TenantHttpInterceptor`** (Angular) — sends `X-StellaOps-Tenant`, `X-Stella-Tenant`, `X-Tenant-Id` on all authenticated requests. Already works but emits 3 names for the same value. +2. **`GlobalContextHttpInterceptor`** (Angular) — injects `regions`, `environments`, `timeWindow` as **query parameters**, but only on 6 hardcoded `/api/v2/*` route prefixes. Does not inject `stage`. +3. **Gateway** — strips reserved identity headers, re-writes from JWT claims. Copies all other headers downstream. Query params pass through unchanged. +4. **Backend services** — read context from query params via `[AsParameters]` binding (e.g., `TopologyQuery`, `SecurityFindingsQuery` records). No services read context from headers. + +### OpenAPI spec today + +- Gateway serves aggregated spec at `/openapi.json` and `/.well-known/openapi` (1899 paths, 2190 operations). +- Angular already has `GatewayOpenApiHttpClient` that fetches the spec with ETag caching. +- **Critical gap**: The spec declares **zero query parameters**. Services use `[AsParameters]` records but neither the built-in `MapOpenApi()` nor the custom `OpenApiDiscoveryDocumentProvider` emit parameter metadata for them. + +### Header naming today + +| Legacy name | Current canonical | Proposed canonical | Files affected | +|---|---|---|---| +| `X-Stella-Tenant` | `X-StellaOps-Tenant` | `X-Stella-Ops-Tenant` | ~64 | +| `X-Tenant-Id` | `X-StellaOps-Tenant` | `X-Stella-Ops-Tenant` | ~40 | +| `X-Scopes` | `X-StellaOps-Scopes` | `X-Stella-Ops-Scopes` | ~30 | +| `X-Stella-Project` | `X-StellaOps-Project` | `X-Stella-Ops-Project` | ~10 | +| `X-Project-Id` | `X-StellaOps-Project` | `X-Stella-Ops-Project` | ~2 | +| `X-StellaOps-Actor` | — | `X-Stella-Ops-Actor` | ~80 | +| `X-StellaOps-Client` | — | `X-Stella-Ops-Client` | ~40 | +| `X-StellaOps-Identity-Envelope` | — | `X-Stella-Ops-Identity-Envelope` | ~5 | +| `X-Stella-Trace-Id` | — | `X-Stella-Ops-Trace-Id` | ~20 | +| `X-Stella-Request-Id` | — | `X-Stella-Ops-Request-Id` | ~10 | + +**Total scope**: ~300 header string references across ~150 files. + +--- + +## Delivery Tracker + +### TASK-026-01 — Emit query parameter declarations in service OpenAPI specs +Status: DOING +Dependency: none +Owners: Developer (Backend) +Task description: + +Backend services use `[AsParameters]` records (e.g., `TopologyQuery`, `SecurityFindingsQuery`) to bind query parameters, but neither the built-in ASP.NET `MapOpenApi()` nor the custom `OpenApiDiscoveryDocumentProvider` implementations emit `parameters` entries in the OpenAPI spec. + +Fix the OpenAPI generation so that query parameters from `[AsParameters]` records appear in the generated spec. Two approaches (pick one): + +**Option A (preferred)**: Configure ASP.NET Core's built-in OpenAPI to recognize `[AsParameters]`. In .NET 9, `Microsoft.AspNetCore.OpenApi` should auto-detect record properties bound from query strings. Verify this works for services using `AddOpenApi()` + `MapOpenApi()`. If not, add `[FromQuery]` attributes to the record properties. + +**Option B**: For services using custom `OpenApiDiscoveryDocumentProvider`, extend the provider to introspect endpoint parameter metadata and emit `parameters` arrays with `in: "query"` entries. + +Target context parameter names to ensure are declared where endpoints accept them: +- `region` / `regions` +- `environment` / `environments` +- `timeWindow` +- `stage` +- `tenant` / `tenantId` + +Completion criteria: +- [ ] `/openapi.json` from gateway includes `parameters` arrays with `in: "query"` entries for endpoints that accept context params +- [ ] At least `TopologyReadModelEndpoints`, `SecurityReadModelEndpoints` show params in spec +- [ ] Snapshot test updated if openapi_current.json is a contract fixture + +### TASK-026-02 — Verify gateway OpenAPI aggregation preserves query params +Status: DOING +Dependency: TASK-026-01 +Owners: Developer (Backend) +Task description: + +The gateway's `OpenApiDocumentGenerator` aggregates specs from all microservices. Verify that it preserves the `parameters` arrays from individual service specs when building the consolidated `/openapi.json`. + +If the aggregator drops parameters, fix it in `src/Router/__Libraries/StellaOps.Router.Gateway/OpenApi/OpenApiDocumentGenerator.cs`. + +Completion criteria: +- [ ] Gateway `/openapi.json` includes all query parameters that individual service specs declare +- [ ] ETag cache invalidates when parameter declarations change +- [ ] Manual verification: `curl /openapi.json | jq '.paths["/api/v2/topology/regions"].get.parameters'` returns non-empty array + +### TASK-026-03 — Build OpenAPI context parameter map service (Angular) + wire into APP_INITIALIZER +Status: DOING +Dependency: TASK-026-01 +Owners: Developer (Frontend) +Task description: + +Create a new Angular service `OpenApiContextParamMap` (in `core/context/`) that is **pre-loaded during app bootstrap**, before any feature module or route resolves. No usable module should load before this data is available. + +**Current bootstrap chain** (in `app.config.ts`, APP_INITIALIZER): +``` +1. AppConfigService.load() → /platform/envsettings.json [AWAIT] +2. I18nService.loadTranslations() → i18n JSON [AWAIT] +3. BackendProbeService.probe() → OIDC probe [FIRE-AND-FORGET] +``` + +**New bootstrap chain** (add step between i18n and probe): +``` +1. AppConfigService.load() → /platform/envsettings.json [AWAIT] +2. I18nService.loadTranslations() → i18n JSON [AWAIT] +3. OpenApiContextParamMap.initialize() → /openapi.json → build Map [AWAIT] ← NEW +4. BackendProbeService.probe() → OIDC probe [FIRE-AND-FORGET] +``` + +Step 3 depends on step 1 (needs base URL from config). It MUST complete before routes load so the interceptor's param map is populated for the first API call. + +**Service requirements:** + +1. `initialize()` method — called from `provideAppInitializer()` in `app.config.ts`: + - Fetches the OpenAPI spec via the existing `GatewayOpenApiHttpClient.getOpenApiSpec()`. + - IMPORTANT: Must use raw `HttpBackend` (not `HttpClient`) to avoid circular dependency with interceptors that depend on this service. Follow the same pattern as `AppConfigService.load()` which uses `HttpBackend` to bypass interceptors. + - Parses the `paths` object and builds a `Map>` where: + - Key = normalized path pattern (e.g., `/api/v2/topology/regions`) + - Value = set of context query param names declared on that path (intersection with known context params: `region`, `regions`, `environment`, `environments`, `timeWindow`, `stage`, `tenant`, `tenantId`) + - Must NOT throw — if spec fetch fails, initialize with empty map (graceful degradation, no context injection). + +2. `getContextParams(url: string): Set | undefined` — synchronous lookup method used by the interceptor at request time. Must be fast (O(1)-ish). + +3. ETag caching — store the ETag from the spec response. On subsequent calls (e.g., periodic refresh), send `If-None-Match` to avoid re-downloading unchanged spec. + +**Path matching strategy:** +- Strip query string and fragment from URL before matching. +- Normalize path params: `/api/v2/topology/environments/{id}` matches `/api/v2/topology/environments/abc-123`. +- Build a regex or trie from OpenAPI path templates for O(1)-ish lookup at request time. A simple approach: convert `{param}` segments to `[^/]+` regex, compile once during `initialize()`. + +**APP_INITIALIZER wiring** (in `app.config.ts`): +```typescript +provideAppInitializer(async () => { + const configService = inject(AppConfigService); + const i18nService = inject(I18nService); + const paramMap = inject(OpenApiContextParamMap); + const probeService = inject(BackendProbeService); + + await configService.load(); + await i18nService.loadTranslations(); + await paramMap.initialize(); // ← NEW — blocks until map is ready + void probeService.probe(); +}), +``` + +Completion criteria: +- [ ] Service exists at `core/context/openapi-context-param-map.service.ts` +- [ ] Wired into `provideAppInitializer()` in `app.config.ts` — runs after config load, before probe +- [ ] Map is built from live OpenAPI spec at startup, before any route resolves +- [ ] Uses `HttpBackend` (not `HttpClient`) to avoid interceptor circular dependency +- [ ] `getContextParams()` returns correct param set for known paths +- [ ] Falls back gracefully if spec fetch fails (empty map, no crash, no context injection) +- [ ] Unit tests with mock spec covering: exact match, parameterized path match, no-match, empty spec, fetch failure + +### TASK-026-04 — Rewrite GlobalContextHttpInterceptor to use OpenAPI map +Status: TODO +Dependency: TASK-026-03 +Owners: Developer (Frontend) +Task description: + +Replace the current `GlobalContextHttpInterceptor` implementation: + +**Current** (hardcoded): +```typescript +if (!this.isPack22ContextAwareRoute(request.url)) { + return next.handle(request); +} +// always inject all params +``` + +**New** (OpenAPI-driven): +```typescript +const contextParams = this.paramMap.getContextParams(request.url); +if (!contextParams || contextParams.size === 0) { + return next.handle(request); +} +// inject ONLY the params this endpoint declares +if (contextParams.has('regions') && regions.length > 0 && !params.has('regions')) { + params = params.set('regions', regions.join(',')); +} +// ... etc for each known context param +``` + +Also add `stage` support (currently missing from interceptor). + +Remove the duplicate param aliases (`region` + `regions`, `tenant` + `tenantId`) — inject only the name declared in the spec. + +Completion criteria: +- [ ] `isPack22ContextAwareRoute()` method deleted +- [ ] Interceptor uses `OpenApiContextParamMap.getContextParams()` for decisions +- [ ] Only declared params are injected per-endpoint +- [ ] `stage` param supported +- [ ] No params injected when spec unavailable (graceful degradation) +- [ ] Unit tests with mock param map + +### TASK-026-05 — Centralize header name constants +Status: TODO +Dependency: none (parallel with Phase 1) +Owners: Developer (Backend) +Task description: + +Extend the existing `StellaOpsHttpHeaderNames` class in `src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsHttpHeaderNames.cs` (or create a new shared constants file in a lower-level shared lib if Auth.Abstractions is too high in the dependency graph) with ALL canonical header names: + +```csharp +public static class StellaOpsHttpHeaders +{ + // Identity + public const string Tenant = "X-Stella-Ops-Tenant"; + public const string Actor = "X-Stella-Ops-Actor"; + public const string Scopes = "X-Stella-Ops-Scopes"; + public const string Roles = "X-Stella-Ops-Roles"; + public const string Client = "X-Stella-Ops-Client"; + public const string Project = "X-Stella-Ops-Project"; + public const string Session = "X-Stella-Ops-Session"; + + // Envelope (Hybrid trust) + public const string IdentityEnvelope = "X-Stella-Ops-Identity-Envelope"; + public const string IdentityEnvelopeSignature = "X-Stella-Ops-Identity-Envelope-Signature"; + + // Observability + public const string TraceId = "X-Stella-Ops-Trace-Id"; + public const string RequestId = "X-Stella-Ops-Request-Id"; + + // Audit + public const string AuditContext = "X-Stella-Ops-Audit-Context"; +} +``` + +Completion criteria: +- [ ] Single source of truth for all header name strings +- [ ] No hardcoded header strings remain in any service — all reference the constants +- [ ] Compiles without errors + +### TASK-026-06 — Rename headers in frontend (Angular) +Status: TODO +Dependency: TASK-026-05 +Owners: Developer (Frontend) +Task description: + +Update all Angular interceptors and services to use the new `X-Stella-Ops-*` naming: + +Files to update: +- `core/auth/tenant-http.interceptor.ts` — change `X-StellaOps-Tenant` → `X-Stella-Ops-Tenant`, remove `X-Stella-Tenant` and `X-Tenant-Id` emissions entirely, change `X-Project-Id` → `X-Stella-Ops-Project`, change `X-Stella-Trace-Id` → `X-Stella-Ops-Trace-Id` +- `core/auth/auth-http.interceptor.ts` — change `X-StellaOps-DPoP-Retry` → `X-Stella-Ops-DPoP-Retry` +- `core/orchestrator/operator-metadata.interceptor.ts` — change `X-Stella-Require-Operator` → `X-Stella-Ops-Require-Operator`, `X-Stella-Operator-Reason` → `X-Stella-Ops-Operator-Reason`, `X-Stella-Operator-Ticket` → `X-Stella-Ops-Operator-Ticket` +- `core/api/gateway-openapi.client.ts` — change `X-Stella-Trace-Id` / `X-Stella-Request-Id` → `X-Stella-Ops-Trace-Id` / `X-Stella-Ops-Request-Id` +- `core/auth/tenant-activation.service.ts` — update JSDoc references + +Create a TypeScript constants file `core/http/stella-ops-headers.ts`: +```typescript +export const StellaOpsHeaders = { + Tenant: 'X-Stella-Ops-Tenant', + Actor: 'X-Stella-Ops-Actor', + Scopes: 'X-Stella-Ops-Scopes', + Project: 'X-Stella-Ops-Project', + TraceId: 'X-Stella-Ops-Trace-Id', + RequestId: 'X-Stella-Ops-Request-Id', + AuditContext: 'X-Stella-Ops-Audit-Context', + DpopRetry: 'X-Stella-Ops-DPoP-Retry', + OperatorReason: 'X-Stella-Ops-Operator-Reason', + OperatorTicket: 'X-Stella-Ops-Operator-Ticket', + RequireOperator: 'X-Stella-Ops-Require-Operator', +} as const; +``` + +Completion criteria: +- [ ] Zero references to `X-StellaOps-*`, `X-Stella-Tenant`, `X-Tenant-Id`, `X-Scopes`, `X-Project-Id` in `src/Web/` +- [ ] All interceptors use constants from `stella-ops-headers.ts` +- [ ] `ng build` passes + +### TASK-026-07 — Rename headers in gateway (Router) +Status: TODO +Dependency: TASK-026-05 +Owners: Developer (Backend) +Task description: + +Update the gateway to emit and read the new canonical header names: + +Key files: +- `src/Router/StellaOps.Gateway.WebService/Middleware/IdentityHeaderPolicyMiddleware.cs` — update the `ReservedHeaders` array and downstream header writes (lines 481-567) to use `X-Stella-Ops-*` +- `src/Router/__Libraries/StellaOps.Microservice.AspNetCore/AspNetRouterRequestDispatcher.cs` — update header name constants (lines 30-35) +- `src/Router/__Libraries/StellaOps.Microservice.AspNetCore/TenantAccessorPopulationMiddleware.cs` — if it reads tenant headers + +During transition, the gateway MUST accept BOTH old and new names on incoming requests (read both, prefer new). On downstream writes, emit ONLY the new names. + +Completion criteria: +- [ ] Gateway reads both `X-StellaOps-Tenant` and `X-Stella-Ops-Tenant` on incoming (backward compat) +- [ ] Gateway writes only `X-Stella-Ops-*` downstream +- [ ] `ReservedHeaders` array includes both old and new names (strips both from incoming) +- [ ] All services continue to work after gateway rename +- [ ] Gateway integration tests pass + +### TASK-026-08 — Rename headers in backend services +Status: TODO +Dependency: TASK-026-05, TASK-026-07 +Owners: Developer (Backend) +Task description: + +Update all backend services to read the new canonical header names. Replace all scattered header string constants with references to the centralized constants from TASK-026-05. + +High-impact modules (by reference count): +1. **Notifier** (~50 refs to `X-Tenant-Id`) — `src/Notifier/` +2. **Scheduler** (~30 refs to `X-Tenant-Id`) — `src/JobEngine/StellaOps.Scheduler*/` +3. **EvidenceLocker** (~20 refs) — `src/EvidenceLocker/` +4. **Graph** (legacy `X-Stella-Tenant`, `X-Stella-Project`) — `src/Graph/` +5. **Policy** (legacy `X-Stella-Tenant`, `X-Stella-Project`) — `src/Policy/` +6. **Scanner** (legacy `X-Stella-Tenant`) — `src/Scanner/` +7. **Platform** (legacy `X-Stella-Tenant`, `X-Stella-Project`) — `src/Platform/` +8. **Authority** (mixed) — `src/Authority/` +9. **AdvisoryAI** (mostly correct `X-StellaOps-*`) — `src/AdvisoryAI/` +10. **Telemetry** (`X-Tenant-Id`) — `src/Telemetry/` +11. **Concelier** (legacy `X-Stella-TraceId`) — `src/Concelier/` + +During transition, services SHOULD accept both old and new names (read new first, fall back to old). This prevents breakage if gateway and services are deployed at different times. + +Approach per service: +1. Add dependency on the shared constants package from TASK-026-05 +2. Replace all hardcoded header strings with constant references +3. In request context resolvers (e.g., `ScannerRequestContextResolver`, `GraphRequestContextResolver`, `PlatformRequestContextResolver`), read new name first, fall back to old +4. In test helpers/factories, update header names in test request builders + +Completion criteria: +- [ ] `grep -r "X-Stella-Tenant\|X-Tenant-Id\|X-StellaOps-\|X-Scopes\b\|X-Project-Id" src/ --include="*.cs"` returns zero matches (excluding the backward-compat fallback reads) +- [ ] All services reference centralized constants +- [ ] All unit/integration tests pass +- [ ] Test helpers updated to use new header names + +### TASK-026-09 — Update documentation +Status: TODO +Dependency: TASK-026-04, TASK-026-08 +Owners: Documentation author +Task description: + +Update the following docs to reflect the new header naming and context propagation approach: +- `docs/modules/platform/architecture-overview.md` — context propagation section +- Module dossiers for Router, Platform, Scanner, Policy if they reference header names +- This sprint's Decisions & Risks section with links to updated docs + +Completion criteria: +- [ ] All docs reference `X-Stella-Ops-*` naming +- [ ] Context propagation flow documented (OpenAPI-driven query param injection) +- [ ] No references to legacy header names in `docs/` + +--- + +## Execution Log + +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-10 | Sprint created with research findings. | Planning | +| 2026-03-10 | Implemented frontend OpenAPI context map bootstrap, rewrote the global context interceptor to use the route map, introduced canonical `X-Stella-Ops-*` frontend header constants, and repaired the stale header cleanup fallout that was breaking `ng build`. Focused specs passed (`27/27`) and `npm run build` passed after the cleanup. | Codex | + +## Decisions & Risks + +### D1: Header naming convention — `X-Stella-Ops-*` (hyphenated) +Decision: Use `X-Stella-Ops-*` as the canonical prefix (hyphen-separated words). +Rationale: Follows HTTP header convention (hyphen-separated), readable, unambiguous. +Note: HTTP headers are case-insensitive per RFC 7230, so `X-StellaOps-Tenant` and `X-Stella-Ops-Tenant` are equivalent at the wire level. The rename is for code consistency and readability. + +### D2: OpenAPI-driven vs header-based context propagation +Decision: Use OpenAPI-driven query parameter injection (not headers). +Rationale: Precise (only injects where endpoint declares support), zero backend changes needed, self-documenting via OpenAPI spec, no router middleware required. +Alternative rejected: Sending context as `X-Stella-Ops-Context-*` headers with router-side translation to query params — adds moving parts, wastes bandwidth on endpoints that don't use context. + +### D3: Backward compatibility during header rename +Risk: Renaming headers across 150+ files risks breaking services during partial deployment. +Mitigation: Gateway accepts both old and new names on incoming requests. Services read new name first, fall back to old. This allows rolling deployment without coordination. +Timeline: Legacy fallback reads can be removed in a follow-up sprint after all services are confirmed on new names. + +### D4: OpenAPI spec currently declares zero query parameters +Risk: The entire OpenAPI-driven approach depends on query params being in the spec (TASK-026-01). +Mitigation: If ASP.NET Core's built-in OpenAPI doesn't auto-detect `[AsParameters]`, add explicit `[FromQuery]` attributes to record properties. Verify with one service first (Platform) before rolling out. +Fallback: If OpenAPI param emission proves complex, the interceptor can use a static fallback map (hardcoded like today but more complete) while the spec catches up. + +### D5: Header cleanup scope (~300 references, ~150 files) +Risk: Mass rename across the codebase. +Mitigation: Each module is renamed in a single atomic commit. Use find-and-replace with constant references to prevent typos. Run full test suite per module. + +### D6: Frontend header cleanup is shipped ahead of backend constant unification +Decision: Land the Angular-side canonical header constants and client/interceptor cleanup first, while the backend shared constant unification remains a separate follow-through. +Rationale: The web bundle and request layer were already carrying the highest defect risk from mixed header names and stale replacements. Shipping the frontend cleanup immediately removes build/runtime instability without blocking on the wider backend rename. +Remaining work: backend shared constants, gateway dual-read/write cleanup, and the remaining legacy spec/debug assertions under `src/Web/`. + +## Next Checkpoints + +- Phase 1 done (OpenAPI params in spec): verify with `curl /openapi.json` and snapshot diff +- Phase 2 done (interceptor rewrite): verify no hardcoded route list, context flows to new endpoints automatically +- Phase 3 done (header cleanup): `grep` confirms zero legacy references +- Full integration test: deploy all services, verify login + dashboard + API calls work end-to-end diff --git a/src/Web/StellaOps.Web/src/app/app.config.ts b/src/Web/StellaOps.Web/src/app/app.config.ts index 9ce71fd98..20042c2da 100644 --- a/src/Web/StellaOps.Web/src/app/app.config.ts +++ b/src/Web/StellaOps.Web/src/app/app.config.ts @@ -37,6 +37,7 @@ import { I18nService } from './core/i18n'; import { DoctorTrendService } from './core/doctor/doctor-trend.service'; import { DoctorNotificationService } from './core/doctor/doctor-notification.service'; import { BackendProbeService } from './core/config/backend-probe.service'; +import { OpenApiContextParamMap } from './core/context/openapi-context-param-map.service'; import { AuthHttpInterceptor } from './core/auth/auth-http.interceptor'; import { AuthSessionStore } from './core/auth/auth-session.store'; import { TenantActivationService } from './core/auth/tenant-activation.service'; @@ -290,13 +291,14 @@ export const appConfig: ApplicationConfig = { { provide: TitleStrategy, useClass: PageTitleStrategy }, provideHttpClient(withInterceptorsFromDi()), provideAppInitializer(() => { - const initializerFn = ((configService: AppConfigService, probeService: BackendProbeService, i18nService: I18nService) => async () => { + const initializerFn = ((configService: AppConfigService, probeService: BackendProbeService, i18nService: I18nService, openApiParamMap: OpenApiContextParamMap) => async () => { await configService.load(); await i18nService.loadTranslations(); + await openApiParamMap.initialize(); if (configService.isConfigured()) { probeService.probe(); } - })(inject(AppConfigService), inject(BackendProbeService), inject(I18nService)); + })(inject(AppConfigService), inject(BackendProbeService), inject(I18nService), inject(OpenApiContextParamMap)); return initializerFn(); }), { diff --git a/src/Web/StellaOps.Web/src/app/core/api/abac-overlay.client.ts b/src/Web/StellaOps.Web/src/app/core/api/abac-overlay.client.ts index 9f7d81fe0..ee8590c9b 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/abac-overlay.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/abac-overlay.client.ts @@ -4,6 +4,7 @@ import { Observable, of, delay, throwError } from 'rxjs'; import { AppConfigService } from '../config/app-config.service'; import { AuthSessionStore } from '../auth/auth-session.store'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; /** * ABAC policy input attributes. @@ -241,7 +242,7 @@ export class AbacOverlayHttpClient implements AbacOverlayApi { private buildHeaders(tenantId: string): HttpHeaders { const headers = new HttpHeaders() .set('Content-Type', 'application/json') - .set('X-Tenant-Id', tenantId); + .set(StellaOpsHeaders.Tenant, tenantId); return headers; } @@ -427,3 +428,5 @@ export class MockAbacOverlayClient implements AbacOverlayApi { return of({ revoked: true }).pipe(delay(50)); } } + + diff --git a/src/Web/StellaOps.Web/src/app/core/api/advisories.client.ts b/src/Web/StellaOps.Web/src/app/core/api/advisories.client.ts index e38903cd1..09ef5bda4 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/advisories.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/advisories.client.ts @@ -6,6 +6,7 @@ import { AuthSessionStore } from '../auth/auth-session.store'; import { TenantActivationService } from '../auth/tenant-activation.service'; import { AdvisoryDetail, AdvisoryListResponse, AdvisoryQueryOptions, AdvisorySummary } from './advisories.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export interface AdvisoryApi { listAdvisories(options?: AdvisoryQueryOptions): Observable; @@ -76,13 +77,13 @@ export class AdvisoryApiHttpClient implements AdvisoryApi { private buildHeaders(tenantId: string, traceId: string, projectId?: string, ifNoneMatch?: string): HttpHeaders { let headers = new HttpHeaders({ 'Content-Type': 'application/json', - 'X-StellaOps-Tenant': tenantId, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenantId, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, }); if (projectId) { - headers = headers.set('X-Stella-Project', projectId); + headers = headers.set(StellaOpsHeaders.Project, projectId); } if (ifNoneMatch) { @@ -191,3 +192,5 @@ export class MockAdvisoryApiService implements AdvisoryApi { return of({ ...found }); } } + + diff --git a/src/Web/StellaOps.Web/src/app/core/api/advisory-ai.client.ts b/src/Web/StellaOps.Web/src/app/core/api/advisory-ai.client.ts index 75f503cc7..f109b9858 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/advisory-ai.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/advisory-ai.client.ts @@ -10,6 +10,7 @@ import { catchError } from 'rxjs/operators'; import { AuthSessionStore } from '../auth/auth-session.store'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { AiConsentStatus, AiConsentRequest, @@ -131,9 +132,9 @@ export class AdvisoryAiApiHttpClient implements AdvisoryAiApi { private buildHeaders(traceId: string): HttpHeaders { const tenant = this.authSession.getActiveTenantId() || ''; return new HttpHeaders({ - 'X-StellaOps-Tenant': tenant, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenant, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, Accept: 'application/json', }); } diff --git a/src/Web/StellaOps.Web/src/app/core/api/ai-runs.client.ts b/src/Web/StellaOps.Web/src/app/core/api/ai-runs.client.ts index 1bde91a89..c317cb32a 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/ai-runs.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/ai-runs.client.ts @@ -13,6 +13,7 @@ import { catchError } from 'rxjs/operators'; import { AuthSessionStore } from '../auth/auth-session.store'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { AiRun, AiRunSummary, @@ -160,9 +161,9 @@ export class AiRunsHttpClient implements AiRunsApi { private buildHeaders(traceId: string): HttpHeaders { const tenant = this.authSession.getActiveTenantId() || ''; return new HttpHeaders({ - 'X-StellaOps-Tenant': tenant, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenant, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, Accept: 'application/json', }); } diff --git a/src/Web/StellaOps.Web/src/app/core/api/analytics.client.ts b/src/Web/StellaOps.Web/src/app/core/api/analytics.client.ts index 3980a76b1..63eb1f744 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/analytics.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/analytics.client.ts @@ -6,6 +6,7 @@ import { Observable, of, throwError } from 'rxjs'; import { catchError, delay } from 'rxjs/operators'; import { AuthSessionStore } from '../auth/auth-session.store'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { AnalyticsAttestationCoverage, AnalyticsComponentTrendPoint, @@ -172,9 +173,9 @@ export class AnalyticsHttpClient { private buildHeaders(traceId: string, tenantId?: string): HttpHeaders { const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId() || ''; return new HttpHeaders({ - 'X-StellaOps-Tenant': tenant, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenant, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, Accept: 'application/json', }); } diff --git a/src/Web/StellaOps.Web/src/app/core/api/aoc.client.ts b/src/Web/StellaOps.Web/src/app/core/api/aoc.client.ts index 1c4358885..51f3149d1 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/aoc.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/aoc.client.ts @@ -5,6 +5,7 @@ import { catchError, map } from 'rxjs/operators'; import { AppConfigService } from '../config/app-config.service'; import { AuthSessionStore } from '../auth/auth-session.store'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { AocMetrics, AocVerificationRequest, @@ -140,9 +141,9 @@ export class AocHttpClient implements AocApi { private buildHeaders(traceId: string): HttpHeaders { const tenantId = this.authSession.getActiveTenantId() || ''; return new HttpHeaders({ - 'X-StellaOps-Tenant': tenantId, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenantId, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, Accept: 'application/json', }); } @@ -180,8 +181,8 @@ export class AocClient { const tenantId = this.authSession.getActiveTenantId() || ''; const traceId = generateTraceId(); return new HttpHeaders({ - 'X-StellaOps-Tenant': tenantId, - 'X-Stella-Trace-Id': traceId, + [StellaOpsHeaders.Tenant]: tenantId, + [StellaOpsHeaders.TraceId]: traceId, Accept: 'application/json', }); } diff --git a/src/Web/StellaOps.Web/src/app/core/api/attestation-chain.client.ts b/src/Web/StellaOps.Web/src/app/core/api/attestation-chain.client.ts index 005694bbe..e73ece127 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/attestation-chain.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/attestation-chain.client.ts @@ -20,6 +20,7 @@ import { } from './attestation-chain.models'; import { TenantActivationService } from '../auth/tenant-activation.service'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; /** * Attestation Chain API interface. @@ -194,11 +195,11 @@ export class AttestationChainHttpClient implements AttestationChainApi { const tenantId = options?.tenantId ?? this.tenantService?.activeTenantId(); if (tenantId) { - headers['X-Tenant-Id'] = tenantId; + headers[StellaOpsHeaders.Tenant] = tenantId; } const traceId = options?.traceId ?? generateTraceId(); - headers['X-Trace-Id'] = traceId; + headers[StellaOpsHeaders.TraceId] = traceId; return headers; } @@ -310,3 +311,4 @@ export class AttestationChainMockClient implements AttestationChainApi { }); } } + diff --git a/src/Web/StellaOps.Web/src/app/core/api/audit-bundles.client.ts b/src/Web/StellaOps.Web/src/app/core/api/audit-bundles.client.ts index 2eda2cbaa..88c12ef49 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/audit-bundles.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/audit-bundles.client.ts @@ -4,6 +4,7 @@ import { Observable, of, delay, map, catchError, throwError } from 'rxjs'; import { TenantActivationService } from '../auth/tenant-activation.service'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import type { AuditBundleCreateRequest, AuditBundleJobResponse, @@ -203,12 +204,12 @@ export class AuditBundlesHttpClient implements AuditBundlesApi { private buildHeaders(tenantId: string, traceId: string, projectId?: string): HttpHeaders { let headers = new HttpHeaders({ - 'X-Stella-Tenant': tenantId, - 'X-Stella-Trace-Id': traceId, + [StellaOpsHeaders.Tenant]: tenantId, + [StellaOpsHeaders.TraceId]: traceId, Accept: 'application/json', }); - if (projectId) headers = headers.set('X-Stella-Project', projectId); + if (projectId) headers = headers.set(StellaOpsHeaders.Project, projectId); return headers; } @@ -347,3 +348,5 @@ function createUnknownSubject(bundleId: string): BundleSubjectRef { digest: {}, }; } + + diff --git a/src/Web/StellaOps.Web/src/app/core/api/authority-admin.client.ts b/src/Web/StellaOps.Web/src/app/core/api/authority-admin.client.ts index 803fe46d8..496beea64 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/authority-admin.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/authority-admin.client.ts @@ -6,6 +6,7 @@ import { Injectable, InjectionToken, Inject } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable, of, delay, map } from 'rxjs'; import { AuthSessionStore } from '../auth/auth-session.store'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; // ============================================================================ // Models @@ -138,7 +139,7 @@ export class AuthorityAdminHttpClient implements AuthorityAdminApi { this.authSession.getActiveTenantId() || 'default'; return new HttpHeaders({ - 'X-StellaOps-Tenant': tenantId, + [StellaOpsHeaders.Tenant]: tenantId, }); } } diff --git a/src/Web/StellaOps.Web/src/app/core/api/authority-console.client.ts b/src/Web/StellaOps.Web/src/app/core/api/authority-console.client.ts index 8e6ca3078..d41eb70e5 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/authority-console.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/authority-console.client.ts @@ -3,6 +3,7 @@ import { Inject, Injectable, InjectionToken } from '@angular/core'; import { Observable } from 'rxjs'; import { AuthSessionStore } from '../auth/auth-session.store'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export interface AuthorityTenantViewDto { readonly id: string; @@ -107,9 +108,7 @@ export class AuthorityConsoleApiHttpClient implements AuthorityConsoleApi { } return new HttpHeaders({ - 'X-StellaOps-Tenant': tenantId, - 'X-Stella-Tenant': tenantId, - 'X-Tenant-Id': tenantId, + [StellaOpsHeaders.Tenant]: tenantId, }); } } diff --git a/src/Web/StellaOps.Web/src/app/core/api/console-export.client.ts b/src/Web/StellaOps.Web/src/app/core/api/console-export.client.ts index d41008de6..39debdab9 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/console-export.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/console-export.client.ts @@ -15,6 +15,7 @@ import { ConsoleExportStatusDto, } from './console-export.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; interface ExportRequestOptions { tenantId?: string; @@ -94,9 +95,9 @@ export class ConsoleExportClient { const trace = opts.traceId ?? generateTraceId(); return new HttpHeaders({ - 'X-StellaOps-Tenant': tenant, - 'X-Stella-Trace-Id': trace, - 'X-Stella-Request-Id': trace, + [StellaOpsHeaders.Tenant]: tenant, + [StellaOpsHeaders.TraceId]: trace, + [StellaOpsHeaders.RequestId]: trace, }); } diff --git a/src/Web/StellaOps.Web/src/app/core/api/console-search.client.ts b/src/Web/StellaOps.Web/src/app/core/api/console-search.client.ts index 9a4020356..a3aa29561 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/console-search.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/console-search.client.ts @@ -20,6 +20,7 @@ import { DownloadManifestItem, } from './console-search.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; /** * Console Search & Downloads API interface. @@ -200,13 +201,13 @@ export class ConsoleSearchHttpClient implements ConsoleSearchApi { const trace = opts.traceId ?? generateTraceId(); let headers = new HttpHeaders({ - 'X-Stella-Trace-Id': trace, - 'X-Stella-Request-Id': trace, + [StellaOpsHeaders.TraceId]: trace, + [StellaOpsHeaders.RequestId]: trace, Accept: 'application/json', }); if (tenant) { - headers = headers.set('X-StellaOps-Tenant', tenant); + headers = headers.set(StellaOpsHeaders.Tenant, tenant); } if (opts.ifNoneMatch) { @@ -483,3 +484,5 @@ export class MockConsoleSearchClient implements ConsoleSearchApi { return btoa(JSON.stringify(cursorData)); } } + + diff --git a/src/Web/StellaOps.Web/src/app/core/api/console-status.client.ts b/src/Web/StellaOps.Web/src/app/core/api/console-status.client.ts index 10ffa1f6d..d6b2fd9b5 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/console-status.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/console-status.client.ts @@ -6,6 +6,7 @@ import { map } from 'rxjs/operators'; import { AuthSessionStore } from '../auth/auth-session.store'; import { ConsoleRunEventDto, ConsoleStatusDto } from './console-status.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export const CONSOLE_API_BASE_URL = new InjectionToken('CONSOLE_API_BASE_URL'); @@ -34,9 +35,9 @@ export class ConsoleStatusClient { const tenant = this.resolveTenant(tenantId); const trace = traceId ?? generateTraceId(); const headers = new HttpHeaders({ - 'X-StellaOps-Tenant': tenant, - 'X-Stella-Trace-Id': trace, - 'X-Stella-Request-Id': trace, + [StellaOpsHeaders.Tenant]: tenant, + [StellaOpsHeaders.TraceId]: trace, + [StellaOpsHeaders.RequestId]: trace, }); return this.http.get(`${this.baseUrl}/status`, { headers }).pipe( diff --git a/src/Web/StellaOps.Web/src/app/core/api/console-vex.client.ts b/src/Web/StellaOps.Web/src/app/core/api/console-vex.client.ts index ce251d630..6a2d7064e 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/console-vex.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/console-vex.client.ts @@ -22,6 +22,7 @@ import { VexSourceType, } from './console-vex.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; /** * Console VEX API interface. @@ -165,9 +166,9 @@ export class ConsoleVexHttpClient implements ConsoleVexApi { const trace = opts.traceId ?? generateTraceId(); let headers = new HttpHeaders({ - 'X-StellaOps-Tenant': tenant, - 'X-Stella-Trace-Id': trace, - 'X-Stella-Request-Id': trace, + [StellaOpsHeaders.Tenant]: tenant, + [StellaOpsHeaders.TraceId]: trace, + [StellaOpsHeaders.RequestId]: trace, Accept: 'application/json', }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/console-vuln.client.ts b/src/Web/StellaOps.Web/src/app/core/api/console-vuln.client.ts index 86047e39a..6b1f3a47c 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/console-vuln.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/console-vuln.client.ts @@ -21,6 +21,7 @@ import { ReachabilityStatus, } from './console-vuln.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; /** * Console Vuln API interface. @@ -133,9 +134,9 @@ export class ConsoleVulnHttpClient implements ConsoleVulnApi { const trace = opts.traceId ?? generateTraceId(); let headers = new HttpHeaders({ - 'X-StellaOps-Tenant': tenant, - 'X-Stella-Trace-Id': trace, - 'X-Stella-Request-Id': trace, + [StellaOpsHeaders.Tenant]: tenant, + [StellaOpsHeaders.TraceId]: trace, + [StellaOpsHeaders.RequestId]: trace, Accept: 'application/json', }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/cvss.client.ts b/src/Web/StellaOps.Web/src/app/core/api/cvss.client.ts index 183ee962e..eebacd57a 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/cvss.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/cvss.client.ts @@ -13,6 +13,7 @@ import { CvssEvidenceItem, } from './cvss.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export const CVSS_API_BASE_URL = new InjectionToken('CVSS_API_BASE_URL'); @@ -103,7 +104,7 @@ export class CvssClient { } private buildHeaders(tenantId: string): HttpHeaders { - let headers = new HttpHeaders({ 'X-Stella-Tenant': tenantId, 'X-Stella-Trace-Id': generateTraceId() }); + let headers = new HttpHeaders({ [StellaOpsHeaders.Tenant]: tenantId, [StellaOpsHeaders.TraceId]: generateTraceId() }); return headers; } diff --git a/src/Web/StellaOps.Web/src/app/core/api/evidence-pack.client.ts b/src/Web/StellaOps.Web/src/app/core/api/evidence-pack.client.ts index 27fa4acce..cfc51c516 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/evidence-pack.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/evidence-pack.client.ts @@ -13,6 +13,7 @@ import { catchError, map } from 'rxjs/operators'; import { AuthSessionStore } from '../auth/auth-session.store'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { EvidencePack, SignedEvidencePack, @@ -126,9 +127,9 @@ export class EvidencePackHttpClient implements EvidencePackApi { private buildHeaders(traceId: string): HttpHeaders { const tenant = this.authSession.getActiveTenantId() || ''; return new HttpHeaders({ - 'X-StellaOps-Tenant': tenant, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenant, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, Accept: 'application/json', }); } diff --git a/src/Web/StellaOps.Web/src/app/core/api/evidence.client.ts b/src/Web/StellaOps.Web/src/app/core/api/evidence.client.ts index 407951811..c4c6190e7 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/evidence.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/evidence.client.ts @@ -3,6 +3,7 @@ import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular import { Observable, of, delay, firstValueFrom, catchError, throwError } from 'rxjs'; import { AuthSessionStore } from '../auth/auth-session.store'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { EvidenceData, @@ -110,7 +111,7 @@ export class EvidenceHttpClient implements EvidenceApi { const tenantId = this.authSession.getActiveTenantId(); const headers: Record = {}; if (tenantId) { - headers['X-StellaOps-Tenant'] = tenantId; + headers[StellaOpsHeaders.Tenant] = tenantId; } return new HttpHeaders(headers); } @@ -457,3 +458,4 @@ export class MockEvidenceApiService implements EvidenceApi { return new Blob([json], { type: mimeType }); } } + diff --git a/src/Web/StellaOps.Web/src/app/core/api/exception.client.ts b/src/Web/StellaOps.Web/src/app/core/api/exception.client.ts index b2d552812..94038b6f3 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/exception.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/exception.client.ts @@ -12,6 +12,7 @@ import { ExceptionStatusTransition, } from './exception.contract.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export interface ExceptionRequestOptions { readonly tenantId?: string; @@ -174,13 +175,13 @@ export class ExceptionApiHttpClient implements ExceptionApi { private buildHeaders(tenantId: string, traceId: string, projectId?: string): HttpHeaders { const headers: Record = { 'Content-Type': 'application/json', - 'X-StellaOps-Tenant': tenantId, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenantId, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, }; if (projectId) { - headers['X-Stella-Project'] = projectId; + headers[StellaOpsHeaders.Project] = projectId; } return new HttpHeaders(headers); @@ -500,3 +501,4 @@ export class MockExceptionApiService implements ExceptionApi { }); } } + diff --git a/src/Web/StellaOps.Web/src/app/core/api/export-center.client.ts b/src/Web/StellaOps.Web/src/app/core/api/export-center.client.ts index 4762db6d5..a334079e7 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/export-center.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/export-center.client.ts @@ -24,6 +24,7 @@ import { ExportFormat, } from './export-center.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export const EXPORT_CENTER_API_BASE_URL = new InjectionToken('EXPORT_CENTER_API_BASE_URL'); @@ -196,9 +197,9 @@ export class ExportCenterHttpClient implements ExportCenterApi { const trace = opts.traceId ?? generateTraceId(); return new HttpHeaders({ - 'X-StellaOps-Tenant': tenant, - 'X-Stella-Trace-Id': trace, - 'X-Stella-Request-Id': trace, + [StellaOpsHeaders.Tenant]: tenant, + [StellaOpsHeaders.TraceId]: trace, + [StellaOpsHeaders.RequestId]: trace, Accept: 'application/json', }); } diff --git a/src/Web/StellaOps.Web/src/app/core/api/findings-ledger.client.ts b/src/Web/StellaOps.Web/src/app/core/api/findings-ledger.client.ts index 45f0421ef..fff3f5528 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/findings-ledger.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/findings-ledger.client.ts @@ -6,6 +6,7 @@ import { AppConfigService } from '../config/app-config.service'; import { AuthSessionStore } from '../auth/auth-session.store'; import { TenantActivationService } from '../auth/tenant-activation.service'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; /** * Workflow action types for Findings Ledger. @@ -324,10 +325,10 @@ export class FindingsLedgerHttpClient implements FindingsLedgerApi { private buildHeaders(tenantId: string, projectId?: string, traceId?: string): HttpHeaders { let headers = new HttpHeaders() .set('Content-Type', 'application/json') - .set('X-Stella-Tenant', tenantId); + .set(StellaOpsHeaders.Tenant, tenantId); - if (projectId) headers = headers.set('X-Stella-Project', projectId); - if (traceId) headers = headers.set('X-Stella-Trace-Id', traceId); + if (projectId) headers = headers.set(StellaOpsHeaders.Project, projectId); + if (traceId) headers = headers.set(StellaOpsHeaders.TraceId, traceId); return headers; } @@ -503,3 +504,5 @@ export class MockFindingsLedgerClient implements FindingsLedgerApi { }).pipe(delay(150)); } } + + diff --git a/src/Web/StellaOps.Web/src/app/core/api/first-signal.client.ts b/src/Web/StellaOps.Web/src/app/core/api/first-signal.client.ts index 525d1e0d5..bdd920c77 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/first-signal.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/first-signal.client.ts @@ -9,6 +9,7 @@ import { CONSOLE_API_BASE_URL, EVENT_SOURCE_FACTORY, type EventSourceFactory } f import { JOBENGINE_API_BASE_URL } from './jobengine.client'; import { FirstSignalResponse, type FirstSignalRunStreamPayload } from './first-signal.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export interface FirstSignalApi { getFirstSignal( @@ -125,13 +126,13 @@ export class FirstSignalHttpClient implements FirstSignalApi { private buildHeaders(tenantId: string, traceId: string, projectId?: string, ifNoneMatch?: string): HttpHeaders { let headers = new HttpHeaders({ 'Content-Type': 'application/json', - 'X-StellaOps-Tenant': tenantId, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenantId, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, }); if (projectId) { - headers = headers.set('X-Stella-Project', projectId); + headers = headers.set(StellaOpsHeaders.Project, projectId); } if (ifNoneMatch) { @@ -181,3 +182,5 @@ export class MockFirstSignalClient implements FirstSignalApi { }); } } + + diff --git a/src/Web/StellaOps.Web/src/app/core/api/gateway-observability.client.ts b/src/Web/StellaOps.Web/src/app/core/api/gateway-observability.client.ts index c25413f81..4f72a34ea 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/gateway-observability.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/gateway-observability.client.ts @@ -19,6 +19,7 @@ import { ObsQueryOptions, } from './gateway-observability.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export const OBS_API_BASE_URL = new InjectionToken('OBS_API_BASE_URL'); @@ -196,9 +197,9 @@ export class GatewayObservabilityHttpClient implements GatewayObservabilityApi { private buildHeaders(traceId: string): HttpHeaders { const tenant = this.authSession.getActiveTenantId() || ''; return new HttpHeaders({ - 'X-StellaOps-Tenant': tenant, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenant, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, Accept: 'application/json', }); } diff --git a/src/Web/StellaOps.Web/src/app/core/api/gateway-openapi.client.ts b/src/Web/StellaOps.Web/src/app/core/api/gateway-openapi.client.ts index 934fdc966..09521ef36 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/gateway-openapi.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/gateway-openapi.client.ts @@ -14,6 +14,7 @@ import { OpenApiQueryOptions, } from './gateway-openapi.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export const GATEWAY_API_BASE_URL = new InjectionToken('GATEWAY_API_BASE_URL'); @@ -134,9 +135,9 @@ export class GatewayOpenApiHttpClient implements GatewayOpenApiApi { private buildHeaders(traceId: string): HttpHeaders { const tenant = this.authSession.getActiveTenantId() || ''; return new HttpHeaders({ - 'X-StellaOps-Tenant': tenant, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenant, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, Accept: 'application/json', }); } diff --git a/src/Web/StellaOps.Web/src/app/core/api/graph-platform.client.ts b/src/Web/StellaOps.Web/src/app/core/api/graph-platform.client.ts index cad8bd579..04dd35ea7 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/graph-platform.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/graph-platform.client.ts @@ -27,6 +27,7 @@ import { GraphEdge, } from './graph-platform.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export const GRAPH_API_BASE_URL = new InjectionToken('GRAPH_API_BASE_URL'); @@ -245,9 +246,9 @@ export class GraphPlatformHttpClient implements GraphPlatformApi { const trace = opts.traceId ?? generateTraceId(); let headers = new HttpHeaders({ - 'X-StellaOps-Tenant': tenant, - 'X-Stella-Trace-Id': trace, - 'X-Stella-Request-Id': trace, + [StellaOpsHeaders.Tenant]: tenant, + [StellaOpsHeaders.TraceId]: trace, + [StellaOpsHeaders.RequestId]: trace, Accept: 'application/json', }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/identity-provider.client.ts b/src/Web/StellaOps.Web/src/app/core/api/identity-provider.client.ts index 7a1052283..8c7d35e1a 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/identity-provider.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/identity-provider.client.ts @@ -10,6 +10,7 @@ import { catchError } from 'rxjs/operators'; import { AuthSessionStore } from '../auth/auth-session.store'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; // --------------------------------------------------------------------------- // Types @@ -180,9 +181,9 @@ export class IdentityProviderApiHttpClient implements IdentityProviderApi { private buildHeaders(traceId: string): HttpHeaders { const tenant = this.authSession.getActiveTenantId() || ''; return new HttpHeaders({ - 'X-StellaOps-Tenant': tenant, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenant, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, Accept: 'application/json', }); } diff --git a/src/Web/StellaOps.Web/src/app/core/api/jobengine-control.client.ts b/src/Web/StellaOps.Web/src/app/core/api/jobengine-control.client.ts index d08432031..ff79c9588 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/jobengine-control.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/jobengine-control.client.ts @@ -27,6 +27,7 @@ import { UpdateJobEngineQuotaRequest, } from './jobengine-control.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export interface OrchestratorControlApi { listQuotas(options?: JobEngineQuotaQueryOptions): Observable; @@ -74,7 +75,7 @@ export interface OrchestratorControlApi { export const ORCHESTRATOR_CONTROL_API = new InjectionToken('ORCHESTRATOR_CONTROL_API'); -const OPERATOR_METADATA_SENTINEL_HEADER = 'X-Stella-Require-Operator'; +const OPERATOR_METADATA_SENTINEL_HEADER = StellaOpsHeaders.RequireOperator; @Injectable({ providedIn: 'root' }) export class JobEngineControlHttpClient implements OrchestratorControlApi { @@ -364,16 +365,16 @@ export class JobEngineControlHttpClient implements OrchestratorControlApi { ): HttpHeaders { let headers = new HttpHeaders({ 'Content-Type': 'application/json', - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, }); if (tenantId) { - headers = headers.set('X-StellaOps-Tenant', tenantId); + headers = headers.set(StellaOpsHeaders.Tenant, tenantId); } if (projectId) { - headers = headers.set('X-Stella-Project', projectId); + headers = headers.set(StellaOpsHeaders.Project, projectId); } if (ifNoneMatch) { @@ -738,3 +739,5 @@ export class MockJobEngineControlClient implements OrchestratorControlApi { }); } } + + diff --git a/src/Web/StellaOps.Web/src/app/core/api/jobengine-jobs.client.ts b/src/Web/StellaOps.Web/src/app/core/api/jobengine-jobs.client.ts index 62918c81c..aa2fb39f0 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/jobengine-jobs.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/jobengine-jobs.client.ts @@ -7,6 +7,7 @@ import { AuthSessionStore } from '../auth/auth-session.store'; import { TenantActivationService } from '../auth/tenant-activation.service'; import { JOBENGINE_API_BASE_URL } from './jobengine.client'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export interface JobEngineJobsQuery { readonly tenantId?: string; @@ -258,15 +259,17 @@ export class JobEngineJobsClient { private buildHeaders(tenantId: string, traceId: string, projectId?: string): HttpHeaders { let headers = new HttpHeaders({ 'Content-Type': 'application/json', - 'X-StellaOps-Tenant': tenantId, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenantId, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, }); if (projectId) { - headers = headers.set('X-Stella-Project', projectId); + headers = headers.set(StellaOpsHeaders.Project, projectId); } return headers; } } + + diff --git a/src/Web/StellaOps.Web/src/app/core/api/jobengine.client.ts b/src/Web/StellaOps.Web/src/app/core/api/jobengine.client.ts index 52718cf89..8b4bd638a 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/jobengine.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/jobengine.client.ts @@ -6,6 +6,7 @@ import { AuthSessionStore } from '../auth/auth-session.store'; import { TenantActivationService } from '../auth/tenant-activation.service'; import { OrchestratorQueryOptions, OrchestratorSource, OrchestratorSourcesResponse } from './jobengine.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export interface OrchestratorApi { listSources(options?: OrchestratorQueryOptions): Observable; @@ -74,13 +75,13 @@ export class OrchestratorHttpClient implements OrchestratorApi { private buildHeaders(tenantId: string, traceId: string, projectId?: string, ifNoneMatch?: string): HttpHeaders { let headers = new HttpHeaders({ 'Content-Type': 'application/json', - 'X-StellaOps-Tenant': tenantId, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenantId, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, }); if (projectId) { - headers = headers.set('X-Stella-Project', projectId); + headers = headers.set(StellaOpsHeaders.Project, projectId); } if (ifNoneMatch) { @@ -160,3 +161,5 @@ export class MockJobEngineClient implements OrchestratorApi { } } + + diff --git a/src/Web/StellaOps.Web/src/app/core/api/noise-gating.client.ts b/src/Web/StellaOps.Web/src/app/core/api/noise-gating.client.ts index 7a6e3afbd..907503713 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/noise-gating.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/noise-gating.client.ts @@ -11,6 +11,7 @@ import { map, catchError, tap } from 'rxjs/operators'; import { AuthSessionStore } from '../auth/auth-session.store'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { NoiseGatingDeltaReport, ComputeDeltaRequest, @@ -171,9 +172,9 @@ export class NoiseGatingApiHttpClient implements NoiseGatingApi { private buildHeaders(traceId: string): HttpHeaders { const tenant = this.authSession.getActiveTenantId() || ''; return new HttpHeaders({ - 'X-StellaOps-Tenant': tenant, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenant, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, 'Content-Type': 'application/json', Accept: 'application/json', }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/notify.client.ts b/src/Web/StellaOps.Web/src/app/core/api/notify.client.ts index ea2a1ca23..80abb7b29 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/notify.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/notify.client.ts @@ -37,6 +37,7 @@ import { NotifyQueryOptions, } from './notify.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export interface NotifyApi { // WEB-NOTIFY-38-001: Base notification APIs @@ -367,15 +368,15 @@ export class NotifyApiHttpClient implements NotifyApi { return new HttpHeaders(); } - return new HttpHeaders({ 'X-StellaOps-Tenant': tenant }); + return new HttpHeaders({ [StellaOpsHeaders.Tenant]: tenant }); } private buildHeadersWithTrace(traceId: string): HttpHeaders { const tenant = this.resolveTenantId(); return new HttpHeaders({ - 'X-StellaOps-Tenant': tenant, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenant, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, Accept: 'application/json', }); } diff --git a/src/Web/StellaOps.Web/src/app/core/api/policy-engine.client.ts b/src/Web/StellaOps.Web/src/app/core/api/policy-engine.client.ts index 21d25e5a5..9174e3f1c 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/policy-engine.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/policy-engine.client.ts @@ -4,6 +4,7 @@ import { Observable, delay, map, of, throwError } from 'rxjs'; import { AppConfigService } from '../config/app-config.service'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { RiskProfileListResponse, RiskProfileResponse, @@ -179,11 +180,11 @@ export class PolicyEngineHttpClient implements PolicyEngineApi { .set('Accept', 'application/json'); if (options.tenantId) { - headers = headers.set('X-Tenant-Id', options.tenantId); + headers = headers.set(StellaOpsHeaders.Tenant, options.tenantId); } const traceId = options.traceId ?? generateTraceId(); - headers = headers.set('X-Stella-Trace-Id', traceId); + headers = headers.set(StellaOpsHeaders.TraceId, traceId); return headers; } @@ -1548,3 +1549,5 @@ export class MockPolicyEngineApi implements PolicyEngineApi { }).pipe(delay(100)); } } + + diff --git a/src/Web/StellaOps.Web/src/app/core/api/policy-exceptions.client.ts b/src/Web/StellaOps.Web/src/app/core/api/policy-exceptions.client.ts index 35aa3ec64..a4f697934 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/policy-exceptions.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/policy-exceptions.client.ts @@ -12,6 +12,7 @@ import { PolicySimulateResponse, } from './policy-exceptions.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export interface PolicyExceptionsApi { getEffective(request: PolicyEffectiveRequest, options?: PolicyExceptionsRequestOptions): Observable; @@ -80,13 +81,13 @@ export class PolicyExceptionsHttpClient implements PolicyExceptionsApi { private buildHeaders(tenantId: string, traceId: string, projectId?: string): HttpHeaders { const headers: Record = { 'Content-Type': 'application/json', - 'X-StellaOps-Tenant': tenantId, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenantId, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, }; if (projectId) { - headers['X-Stella-Project'] = projectId; + headers[StellaOpsHeaders.Project] = projectId; } return new HttpHeaders(headers); @@ -168,3 +169,4 @@ export class MockPolicyExceptionsApiService implements PolicyExceptionsApi { } } + diff --git a/src/Web/StellaOps.Web/src/app/core/api/policy-gates.client.ts b/src/Web/StellaOps.Web/src/app/core/api/policy-gates.client.ts index dfa885460..f5b84453d 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/policy-gates.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/policy-gates.client.ts @@ -5,6 +5,7 @@ import { catchError, map } from 'rxjs/operators'; import { AuthSessionStore } from '../auth/auth-session.store'; import { TenantActivationService } from '../auth/tenant-activation.service'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { PolicyProfile, PolicyProfileType, @@ -617,9 +618,9 @@ export class PolicyGatesHttpClient implements PolicyGatesApi { private buildHeaders(tenantId: string, traceId: string): HttpHeaders { return new HttpHeaders({ 'Content-Type': 'application/json', - 'X-StellaOps-Tenant': tenantId, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenantId, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, }); } } diff --git a/src/Web/StellaOps.Web/src/app/core/api/policy-governance.client.ts b/src/Web/StellaOps.Web/src/app/core/api/policy-governance.client.ts index 70b1ab783..bf5c8832c 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/policy-governance.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/policy-governance.client.ts @@ -35,6 +35,7 @@ import { RiskProfileGovernanceStatus, } from './policy-governance.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; /** * Policy Governance API interface. @@ -922,17 +923,14 @@ export class HttpPolicyGovernanceApi implements PolicyGovernanceApi { const traceId = options.traceId?.trim(); if (traceId) { headers = headers - .set('X-Trace-Id', traceId) - .set('X-Stella-Trace-Id', traceId) - .set('X-Stella-Request-Id', traceId); + .set(StellaOpsHeaders.TraceId, traceId) + .set(StellaOpsHeaders.RequestId, traceId); } const tenantId = this.resolveTenantId(options.tenantId); if (tenantId) { headers = headers - .set('X-StellaOps-Tenant', tenantId) - .set('X-Stella-Tenant', tenantId) - .set('X-Tenant-Id', tenantId); + .set(StellaOpsHeaders.Tenant, tenantId); } return headers; } @@ -1179,3 +1177,5 @@ export class HttpPolicyGovernanceApi implements PolicyGovernanceApi { }); } } + + diff --git a/src/Web/StellaOps.Web/src/app/core/api/policy-registry.client.ts b/src/Web/StellaOps.Web/src/app/core/api/policy-registry.client.ts index fbb265723..79f0368ad 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/policy-registry.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/policy-registry.client.ts @@ -4,6 +4,7 @@ import { Observable, delay, of, catchError, map } from 'rxjs'; import { AppConfigService } from '../config/app-config.service'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { PolicyQueryOptions } from './policy-engine.models'; // ============================================================================ @@ -199,11 +200,11 @@ export class PolicyRegistryHttpClient implements PolicyRegistryApi { .set('Accept', 'application/json'); if (options.tenantId) { - headers = headers.set('X-Tenant-Id', options.tenantId); + headers = headers.set(StellaOpsHeaders.Tenant, options.tenantId); } const traceId = options.traceId ?? generateTraceId(); - headers = headers.set('X-Stella-Trace-Id', traceId); + headers = headers.set(StellaOpsHeaders.TraceId, traceId); return headers; } @@ -468,3 +469,5 @@ export class MockPolicyRegistryClient implements PolicyRegistryApi { }).pipe(delay(25)); } } + + diff --git a/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.ts b/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.ts index bc9ec95df..e6358ef73 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.ts @@ -5,6 +5,7 @@ import { Observable, of, delay, throwError } from 'rxjs'; import { AuthSessionStore } from '../auth/auth-session.store'; import { TenantActivationService } from '../auth/tenant-activation.service'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { ShadowModeConfig, ShadowModeResults, @@ -446,9 +447,9 @@ export class PolicySimulationHttpClient implements PolicySimulationApi { private buildHeaders(tenantId: string, traceId: string): HttpHeaders { return new HttpHeaders({ 'Content-Type': 'application/json', - 'X-StellaOps-Tenant': tenantId, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenantId, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, }); } diff --git a/src/Web/StellaOps.Web/src/app/core/api/policy-streaming.client.ts b/src/Web/StellaOps.Web/src/app/core/api/policy-streaming.client.ts index f163d1ca7..707352a4d 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/policy-streaming.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/policy-streaming.client.ts @@ -3,6 +3,7 @@ import { Observable, Subject, finalize } from 'rxjs'; import { AppConfigService } from '../config/app-config.service'; import { AuthSessionStore } from '../auth/auth-session.store'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { RiskSimulationResult, PolicyEvaluationResponse, @@ -153,7 +154,7 @@ export class PolicyStreamingClient { const headers: Record = { 'Content-Type': 'application/json', 'Accept': 'text/event-stream', - 'X-Tenant-Id': tenantId, + [StellaOpsHeaders.Tenant]: tenantId, }; if (session?.accessToken) { @@ -239,7 +240,7 @@ export class PolicyStreamingClient { const headers: Record = { 'Content-Type': 'application/json', 'Accept': 'text/event-stream', - 'X-Tenant-Id': tenantId, + [StellaOpsHeaders.Tenant]: tenantId, }; if (session?.accessToken) { diff --git a/src/Web/StellaOps.Web/src/app/core/api/risk-http.client.ts b/src/Web/StellaOps.Web/src/app/core/api/risk-http.client.ts index 4f4149565..f9eab5714 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/risk-http.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/risk-http.client.ts @@ -14,6 +14,7 @@ import { SeverityTransitionEvent, } from './risk.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export const RISK_API_BASE_URL = new InjectionToken('RISK_API_BASE_URL'); @@ -150,9 +151,9 @@ export class RiskHttpClient implements RiskApi { } private buildHeaders(tenantId: string, projectId?: string, traceId?: string): HttpHeaders { - let headers = new HttpHeaders({ 'X-Stella-Tenant': tenantId }); - if (projectId) headers = headers.set('X-Stella-Project', projectId); - if (traceId) headers = headers.set('X-Stella-Trace-Id', traceId); + let headers = new HttpHeaders({ [StellaOpsHeaders.Tenant]: tenantId }); + if (projectId) headers = headers.set(StellaOpsHeaders.Project, projectId); + if (traceId) headers = headers.set(StellaOpsHeaders.TraceId, traceId); return headers; } @@ -161,3 +162,5 @@ export class RiskHttpClient implements RiskApi { return tenant ?? ''; } } + + diff --git a/src/Web/StellaOps.Web/src/app/core/api/scheduler.client.ts b/src/Web/StellaOps.Web/src/app/core/api/scheduler.client.ts index 3db508845..cbc069602 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/scheduler.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/scheduler.client.ts @@ -7,6 +7,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable, of } from 'rxjs'; import { delay, map, switchMap } from 'rxjs/operators'; import { AuthSessionStore } from '../auth/auth-session.store'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import type { Schedule, ScheduleImpactPreview, @@ -324,7 +325,7 @@ export class SchedulerHttpClient implements SchedulerApi { const tenantId = this.authSession.getActiveTenantId(); const headers: Record = {}; if (tenantId) { - headers['X-StellaOps-Tenant'] = tenantId; + headers[StellaOpsHeaders.Tenant] = tenantId; } return new HttpHeaders(headers); } @@ -397,3 +398,4 @@ export class MockSchedulerClient implements SchedulerApi { }).pipe(delay(200)); } } + diff --git a/src/Web/StellaOps.Web/src/app/core/api/security-findings.client.ts b/src/Web/StellaOps.Web/src/app/core/api/security-findings.client.ts index 40986fa30..e23472e05 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/security-findings.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/security-findings.client.ts @@ -7,6 +7,7 @@ import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular import { Observable, of, throwError } from 'rxjs'; import { catchError, delay, map } from 'rxjs/operators'; import { AuthSessionStore } from '../auth/auth-session.store'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { PlatformContextStore } from '../context/platform-context.store'; // ============================================================================ @@ -237,7 +238,7 @@ export class SecurityFindingsHttpClient implements SecurityFindingsApi { const tenantId = this.authSession.getActiveTenantId(); const headers: Record = {}; if (tenantId) { - headers['X-StellaOps-Tenant'] = tenantId; + headers[StellaOpsHeaders.Tenant] = tenantId; } return new HttpHeaders(headers); } @@ -740,3 +741,4 @@ export class MockSecurityFindingsClient implements SecurityFindingsApi { return of(detail).pipe(delay(220)); } } + diff --git a/src/Web/StellaOps.Web/src/app/core/api/security-overview.client.ts b/src/Web/StellaOps.Web/src/app/core/api/security-overview.client.ts index 71a4fbff1..a5110785a 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/security-overview.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/security-overview.client.ts @@ -7,6 +7,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable, forkJoin, of } from 'rxjs'; import { catchError, delay, map } from 'rxjs/operators'; import { AuthSessionStore } from '../auth/auth-session.store'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { SECURITY_FINDINGS_API_BASE_URL } from './security-findings.client'; import { POLICY_EXCEPTIONS_API_BASE_URL } from './policy-exceptions.client'; @@ -163,7 +164,7 @@ export class SecurityOverviewHttpClient implements SecurityOverviewApi { const tenantId = this.authSession.getActiveTenantId(); const headers: Record = {}; if (tenantId) { - headers['X-StellaOps-Tenant'] = tenantId; + headers[StellaOpsHeaders.Tenant] = tenantId; } return new HttpHeaders(headers); } @@ -199,3 +200,4 @@ export class MockSecurityOverviewClient implements SecurityOverviewApi { }).pipe(delay(300)); } } + diff --git a/src/Web/StellaOps.Web/src/app/core/api/triage-evidence.client.ts b/src/Web/StellaOps.Web/src/app/core/api/triage-evidence.client.ts index b2eaaa0da..e833a9978 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/triage-evidence.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/triage-evidence.client.ts @@ -20,6 +20,7 @@ import { } from './triage-evidence.models'; import { TenantActivationService } from '../auth/tenant-activation.service'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; /** * Triage Evidence API interface. @@ -213,11 +214,11 @@ export class TriageEvidenceHttpClient implements TriageEvidenceApi { const tenantId = options?.tenantId ?? this.tenantService?.activeTenantId(); if (tenantId) { - headers['X-Tenant-Id'] = tenantId; + headers[StellaOpsHeaders.Tenant] = tenantId; } const traceId = options?.traceId ?? generateTraceId(); - headers['X-Trace-Id'] = traceId; + headers[StellaOpsHeaders.TraceId] = traceId; return headers; } @@ -349,3 +350,4 @@ export class TriageEvidenceMockClient implements TriageEvidenceApi { return of(null); } } + diff --git a/src/Web/StellaOps.Web/src/app/core/api/vex-consensus.client.ts b/src/Web/StellaOps.Web/src/app/core/api/vex-consensus.client.ts index 3016c848d..0473ad573 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/vex-consensus.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/vex-consensus.client.ts @@ -6,6 +6,7 @@ import { AppConfigService } from '../config/app-config.service'; import { AuthSessionStore } from '../auth/auth-session.store'; import { TenantActivationService } from '../auth/tenant-activation.service'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; /** * VEX statement state per OpenVEX spec. @@ -371,10 +372,10 @@ export class VexConsensusHttpClient implements VexConsensusApi { private buildHeaders(tenantId: string, projectId?: string, traceId?: string, ifNoneMatch?: string): HttpHeaders { let headers = new HttpHeaders() .set('Content-Type', 'application/json') - .set('X-Stella-Tenant', tenantId); + .set(StellaOpsHeaders.Tenant, tenantId); - if (projectId) headers = headers.set('X-Stella-Project', projectId); - if (traceId) headers = headers.set('X-Stella-Trace-Id', traceId); + if (projectId) headers = headers.set(StellaOpsHeaders.Project, projectId); + if (traceId) headers = headers.set(StellaOpsHeaders.TraceId, traceId); if (ifNoneMatch) headers = headers.set('If-None-Match', ifNoneMatch); return headers; @@ -604,3 +605,5 @@ export class MockVexConsensusClient implements VexConsensusApi { // No-op } } + + diff --git a/src/Web/StellaOps.Web/src/app/core/api/vex-decisions.client.ts b/src/Web/StellaOps.Web/src/app/core/api/vex-decisions.client.ts index cf6e5e19c..625fa1fba 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/vex-decisions.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/vex-decisions.client.ts @@ -5,6 +5,7 @@ import { Observable, of, delay, map, catchError, throwError } from 'rxjs'; import { AuthSessionStore } from '../auth/auth-session.store'; import { TenantActivationService } from '../auth/tenant-activation.service'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import type { VexDecision } from './evidence.models'; import type { VexDecisionCreateRequest, @@ -98,12 +99,12 @@ export class VexDecisionsHttpClient implements VexDecisionsApi { private buildHeaders(tenantId: string, projectId?: string, traceId?: string, ifNoneMatch?: string): HttpHeaders { let headers = new HttpHeaders({ - 'X-Stella-Tenant': tenantId, - 'X-Stella-Trace-Id': traceId ?? generateTraceId(), + [StellaOpsHeaders.Tenant]: tenantId, + [StellaOpsHeaders.TraceId]: traceId ?? generateTraceId(), Accept: 'application/json', }); - if (projectId) headers = headers.set('X-Stella-Project', projectId); + if (projectId) headers = headers.set(StellaOpsHeaders.Project, projectId); if (ifNoneMatch) headers = headers.set('If-None-Match', ifNoneMatch); return headers; @@ -227,3 +228,5 @@ export class MockVexDecisionsClient implements VexDecisionsApi { return next; } } + + diff --git a/src/Web/StellaOps.Web/src/app/core/api/vex-evidence.client.ts b/src/Web/StellaOps.Web/src/app/core/api/vex-evidence.client.ts index 2da41b9e8..cc88c4f9f 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/vex-evidence.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/vex-evidence.client.ts @@ -15,6 +15,7 @@ import { VexStatementSummary, } from './vex-evidence.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; export interface VexEvidenceApi { listStatements(options?: VexQueryOptions): Observable; @@ -134,13 +135,13 @@ export class VexEvidenceHttpClient implements VexEvidenceApi { private buildHeaders(tenantId: string, traceId: string, projectId?: string, ifNoneMatch?: string): HttpHeaders { let headers = new HttpHeaders({ 'Content-Type': 'application/json', - 'X-StellaOps-Tenant': tenantId, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenantId, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, }); if (projectId) { - headers = headers.set('X-Stella-Project', projectId); + headers = headers.set(StellaOpsHeaders.Project, projectId); } if (ifNoneMatch) { @@ -278,3 +279,5 @@ export class MockVexEvidenceClient implements VexEvidenceApi { }); } } + + diff --git a/src/Web/StellaOps.Web/src/app/core/api/vex-hub.client.ts b/src/Web/StellaOps.Web/src/app/core/api/vex-hub.client.ts index 6a8493a63..7583db012 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/vex-hub.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/vex-hub.client.ts @@ -10,6 +10,7 @@ import { map, catchError } from 'rxjs/operators'; import { AuthSessionStore } from '../auth/auth-session.store'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { VexStatement, VexStatementSearchParams, @@ -206,9 +207,9 @@ export class VexHubApiHttpClient implements VexHubApi { private buildHeaders(traceId: string): HttpHeaders { const tenant = this.authSession.getActiveTenantId() || ''; return new HttpHeaders({ - 'X-StellaOps-Tenant': tenant, - 'X-Stella-Trace-Id': traceId, - 'X-Stella-Request-Id': traceId, + [StellaOpsHeaders.Tenant]: tenant, + [StellaOpsHeaders.TraceId]: traceId, + [StellaOpsHeaders.RequestId]: traceId, Accept: 'application/json', }); } diff --git a/src/Web/StellaOps.Web/src/app/core/api/vulnerability-http.client.ts b/src/Web/StellaOps.Web/src/app/core/api/vulnerability-http.client.ts index 3c7d42dce..79cfc3c5c 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/vulnerability-http.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/vulnerability-http.client.ts @@ -16,6 +16,7 @@ import { VulnRequestLog, } from './vulnerability.models'; import { generateTraceId } from './trace.util'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { VulnerabilityApi } from './vulnerability.client'; export const VULNERABILITY_API_BASE_URL = new InjectionToken('VULNERABILITY_API_BASE_URL'); @@ -362,11 +363,11 @@ export class VulnerabilityHttpClient implements VulnerabilityApi { private buildHeaders(tenantId: string, projectId?: string, traceId?: string, requestId?: string): HttpHeaders { let headers = new HttpHeaders() .set('Content-Type', 'application/json') - .set('X-Stella-Tenant', tenantId); + .set(StellaOpsHeaders.Tenant, tenantId); - if (projectId) headers = headers.set('X-Stella-Project', projectId); - if (traceId) headers = headers.set('X-Stella-Trace-Id', traceId); - if (requestId) headers = headers.set('X-Request-Id', requestId); + if (projectId) headers = headers.set(StellaOpsHeaders.Project, projectId); + if (traceId) headers = headers.set(StellaOpsHeaders.TraceId, traceId); + if (requestId) headers = headers.set(StellaOpsHeaders.RequestId, requestId); return headers; } @@ -425,3 +426,5 @@ export class VulnerabilityHttpClient implements VulnerabilityApi { console.debug('[VulnHttpClient]', log.method, log.path, log.statusCode, `${log.durationMs}ms`); } } + + diff --git a/src/Web/StellaOps.Web/src/app/core/auth/auth-http.interceptor.ts b/src/Web/StellaOps.Web/src/app/core/auth/auth-http.interceptor.ts index 54d36684a..9f38e6e1c 100644 --- a/src/Web/StellaOps.Web/src/app/core/auth/auth-http.interceptor.ts +++ b/src/Web/StellaOps.Web/src/app/core/auth/auth-http.interceptor.ts @@ -6,8 +6,9 @@ import { catchError, switchMap } from 'rxjs/operators'; import { AppConfigService } from '../config/app-config.service'; import { DpopService } from './dpop/dpop.service'; import { AuthorityAuthService } from './authority-auth.service'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; -const RETRY_HEADER = 'X-StellaOps-DPoP-Retry'; +const RETRY_HEADER = StellaOpsHeaders.DpopRetry; @Injectable() export class AuthHttpInterceptor implements HttpInterceptor { diff --git a/src/Web/StellaOps.Web/src/app/core/auth/index.ts b/src/Web/StellaOps.Web/src/app/core/auth/index.ts index 69c4423b8..c400e57fd 100644 --- a/src/Web/StellaOps.Web/src/app/core/auth/index.ts +++ b/src/Web/StellaOps.Web/src/app/core/auth/index.ts @@ -46,9 +46,12 @@ export { export { TenantHttpInterceptor, - TENANT_HEADERS, } from './tenant-http.interceptor'; +export { + StellaOpsHeaders, +} from '../http/stella-ops-headers'; + export { TenantHeaderTelemetryService, } from './tenant-header-telemetry.service'; diff --git a/src/Web/StellaOps.Web/src/app/core/auth/tenant-http.interceptor.spec.ts b/src/Web/StellaOps.Web/src/app/core/auth/tenant-http.interceptor.spec.ts index 00168500a..f9d4bc17b 100644 --- a/src/Web/StellaOps.Web/src/app/core/auth/tenant-http.interceptor.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/auth/tenant-http.interceptor.spec.ts @@ -3,10 +3,10 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { ConsoleSessionStore } from '../console/console-session.store'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; import { TenantActivationService } from './tenant-activation.service'; import { AuthSessionStore } from './auth-session.store'; -import { TENANT_HEADERS, TenantHttpInterceptor } from './tenant-http.interceptor'; -import { TenantHeaderTelemetryService } from './tenant-header-telemetry.service'; +import { TenantHttpInterceptor } from './tenant-http.interceptor'; class MockTenantActivationService { activeTenantId = () => null; @@ -39,7 +39,6 @@ describe('TenantHttpInterceptor', () => { let httpMock: HttpTestingController; let consoleStore: MockConsoleSessionStore; let authStore: MockAuthSessionStore; - let telemetry: TenantHeaderTelemetryService; beforeEach(() => { TestBed.configureTestingModule({ @@ -60,53 +59,41 @@ describe('TenantHttpInterceptor', () => { httpMock = TestBed.inject(HttpTestingController); consoleStore = TestBed.inject(ConsoleSessionStore) as unknown as MockConsoleSessionStore; authStore = TestBed.inject(AuthSessionStore) as unknown as MockAuthSessionStore; - telemetry = TestBed.inject(TenantHeaderTelemetryService); }); afterEach(() => { httpMock.verify(); }); - it('adds canonical and compatibility tenant headers from selected tenant', () => { + it('adds canonical tenant headers from selected tenant', () => { consoleStore.setSelectedTenantId('tenant-bravo'); http.get('/api/v2/platform/overview').subscribe(); const request = httpMock.expectOne('/api/v2/platform/overview'); - expect(request.request.headers.get(TENANT_HEADERS.STELLAOPS_TENANT)).toBe('tenant-bravo'); - expect(request.request.headers.get(TENANT_HEADERS.STELLA_TENANT)).toBe('tenant-bravo'); - expect(request.request.headers.get(TENANT_HEADERS.TENANT_ID)).toBe('tenant-bravo'); + expect(request.request.headers.get(StellaOpsHeaders.Tenant)).toBe('tenant-bravo'); + expect(request.request.headers.get(StellaOpsHeaders.TraceId)).toBeTruthy(); + expect(request.request.headers.get(StellaOpsHeaders.RequestId)).toBeTruthy(); request.flush({}); }); - it('normalizes legacy header input and tracks legacy usage telemetry', () => { + it('preserves an explicitly supplied canonical tenant header', () => { http.get('/api/v2/security/findings', { headers: new HttpHeaders({ - [TENANT_HEADERS.STELLA_TENANT]: 'tenant-legacy', + [StellaOpsHeaders.Tenant]: 'tenant-canonical', }), }).subscribe(); const request = httpMock.expectOne('/api/v2/security/findings'); - expect(request.request.headers.get(TENANT_HEADERS.STELLAOPS_TENANT)).toBe('tenant-legacy'); - expect(request.request.headers.get(TENANT_HEADERS.STELLA_TENANT)).toBe('tenant-legacy'); - expect(request.request.headers.get(TENANT_HEADERS.TENANT_ID)).toBe('tenant-legacy'); + expect(request.request.headers.get(StellaOpsHeaders.Tenant)).toBe('tenant-canonical'); request.flush({}); - - expect(telemetry.legacyUsage()).toEqual([ - { - headerName: TENANT_HEADERS.STELLA_TENANT, - count: 1, - }, - ]); }); it('skips tenant headers for public config endpoint requests', () => { http.get('/config.json').subscribe(); const request = httpMock.expectOne('/config.json'); - expect(request.request.headers.has(TENANT_HEADERS.STELLAOPS_TENANT)).toBeFalse(); - expect(request.request.headers.has(TENANT_HEADERS.STELLA_TENANT)).toBeFalse(); - expect(request.request.headers.has(TENANT_HEADERS.TENANT_ID)).toBeFalse(); + expect(request.request.headers.has(StellaOpsHeaders.Tenant)).toBeFalse(); request.flush({}); }); @@ -117,9 +104,7 @@ describe('TenantHttpInterceptor', () => { http.get('/api/v2/platform/overview').subscribe(); const request = httpMock.expectOne('/api/v2/platform/overview'); - expect(request.request.headers.has(TENANT_HEADERS.STELLAOPS_TENANT)).toBeFalse(); - expect(request.request.headers.has(TENANT_HEADERS.STELLA_TENANT)).toBeFalse(); - expect(request.request.headers.has(TENANT_HEADERS.TENANT_ID)).toBeFalse(); + expect(request.request.headers.has(StellaOpsHeaders.Tenant)).toBeFalse(); request.flush({}); }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/auth/tenant-http.interceptor.ts b/src/Web/StellaOps.Web/src/app/core/auth/tenant-http.interceptor.ts index f26773e57..d83696048 100644 --- a/src/Web/StellaOps.Web/src/app/core/auth/tenant-http.interceptor.ts +++ b/src/Web/StellaOps.Web/src/app/core/auth/tenant-http.interceptor.ts @@ -6,42 +6,28 @@ import { catchError } from 'rxjs/operators'; import { TenantActivationService } from './tenant-activation.service'; import { AuthSessionStore } from './auth-session.store'; import { ConsoleSessionStore } from '../console/console-session.store'; -import { TenantHeaderTelemetryService } from './tenant-header-telemetry.service'; - -/** - * HTTP headers for tenant scoping. - */ -export const TENANT_HEADERS = { - STELLA_TENANT: 'X-Stella-Tenant', - TENANT_ID: 'X-Tenant-Id', - STELLAOPS_TENANT: 'X-StellaOps-Tenant', - PROJECT_ID: 'X-Project-Id', - TRACE_ID: 'X-Stella-Trace-Id', - REQUEST_ID: 'X-Request-Id', - AUDIT_CONTEXT: 'X-Audit-Context', -} as const; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; /** * HTTP interceptor that adds tenant headers to all API requests. * Implements WEB-TEN-47-001 tenant header injection. + * + * Emits only the canonical X-Stella-Ops-* headers. */ @Injectable() export class TenantHttpInterceptor implements HttpInterceptor { private readonly tenantService = inject(TenantActivationService); private readonly authStore = inject(AuthSessionStore); private readonly consoleStore = inject(ConsoleSessionStore); - private readonly telemetry = inject(TenantHeaderTelemetryService); intercept( request: HttpRequest, next: HttpHandler ): Observable> { - // Skip if already has tenant headers or is a public endpoint if (this.shouldSkip(request)) { return next.handle(request); } - // Clone request with tenant headers const modifiedRequest = this.addTenantHeaders(request); return next.handle(modifiedRequest).pipe( @@ -50,7 +36,6 @@ export class TenantHttpInterceptor implements HttpInterceptor { } private shouldSkip(request: HttpRequest): boolean { - // Skip public endpoints that don't require tenant context const url = request.url.toLowerCase(); const publicPaths = [ '/api/auth/', @@ -60,6 +45,7 @@ export class TenantHttpInterceptor implements HttpInterceptor { '/metrics', '/config.json', '/.well-known/', + '/openapi.json', ]; return publicPaths.some(path => url.includes(path)); @@ -67,42 +53,33 @@ export class TenantHttpInterceptor implements HttpInterceptor { private addTenantHeaders(request: HttpRequest): HttpRequest { const headers: Record = {}; - this.recordLegacyHeaderUsage(request); - // Canonical tenant value can come from explicit request headers or active session state. const tenantId = this.resolveRequestedTenantId(request) ?? this.getTenantId(); if (tenantId) { - headers[TENANT_HEADERS.STELLAOPS_TENANT] = tenantId; - headers[TENANT_HEADERS.STELLA_TENANT] = tenantId; - headers[TENANT_HEADERS.TENANT_ID] = tenantId; + headers[StellaOpsHeaders.Tenant] = tenantId; } - // Add project ID if active const projectId = this.tenantService.activeProjectId(); if (projectId) { - headers[TENANT_HEADERS.PROJECT_ID] = projectId; + headers[StellaOpsHeaders.Project] = projectId; } - // Add trace ID for correlation - if (!request.headers.has(TENANT_HEADERS.TRACE_ID)) { - headers[TENANT_HEADERS.TRACE_ID] = this.generateTraceId(); + if (!request.headers.has(StellaOpsHeaders.TraceId)) { + headers[StellaOpsHeaders.TraceId] = this.generateTraceId(); } - // Add request ID - if (!request.headers.has(TENANT_HEADERS.REQUEST_ID)) { - headers[TENANT_HEADERS.REQUEST_ID] = this.generateRequestId(); + if (!request.headers.has(StellaOpsHeaders.RequestId)) { + headers[StellaOpsHeaders.RequestId] = this.generateRequestId(); } - // Add audit context for write operations if (this.isWriteOperation(request.method)) { - headers[TENANT_HEADERS.AUDIT_CONTEXT] = this.buildAuditContext(); + headers[StellaOpsHeaders.AuditContext] = this.buildAuditContext(); } return request.clone({ setHeaders: headers }); } private getTenantId(): string | null { - // First check active tenant context const activeTenantId = this.tenantService.activeTenantId(); if (activeTenantId) { return activeTenantId; @@ -113,42 +90,18 @@ export class TenantHttpInterceptor implements HttpInterceptor { return selectedTenant; } - // Fall back to session tenant return this.authStore.tenantId(); } private resolveRequestedTenantId(request: HttpRequest): string | null { - const candidates = [ - request.headers.get(TENANT_HEADERS.STELLAOPS_TENANT), - request.headers.get(TENANT_HEADERS.STELLA_TENANT), - request.headers.get(TENANT_HEADERS.TENANT_ID), - ]; - - for (const candidate of candidates) { - const normalized = candidate?.trim(); - if (normalized) { - return normalized; - } - } - - return null; - } - - private recordLegacyHeaderUsage(request: HttpRequest): void { - if (request.headers.has(TENANT_HEADERS.STELLA_TENANT)) { - this.telemetry.recordLegacyUsage(TENANT_HEADERS.STELLA_TENANT); - } - - if (request.headers.has(TENANT_HEADERS.TENANT_ID)) { - this.telemetry.recordLegacyUsage(TENANT_HEADERS.TENANT_ID); - } + const candidate = request.headers.get(StellaOpsHeaders.Tenant)?.trim(); + return candidate || null; } private handleTenantError( error: HttpErrorResponse, request: HttpRequest ): Observable { - // Handle tenant-specific errors if (error.status === 403) { const errorCode = error.error?.code || error.error?.error; @@ -168,7 +121,6 @@ export class TenantHttpInterceptor implements HttpInterceptor { } } - // Handle tenant not found if (error.status === 404 && error.error?.code === 'TENANT_NOT_FOUND') { console.error('[TenantInterceptor] Tenant not found:', { tenantId: this.tenantService.activeTenantId(), @@ -192,17 +144,14 @@ export class TenantHttpInterceptor implements HttpInterceptor { ua: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown', }; - // Base64 encode for header transport return btoa(JSON.stringify(context)); } private generateTraceId(): string { - // Use crypto.randomUUID if available, otherwise fallback if (typeof crypto !== 'undefined' && crypto.randomUUID) { return crypto.randomUUID(); } - // Fallback: timestamp + random const timestamp = Date.now().toString(36); const random = Math.random().toString(36).slice(2, 10); return `${timestamp}-${random}`; diff --git a/src/Web/StellaOps.Web/src/app/core/context/global-context-http.interceptor.ts b/src/Web/StellaOps.Web/src/app/core/context/global-context-http.interceptor.ts index 28ef41404..e69579133 100644 --- a/src/Web/StellaOps.Web/src/app/core/context/global-context-http.interceptor.ts +++ b/src/Web/StellaOps.Web/src/app/core/context/global-context-http.interceptor.ts @@ -3,13 +3,23 @@ import { Injectable, inject } from '@angular/core'; import { Observable } from 'rxjs'; import { PlatformContextStore } from './platform-context.store'; +import { OpenApiContextParamMap } from './openapi-context-param-map.service'; +/** + * Injects global context query parameters into outgoing HTTP requests. + * + * Which parameters are injected is driven by the OpenAPI spec — only params + * the endpoint declares are added. The parameter map is pre-loaded at app + * startup via OpenApiContextParamMap.initialize(). + */ @Injectable() export class GlobalContextHttpInterceptor implements HttpInterceptor { private readonly context = inject(PlatformContextStore); + private readonly paramMap = inject(OpenApiContextParamMap); intercept(request: HttpRequest, next: HttpHandler): Observable> { - if (!this.isPack22ContextAwareRoute(request.url)) { + const contextParams = this.paramMap.getContextParams(request.url); + if (!contextParams || contextParams.size === 0) { return next.handle(request); } @@ -18,39 +28,33 @@ export class GlobalContextHttpInterceptor implements HttpInterceptor { const regions = this.context.selectedRegions(); const environments = this.context.selectedEnvironments(); const timeWindow = this.context.timeWindow(); + const stage = this.context.stage(); - if (tenantId && !params.has('tenant') && !params.has('tenantId')) { + if (contextParams.has('tenant') && tenantId && !params.has('tenant')) { params = params.set('tenant', tenantId); + } + if (contextParams.has('tenantId') && tenantId && !params.has('tenantId')) { params = params.set('tenantId', tenantId); } - - if (regions.length > 0 && !params.has('regions') && !params.has('region')) { - const regionFilter = regions.join(','); - params = params.set('regions', regionFilter); - params = params.set('region', regionFilter); + if (contextParams.has('regions') && regions.length > 0 && !params.has('regions')) { + params = params.set('regions', regions.join(',')); } - - if (environments.length > 0 && !params.has('environments') && !params.has('environment')) { - const environmentFilter = environments.join(','); - params = params.set('environments', environmentFilter); - params = params.set('environment', environmentFilter); + if (contextParams.has('region') && regions.length > 0 && !params.has('region')) { + params = params.set('region', regions.join(',')); } - - if (timeWindow && !params.has('timeWindow')) { + if (contextParams.has('environments') && environments.length > 0 && !params.has('environments')) { + params = params.set('environments', environments.join(',')); + } + if (contextParams.has('environment') && environments.length > 0 && !params.has('environment')) { + params = params.set('environment', environments.join(',')); + } + if (contextParams.has('timeWindow') && timeWindow && !params.has('timeWindow')) { params = params.set('timeWindow', timeWindow); } + if (contextParams.has('stage') && stage && stage !== 'all' && !params.has('stage')) { + params = params.set('stage', stage); + } return next.handle(request.clone({ params })); } - - private isPack22ContextAwareRoute(url: string): boolean { - return ( - url.includes('/api/v2/releases') || - url.includes('/api/v2/security') || - url.includes('/api/v2/evidence') || - url.includes('/api/v2/topology') || - url.includes('/api/v2/platform') || - url.includes('/api/v2/integrations') - ); - } } diff --git a/src/Web/StellaOps.Web/src/app/core/context/openapi-context-param-map.service.spec.ts b/src/Web/StellaOps.Web/src/app/core/context/openapi-context-param-map.service.spec.ts new file mode 100644 index 000000000..0a6449abb --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/core/context/openapi-context-param-map.service.spec.ts @@ -0,0 +1,80 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; + +import { OpenApiContextParamMap } from './openapi-context-param-map.service'; + +describe('OpenApiContextParamMap', () => { + let service: OpenApiContextParamMap; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [OpenApiContextParamMap], + }); + + service = TestBed.inject(OpenApiContextParamMap); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('builds a route map from OpenAPI query parameter declarations', async () => { + const initPromise = service.initialize(); + + const request = httpMock.expectOne('/openapi.json'); + expect(request.request.method).toBe('GET'); + request.flush({ + paths: { + '/api/v2/releases/activity': { + get: { + parameters: [ + { name: 'tenant', in: 'query' }, + { name: 'regions', in: 'query' }, + { name: 'timeWindow', in: 'query' }, + { name: 'irrelevant', in: 'query' }, + ], + }, + }, + '/api/v2/topology/environments/{id}': { + get: { + parameters: [ + { name: 'environment', in: 'query' }, + { name: 'stage', in: 'query' }, + ], + }, + }, + }, + }); + + await initPromise; + + expect(service.getContextParams('/api/v2/releases/activity') ?? new Set()).toEqual( + new Set(['tenant', 'regions', 'timeWindow']), + ); + expect(service.getContextParams('/api/v2/topology/environments/env-01') ?? new Set()).toEqual( + new Set(['environment', 'stage']), + ); + }); + + it('returns no match for routes without declared context parameters', async () => { + const initPromise = service.initialize(); + + const request = httpMock.expectOne('/openapi.json'); + request.flush({ + paths: { + '/api/v2/doctor/summary': { + get: { + parameters: [{ name: 'verbose', in: 'query' }], + }, + }, + }, + }); + + await initPromise; + + expect(service.getContextParams('/api/v2/doctor/summary')).toBeUndefined(); + }); +}); diff --git a/src/Web/StellaOps.Web/src/app/core/context/openapi-context-param-map.service.ts b/src/Web/StellaOps.Web/src/app/core/context/openapi-context-param-map.service.ts new file mode 100644 index 000000000..92f4e2b0c --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/core/context/openapi-context-param-map.service.ts @@ -0,0 +1,197 @@ +import { HttpBackend, HttpClient } from '@angular/common/http'; +import { Injectable, signal } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; + +/** + * Context query parameter names that the interceptor knows how to inject. + * Only parameters from this set are extracted from the OpenAPI spec. + */ +const KNOWN_CONTEXT_PARAMS = new Set([ + 'region', + 'regions', + 'environment', + 'environments', + 'timeWindow', + 'stage', + 'tenant', + 'tenantId', +]); + +interface CompiledRoute { + pattern: RegExp; + params: Set; +} + +/** + * Pre-loads the gateway OpenAPI spec at app startup and builds a fast lookup + * map from URL path → set of context query parameters the endpoint declares. + * + * Uses HttpBackend directly (bypasses interceptors) to avoid circular DI — + * the GlobalContextHttpInterceptor depends on this service. + */ +@Injectable({ providedIn: 'root' }) +export class OpenApiContextParamMap { + private readonly http: HttpClient; + private routes: CompiledRoute[] = []; + private etag: string | null = null; + private _initialized = false; + + /** Reactive flag for components that care about load state. */ + readonly initialized = signal(false); + + constructor(httpBackend: HttpBackend) { + // Raw HttpClient bypasses all interceptors — same pattern as AppConfigService. + this.http = new HttpClient(httpBackend); + } + + /** + * Fetch the OpenAPI spec and build the parameter map. + * Called from provideAppInitializer — blocks route resolution. + * Never throws; falls back to empty map on failure. + */ + async initialize(): Promise { + if (this._initialized) { + return; + } + + try { + const headers: Record = { Accept: 'application/json' }; + if (this.etag) { + headers['If-None-Match'] = this.etag; + } + + const response = await firstValueFrom( + this.http.get>('/openapi.json', { + observe: 'response', + headers, + }), + ); + + const newEtag = response.headers.get('ETag'); + if (newEtag) { + this.etag = newEtag; + } + + if (response.body) { + this.buildRoutes(response.body); + } + } catch { + // Graceful degradation: empty routes = no context injection. + // Covers: network error, 304 Not Modified (spec unchanged), invalid JSON. + } + + this._initialized = true; + this.initialized.set(true); + } + + /** + * Synchronous O(n) lookup — returns the set of context query param names + * the matched endpoint declares, or undefined if no match. + * + * Called by GlobalContextHttpInterceptor on every request. + * With ~1900 routes the linear scan completes in <1ms. + */ + getContextParams(url: string): Set | undefined { + const path = this.extractPath(url); + for (const route of this.routes) { + if (route.pattern.test(path)) { + return route.params; + } + } + return undefined; + } + + // --------------------------------------------------------------------------- + // Internals + // --------------------------------------------------------------------------- + + private buildRoutes(spec: Record): void { + const paths = spec['paths']; + if (!paths || typeof paths !== 'object') { + this.routes = []; + return; + } + + const compiled: CompiledRoute[] = []; + + for (const [pathTemplate, methods] of Object.entries(paths as Record)) { + if (!methods || typeof methods !== 'object') { + continue; + } + + // Collect context params across all HTTP methods for this path. + const contextParams = new Set(); + + for (const details of Object.values(methods as Record)) { + if (!details || typeof details !== 'object') { + continue; + } + + const parameters = (details as Record)['parameters']; + if (!Array.isArray(parameters)) { + continue; + } + + for (const param of parameters) { + if ( + param && + typeof param === 'object' && + (param as Record)['in'] === 'query' + ) { + const name = (param as Record)['name']; + if (typeof name === 'string' && KNOWN_CONTEXT_PARAMS.has(name)) { + contextParams.add(name); + } + } + } + } + + if (contextParams.size > 0) { + compiled.push({ + pattern: this.pathToRegex(pathTemplate), + params: contextParams, + }); + } + } + + this.routes = compiled; + } + + /** + * Convert an OpenAPI path template to a regex. + * `/api/v2/topology/environments/{id}` → /^\/api\/v2\/topology\/environments\/[^/]+$/ + */ + private pathToRegex(template: string): RegExp { + const escaped = template + .replace(/[.*+?^${}()|[\]\\]/g, (match) => { + // Don't escape curly braces used for path params — handle them separately. + if (match === '{' || match === '}') { + return match; + } + return `\\${match}`; + }) + .replace(/\{[^}]+\}/g, '[^/]+'); + + return new RegExp(`^${escaped}$`); + } + + /** Strip origin, query string, and fragment from a URL to get a clean path. */ + private extractPath(url: string): string { + try { + // Absolute URL + if (url.startsWith('http://') || url.startsWith('https://')) { + const parsed = new URL(url); + return parsed.pathname; + } + // Relative URL — strip query/fragment + const qIndex = url.indexOf('?'); + const hIndex = url.indexOf('#'); + let end = url.length; + if (qIndex !== -1) end = Math.min(end, qIndex); + if (hIndex !== -1) end = Math.min(end, hIndex); + return url.slice(0, end); + } catch { + return url; + } + } +} diff --git a/src/Web/StellaOps.Web/src/app/core/http/stella-ops-headers.ts b/src/Web/StellaOps.Web/src/app/core/http/stella-ops-headers.ts new file mode 100644 index 000000000..0f7dd3e84 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/core/http/stella-ops-headers.ts @@ -0,0 +1,35 @@ +/** + * Canonical HTTP header names for Stella Ops. + * + * Single source of truth — all interceptors, API clients, and test helpers + * must reference these constants instead of hardcoding header strings. + */ +export const StellaOpsHeaders = { + // Identity + Tenant: 'X-Stella-Ops-Tenant', + Actor: 'X-Stella-Ops-Actor', + Scopes: 'X-Stella-Ops-Scopes', + Roles: 'X-Stella-Ops-Roles', + Client: 'X-Stella-Ops-Client', + Project: 'X-Stella-Ops-Project', + Session: 'X-Stella-Ops-Session', + + // Envelope (Hybrid trust) + IdentityEnvelope: 'X-Stella-Ops-Identity-Envelope', + IdentityEnvelopeSignature: 'X-Stella-Ops-Identity-Envelope-Signature', + + // Observability + TraceId: 'X-Stella-Ops-Trace-Id', + RequestId: 'X-Stella-Ops-Request-Id', + + // Audit + AuditContext: 'X-Stella-Ops-Audit-Context', + + // Auth (DPoP) + DpopRetry: 'X-Stella-Ops-DPoP-Retry', + + // Operator metadata + RequireOperator: 'X-Stella-Ops-Require-Operator', + OperatorReason: 'X-Stella-Ops-Operator-Reason', + OperatorTicket: 'X-Stella-Ops-Operator-Ticket', +} as const; diff --git a/src/Web/StellaOps.Web/src/app/core/orchestrator/operator-metadata.interceptor.ts b/src/Web/StellaOps.Web/src/app/core/orchestrator/operator-metadata.interceptor.ts index 6b59bf7df..9141434b7 100644 --- a/src/Web/StellaOps.Web/src/app/core/orchestrator/operator-metadata.interceptor.ts +++ b/src/Web/StellaOps.Web/src/app/core/orchestrator/operator-metadata.interceptor.ts @@ -3,10 +3,14 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { OperatorContextService } from './operator-context.service'; +import { StellaOpsHeaders } from '../http/stella-ops-headers'; -export const OPERATOR_METADATA_SENTINEL_HEADER = 'X-Stella-Require-Operator'; -export const OPERATOR_REASON_HEADER = 'X-Stella-Operator-Reason'; -export const OPERATOR_TICKET_HEADER = 'X-Stella-Operator-Ticket'; +/** @deprecated Use StellaOpsHeaders.RequireOperator instead. */ +export const OPERATOR_METADATA_SENTINEL_HEADER = StellaOpsHeaders.RequireOperator; +/** @deprecated Use StellaOpsHeaders.OperatorReason instead. */ +export const OPERATOR_REASON_HEADER = StellaOpsHeaders.OperatorReason; +/** @deprecated Use StellaOpsHeaders.OperatorTicket instead. */ +export const OPERATOR_TICKET_HEADER = StellaOpsHeaders.OperatorTicket; @Injectable() export class OperatorMetadataInterceptor implements HttpInterceptor { @@ -16,20 +20,20 @@ export class OperatorMetadataInterceptor implements HttpInterceptor { request: HttpRequest, next: HttpHandler ): Observable> { - if (!request.headers.has(OPERATOR_METADATA_SENTINEL_HEADER)) { + if (!request.headers.has(StellaOpsHeaders.RequireOperator)) { return next.handle(request); } const current = this.context.snapshot(); - const headers = request.headers.delete(OPERATOR_METADATA_SENTINEL_HEADER); + const headers = request.headers.delete(StellaOpsHeaders.RequireOperator); if (!current) { return next.handle(request.clone({ headers })); } const enriched = headers - .set(OPERATOR_REASON_HEADER, current.reason) - .set(OPERATOR_TICKET_HEADER, current.ticket); + .set(StellaOpsHeaders.OperatorReason, current.reason) + .set(StellaOpsHeaders.OperatorTicket, current.ticket); return next.handle(request.clone({ headers: enriched })); } diff --git a/src/Web/StellaOps.Web/src/app/core/testing/global-context-http.interceptor.spec.ts b/src/Web/StellaOps.Web/src/app/core/testing/global-context-http.interceptor.spec.ts index 1cec82400..2c85d1cd9 100644 --- a/src/Web/StellaOps.Web/src/app/core/testing/global-context-http.interceptor.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/testing/global-context-http.interceptor.spec.ts @@ -2,6 +2,7 @@ import { HTTP_INTERCEPTORS, HttpClient, provideHttpClient, withInterceptorsFromD import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; +import { OpenApiContextParamMap } from '../context/openapi-context-param-map.service'; import { GlobalContextHttpInterceptor } from '../context/global-context-http.interceptor'; import { PlatformContextStore } from '../context/platform-context.store'; @@ -26,6 +27,13 @@ describe('GlobalContextHttpInterceptor', () => { selectedRegions: () => ['apac', 'eu-west', 'us-east', 'us-west'], selectedEnvironments: () => ['dev', 'stage'], timeWindow: () => '24h', + stage: () => 'prod', + }, + }, + { + provide: OpenApiContextParamMap, + useValue: { + getContextParams: () => new Set(['tenant', 'regions', 'region', 'environments', 'environment', 'timeWindow', 'stage']), }, }, ], @@ -39,10 +47,20 @@ describe('GlobalContextHttpInterceptor', () => { httpMock.verify(); }); - it('propagates comma-delimited region and environment scope instead of collapsing to the first selection', () => { + it('propagates only the context parameters declared by the OpenAPI route map', () => { http.get('/api/v2/releases/activity').subscribe(); - const request = httpMock.expectOne('/api/v2/releases/activity?tenant=demo-prod&tenantId=demo-prod®ions=apac,eu-west,us-east,us-west®ion=apac,eu-west,us-east,us-west&environments=dev,stage&environment=dev,stage&timeWindow=24h'); + const request = httpMock.expectOne('/api/v2/releases/activity?tenant=demo-prod®ions=apac,eu-west,us-east,us-west®ion=apac,eu-west,us-east,us-west&environments=dev,stage&environment=dev,stage&timeWindow=24h&stage=prod'); request.flush({ items: [] }); }); + + it('leaves requests untouched when the route map has no declared context parameters', () => { + const paramMap = TestBed.inject(OpenApiContextParamMap) as { getContextParams: (url: string) => Set | undefined }; + paramMap.getContextParams = () => undefined; + + http.get('/api/v2/doctor/summary').subscribe(); + + const request = httpMock.expectOne('/api/v2/doctor/summary'); + request.flush({}); + }); }); diff --git a/src/Web/StellaOps.Web/src/app/features/evidence-ribbon/services/evidence-ribbon.service.ts b/src/Web/StellaOps.Web/src/app/features/evidence-ribbon/services/evidence-ribbon.service.ts index b9199d71d..14ca67878 100644 --- a/src/Web/StellaOps.Web/src/app/features/evidence-ribbon/services/evidence-ribbon.service.ts +++ b/src/Web/StellaOps.Web/src/app/features/evidence-ribbon/services/evidence-ribbon.service.ts @@ -22,6 +22,7 @@ import { } from '../models/evidence-ribbon.models'; import { TenantActivationService } from '../../../core/auth/tenant-activation.service'; import { generateTraceId } from '../../../core/api/trace.util'; +import { StellaOpsHeaders } from '../../../core/http/stella-ops-headers'; /** * Evidence Ribbon Service. @@ -305,12 +306,12 @@ export class EvidenceRibbonService { private buildHeaders(): Record { const headers: Record = { - 'X-Trace-Id': generateTraceId(), + [StellaOpsHeaders.TraceId]: generateTraceId(), }; const tenantId = this.tenantService?.activeTenantId(); if (tenantId) { - headers['X-Tenant-Id'] = tenantId; + headers[StellaOpsHeaders.Tenant] = tenantId; } return headers; @@ -369,3 +370,4 @@ interface PolicyApiResponse { version?: string; evaluatedAt?: string; } + diff --git a/src/Web/StellaOps.Web/src/app/features/sbom-diff/services/sbom-diff.service.ts b/src/Web/StellaOps.Web/src/app/features/sbom-diff/services/sbom-diff.service.ts index 20c83cd86..15ba9cd10 100644 --- a/src/Web/StellaOps.Web/src/app/features/sbom-diff/services/sbom-diff.service.ts +++ b/src/Web/StellaOps.Web/src/app/features/sbom-diff/services/sbom-diff.service.ts @@ -28,6 +28,7 @@ import { } from '../models/sbom-diff.models'; import { TenantActivationService } from '../../../core/auth/tenant-activation.service'; import { generateTraceId } from '../../../core/api/trace.util'; +import { StellaOpsHeaders } from '../../../core/http/stella-ops-headers'; /** * SBOM Diff Service. @@ -315,12 +316,12 @@ export class SbomDiffService { private buildHeaders(): Record { const headers: Record = { - 'X-Trace-Id': generateTraceId(), + [StellaOpsHeaders.TraceId]: generateTraceId(), }; const tenantId = this.tenantService?.activeTenantId(); if (tenantId) { - headers['X-Tenant-Id'] = tenantId; + headers[StellaOpsHeaders.Tenant] = tenantId; } return headers; @@ -382,3 +383,4 @@ export interface SbomVersionInfo { readonly componentCount?: number; readonly artifactDigest?: string; } + diff --git a/src/Web/StellaOps.Web/src/app/features/security-risk/advisory-sources.api.ts b/src/Web/StellaOps.Web/src/app/features/security-risk/advisory-sources.api.ts index c48050593..88d250c9f 100644 --- a/src/Web/StellaOps.Web/src/app/features/security-risk/advisory-sources.api.ts +++ b/src/Web/StellaOps.Web/src/app/features/security-risk/advisory-sources.api.ts @@ -2,6 +2,7 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; import { map, Observable } from 'rxjs'; import { AuthSessionStore } from '../../core/auth/auth-session.store'; +import { StellaOpsHeaders } from '../../core/http/stella-ops-headers'; export interface AdvisorySourceListResponseDto { items: AdvisorySourceListItemDto[]; @@ -177,9 +178,7 @@ export class AdvisorySourcesApi { } return new HttpHeaders({ - 'X-Stella-Tenant': tenantId, - 'X-Tenant-Id': tenantId, - 'X-StellaOps-Tenant': tenantId, + [StellaOpsHeaders.Tenant]: tenantId, }); } } diff --git a/src/Web/StellaOps.Web/src/app/features/security-risk/symbol-sources/symbol-sources.api.ts b/src/Web/StellaOps.Web/src/app/features/security-risk/symbol-sources/symbol-sources.api.ts index a9aa3c9f0..d734762a0 100644 --- a/src/Web/StellaOps.Web/src/app/features/security-risk/symbol-sources/symbol-sources.api.ts +++ b/src/Web/StellaOps.Web/src/app/features/security-risk/symbol-sources/symbol-sources.api.ts @@ -2,6 +2,7 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; import { map, Observable } from 'rxjs'; import { AuthSessionStore } from '../../../core/auth/auth-session.store'; +import { StellaOpsHeaders } from '../../../core/http/stella-ops-headers'; export interface SymbolSourceListItem { sourceId: string; @@ -145,9 +146,7 @@ export class SymbolSourcesApiService { return new HttpHeaders(); } return new HttpHeaders({ - 'X-Stella-Tenant': tenantId, - 'X-Tenant-Id': tenantId, - 'X-StellaOps-Tenant': tenantId, + [StellaOpsHeaders.Tenant]: tenantId, }); } } diff --git a/src/Web/StellaOps.Web/src/app/features/vex-timeline/services/vex-timeline.service.ts b/src/Web/StellaOps.Web/src/app/features/vex-timeline/services/vex-timeline.service.ts index 710131182..38ffbbb33 100644 --- a/src/Web/StellaOps.Web/src/app/features/vex-timeline/services/vex-timeline.service.ts +++ b/src/Web/StellaOps.Web/src/app/features/vex-timeline/services/vex-timeline.service.ts @@ -28,6 +28,7 @@ import { } from '../models/vex-timeline.models'; import { TenantActivationService } from '../../../core/auth/tenant-activation.service'; import { generateTraceId } from '../../../core/api/trace.util'; +import { StellaOpsHeaders } from '../../../core/http/stella-ops-headers'; /** * VEX Timeline Service. @@ -291,12 +292,12 @@ export class VexTimelineService { private buildHeaders(): Record { const headers: Record = { - 'X-Trace-Id': generateTraceId(), + [StellaOpsHeaders.TraceId]: generateTraceId(), }; const tenantId = this.tenantService?.activeTenantId(); if (tenantId) { - headers['X-Tenant-Id'] = tenantId; + headers[StellaOpsHeaders.Tenant] = tenantId; } return headers; @@ -364,3 +365,4 @@ interface VerifyResult { timestamp?: string; error?: string; } +