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

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