Stabilize web context propagation and header constants

This commit is contained in:
master
2026-03-10 16:37:59 +02:00
parent 72746e2f7b
commit e49236f630
65 changed files with 1058 additions and 281 deletions

View File

@@ -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<string, Set<string>>` 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<string> | 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

View File

@@ -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();
}),
{

View File

@@ -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));
}
}

View File

@@ -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<AdvisoryListResponse>;
@@ -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 });
}
}

View File

@@ -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',
});
}

View File

@@ -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',
});
}

View File

@@ -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',
});
}

View File

@@ -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',
});
}

View File

@@ -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 {
});
}
}

View File

@@ -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: {},
};
}

View File

@@ -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,
});
}
}

View File

@@ -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,
});
}
}

View File

@@ -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,
});
}

View File

@@ -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));
}
}

View File

@@ -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<string>('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<ConsoleStatusDto>(`${this.baseUrl}/status`, { headers }).pipe(

View File

@@ -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',
});

View File

@@ -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',
});

View File

@@ -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<string>('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;
}

View File

@@ -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',
});
}

View File

@@ -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<string, string> = {};
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 });
}
}

View File

@@ -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<string, string> = {
'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 {
});
}
}

View File

@@ -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<string>('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',
});
}

View File

@@ -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));
}
}

View File

@@ -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 {
});
}
}

View File

@@ -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<string>('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',
});
}

View File

@@ -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<string>('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',
});
}

View File

@@ -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<string>('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',
});

View File

@@ -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',
});
}

View File

@@ -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<JobEngineQuotaListResponse>;
@@ -74,7 +75,7 @@ export interface OrchestratorControlApi {
export const ORCHESTRATOR_CONTROL_API = new InjectionToken<OrchestratorControlApi>('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 {
});
}
}

View File

@@ -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;
}
}

View File

@@ -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<OrchestratorSourcesResponse>;
@@ -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 {
}
}

View File

@@ -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',
});

View File

@@ -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',
});
}

View File

@@ -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));
}
}

View File

@@ -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<PolicyEffectiveResponse>;
@@ -80,13 +81,13 @@ export class PolicyExceptionsHttpClient implements PolicyExceptionsApi {
private buildHeaders(tenantId: string, traceId: string, projectId?: string): HttpHeaders {
const headers: Record<string, string> = {
'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 {
}
}

View File

@@ -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,
});
}
}

View File

@@ -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 {
});
}
}

View File

@@ -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));
}
}

View File

@@ -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,
});
}

View File

@@ -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<string, string> = {
'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<string, string> = {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
'X-Tenant-Id': tenantId,
[StellaOpsHeaders.Tenant]: tenantId,
};
if (session?.accessToken) {

View File

@@ -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<string>('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 ?? '';
}
}

View File

@@ -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<string, string> = {};
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));
}
}

View File

@@ -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<string, string> = {};
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));
}
}

View File

@@ -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<string, string> = {};
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));
}
}

View File

@@ -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);
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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<VexStatementsResponse>;
@@ -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 {
});
}
}

View File

@@ -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',
});
}

View File

@@ -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<string>('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`);
}
}

View File

@@ -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 {

View File

@@ -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';

View File

@@ -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({});
});
});

View File

@@ -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<unknown>,
next: HttpHandler
): Observable<HttpEvent<unknown>> {
// 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<unknown>): 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<unknown>): HttpRequest<unknown> {
const headers: Record<string, string> = {};
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<unknown>): 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<unknown>): 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<unknown>
): Observable<never> {
// 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}`;

View File

@@ -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<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
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')
);
}
}

View File

@@ -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();
});
});

View File

@@ -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<string>;
}
/**
* 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<void> {
if (this._initialized) {
return;
}
try {
const headers: Record<string, string> = { Accept: 'application/json' };
if (this.etag) {
headers['If-None-Match'] = this.etag;
}
const response = await firstValueFrom(
this.http.get<Record<string, unknown>>('/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<string> | 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<string, unknown>): 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<string, unknown>)) {
if (!methods || typeof methods !== 'object') {
continue;
}
// Collect context params across all HTTP methods for this path.
const contextParams = new Set<string>();
for (const details of Object.values(methods as Record<string, unknown>)) {
if (!details || typeof details !== 'object') {
continue;
}
const parameters = (details as Record<string, unknown>)['parameters'];
if (!Array.isArray(parameters)) {
continue;
}
for (const param of parameters) {
if (
param &&
typeof param === 'object' &&
(param as Record<string, unknown>)['in'] === 'query'
) {
const name = (param as Record<string, unknown>)['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;
}
}
}

View File

@@ -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;

View File

@@ -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<unknown>,
next: HttpHandler
): Observable<HttpEvent<unknown>> {
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 }));
}

View File

@@ -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&regions=apac,eu-west,us-east,us-west&region=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&regions=apac,eu-west,us-east,us-west&region=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<string> | undefined };
paramMap.getContextParams = () => undefined;
http.get('/api/v2/doctor/summary').subscribe();
const request = httpMock.expectOne('/api/v2/doctor/summary');
request.flush({});
});
});

View File

@@ -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<string, string> {
const headers: Record<string, string> = {
'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;
}

View File

@@ -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<string, string> {
const headers: Record<string, string> = {
'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;
}

View File

@@ -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,
});
}
}

View File

@@ -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,
});
}
}

View File

@@ -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<string, string> {
const headers: Record<string, string> = {
'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;
}