Stabilize web context propagation and header constants
This commit is contained in:
@@ -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();
|
||||
}),
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 }));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { HTTP_INTERCEPTORS, HttpClient, provideHttpClient, withInterceptorsFromD
|
||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { OpenApiContextParamMap } from '../context/openapi-context-param-map.service';
|
||||
import { GlobalContextHttpInterceptor } from '../context/global-context-http.interceptor';
|
||||
import { PlatformContextStore } from '../context/platform-context.store';
|
||||
|
||||
@@ -26,6 +27,13 @@ describe('GlobalContextHttpInterceptor', () => {
|
||||
selectedRegions: () => ['apac', 'eu-west', 'us-east', 'us-west'],
|
||||
selectedEnvironments: () => ['dev', 'stage'],
|
||||
timeWindow: () => '24h',
|
||||
stage: () => 'prod',
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: OpenApiContextParamMap,
|
||||
useValue: {
|
||||
getContextParams: () => new Set(['tenant', 'regions', 'region', 'environments', 'environment', 'timeWindow', 'stage']),
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -39,10 +47,20 @@ describe('GlobalContextHttpInterceptor', () => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
it('propagates comma-delimited region and environment scope instead of collapsing to the first selection', () => {
|
||||
it('propagates only the context parameters declared by the OpenAPI route map', () => {
|
||||
http.get('/api/v2/releases/activity').subscribe();
|
||||
|
||||
const request = httpMock.expectOne('/api/v2/releases/activity?tenant=demo-prod&tenantId=demo-prod®ions=apac,eu-west,us-east,us-west®ion=apac,eu-west,us-east,us-west&environments=dev,stage&environment=dev,stage&timeWindow=24h');
|
||||
const request = httpMock.expectOne('/api/v2/releases/activity?tenant=demo-prod®ions=apac,eu-west,us-east,us-west®ion=apac,eu-west,us-east,us-west&environments=dev,stage&environment=dev,stage&timeWindow=24h&stage=prod');
|
||||
request.flush({ items: [] });
|
||||
});
|
||||
|
||||
it('leaves requests untouched when the route map has no declared context parameters', () => {
|
||||
const paramMap = TestBed.inject(OpenApiContextParamMap) as { getContextParams: (url: string) => Set<string> | undefined };
|
||||
paramMap.getContextParams = () => undefined;
|
||||
|
||||
http.get('/api/v2/doctor/summary').subscribe();
|
||||
|
||||
const request = httpMock.expectOne('/api/v2/doctor/summary');
|
||||
request.flush({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user