feat(web): integration-hub + wizard wiring for local-setup flow

Integration hub: extends integration.models with fields needed by the wizard
(capabilities, credentials, readiness), updates the shell and list components,
adds routing for the new hub flow, and broadens the integration-list spec.

Integration wizard: new integrations-hub.component, extended wizard with
capability/credential handling, updated template + type models, and broader
spec coverage.

Sprint docs: SPRINT_20260413_003 (UI-driven local setup rerun) updated with
wiring notes; SPRINT_20260410_001 (no-mocks) adjusted. ReleaseOrchestrator
architecture doc gets a minor clarification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-13 22:15:08 +03:00
parent 852c4d15fe
commit 44a253d485
13 changed files with 412 additions and 35 deletions

View File

@@ -11,9 +11,10 @@
* scm - Source control managers
* ci - CI/CD systems
* runtime-hosts - Runtime and host connector inventory
* feeds - Advisory source connectors
* feeds - Feed mirror connectors
* vex-sources - VEX source connectors
* secrets - Secrets managers / vaults
* storage - Object storage connectors
* registry-admin - Registry plan management and audit (Sprint 023)
* :id - Integration detail (standard contract template)
*
@@ -76,6 +77,13 @@ export const integrationHubRoutes: Routes = [
loadComponent: () =>
import('./integration-list.component').then((m) => m.IntegrationListComponent),
},
{
path: 'feeds',
title: 'Feed Mirrors',
data: { breadcrumb: 'Feed Mirrors', type: 'FeedMirror' },
loadComponent: () =>
import('./integration-list.component').then((m) => m.IntegrationListComponent),
},
{
path: 'advisory-vex-sources',
title: 'Advisory & VEX Sources',
@@ -112,6 +120,13 @@ export const integrationHubRoutes: Routes = [
loadComponent: () =>
import('./integration-list.component').then((m) => m.IntegrationListComponent),
},
{
path: 'storage',
title: 'Object Storage',
data: { breadcrumb: 'Object Storage', type: 'ObjectStorage' },
loadComponent: () =>
import('./integration-list.component').then((m) => m.IntegrationListComponent),
},
{
path: 'notifications',

View File

@@ -111,6 +111,18 @@ describe('IntegrationListComponent', () => {
});
});
it('routes object-storage inventory to the storage onboarding flow', () => {
const navigateSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true));
(component as any).integrationType = IntegrationType.ObjectStorage;
component.typeLabel = 'Object Storage';
component.addIntegration();
expect(navigateSpy).toHaveBeenCalledWith(['/ops/integrations', 'onboarding', 'storage'], {
queryParamsHandling: 'merge',
});
});
it('shows inline success feedback instead of alert on successful test-connection', () => {
integrationService.testConnection.and.returnValue(of({
integrationId: 'int-1',

View File

@@ -405,6 +405,8 @@ export class IntegrationListComponent implements OnInit {
SecretsManager: 'Secrets Vault',
FeedMirror: 'Feed Mirror',
Feed: 'Feed Mirror',
ObjectStorage: 'Object Storage',
Storage: 'Object Storage',
SymbolSource: 'Symbol Source',
Marketplace: 'Marketplace',
Notification: 'Notification Provider',
@@ -616,6 +618,7 @@ export class IntegrationListComponent implements OnInit {
case 'FeedMirror': case 'Feed': return IntegrationType.FeedMirror;
case 'RepoSource': return IntegrationType.RepoSource;
case 'SecretsManager': case 'Secrets': return IntegrationType.SecretsManager;
case 'ObjectStorage': case 'Storage': return IntegrationType.ObjectStorage;
case 'SymbolSource': return IntegrationType.SymbolSource;
case 'Marketplace': return IntegrationType.Marketplace;
default: return undefined;
@@ -630,6 +633,7 @@ export class IntegrationListComponent implements OnInit {
case IntegrationType.FeedMirror: return 'feed';
case IntegrationType.RepoSource: return 'repo';
case IntegrationType.SecretsManager: return 'secrets';
case IntegrationType.ObjectStorage: return 'storage';
case IntegrationType.Registry:
default: return 'registry';
}
@@ -640,7 +644,10 @@ export class IntegrationListComponent implements OnInit {
this.integrationType === IntegrationType.Registry ||
this.integrationType === IntegrationType.Scm ||
this.integrationType === IntegrationType.CiCd ||
this.integrationType === IntegrationType.RuntimeHost
this.integrationType === IntegrationType.RuntimeHost ||
this.integrationType === IntegrationType.FeedMirror ||
this.integrationType === IntegrationType.SecretsManager ||
this.integrationType === IntegrationType.ObjectStorage
);
}

View File

@@ -7,10 +7,10 @@ import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/
import { StellaQuickLinksComponent, type StellaQuickLink } from '../../shared/components/stella-quick-links/stella-quick-links.component';
import { IntegrationHubComponent } from './integration-hub.component';
type TabType = 'registries' | 'scm' | 'ci' | 'runtime-hosts' | 'advisory-vex-sources' | 'secrets';
type TabType = 'registries' | 'scm' | 'ci' | 'runtime-hosts' | 'feeds' | 'advisory-vex-sources' | 'secrets' | 'storage';
const KNOWN_TAB_IDS: readonly string[] = [
'registries', 'scm', 'ci', 'runtime-hosts', 'advisory-vex-sources', 'secrets',
'registries', 'scm', 'ci', 'runtime-hosts', 'feeds', 'advisory-vex-sources', 'secrets', 'storage',
];
const PAGE_TABS: readonly StellaPageTab[] = [
@@ -18,12 +18,14 @@ const PAGE_TABS: readonly StellaPageTab[] = [
{ id: 'scm', label: 'SCM', icon: 'M6 3v12|||M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M18 9a9 9 0 0 1-9 9' },
{ id: 'ci', label: 'CI/CD', icon: 'M5 3l14 9-14 9V3z' },
{ id: 'runtime-hosts', label: 'Runtimes / Hosts', icon: 'M2 2h20v8H2z|||M2 14h20v8H2z|||M6 6h.01|||M6 18h.01' },
{ id: 'feeds', label: 'Feed Mirrors', icon: 'M4 6h16|||M4 12h16|||M4 18h10' },
{ id: 'advisory-vex-sources', label: 'Advisory & VEX', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
{ id: 'secrets', label: 'Secrets', icon: 'M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4' },
{ id: 'storage', label: 'Storage', icon: 'M3 6h18|||M5 6v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6|||M8 10h8|||M8 14h5' },
];
/** Priority order for auto-selecting the first populated tab */
const TAB_PRIORITY: readonly TabType[] = ['registries', 'scm', 'ci', 'advisory-vex-sources', 'runtime-hosts', 'secrets'];
const TAB_PRIORITY: readonly TabType[] = ['registries', 'scm', 'ci', 'feeds', 'advisory-vex-sources', 'runtime-hosts', 'secrets', 'storage'];
@Component({
selector: 'app-integration-shell',

View File

@@ -8,6 +8,7 @@ export enum IntegrationType {
SymbolSource = 7,
Marketplace = 8,
SecretsManager = 9,
ObjectStorage = 10,
}
export enum IntegrationProvider {
@@ -39,6 +40,7 @@ export enum IntegrationProvider {
NuGetOrg = 403,
CratesIo = 404,
GoProxy = 405,
S3Compatible = 450,
EbpfAgent = 500,
EtwAgent = 501,
DyldInterposer = 502,
@@ -168,6 +170,8 @@ export function getIntegrationTypeLabel(type: IntegrationType): string {
return 'Marketplace';
case IntegrationType.SecretsManager:
return 'Secrets Manager';
case IntegrationType.ObjectStorage:
return 'Object Storage';
default:
return 'Unknown';
}
@@ -293,6 +297,8 @@ export function getProviderLabel(provider: IntegrationProvider): string {
return 'crates.io';
case IntegrationProvider.GoProxy:
return 'Go Proxy';
case IntegrationProvider.S3Compatible:
return 'S3-Compatible Storage';
case IntegrationProvider.EbpfAgent:
return 'eBPF Agent';
case IntegrationProvider.EtwAgent:

View File

@@ -61,7 +61,7 @@
@if (currentStep() === 'auth') {
<div class="step-content">
<h2>Connection & Credentials</h2>
<p class="step-description">StellaOps stores only AuthRef URIs here. Keep the actual secret in your vault.</p>
<p class="step-description">Prefer AuthRef URIs for credentials. Local public probes can be created without one when the provider supports it.</p>
@if (selectedProvider(); as provider) {
<div class="auth-method-card selected">
@@ -83,7 +83,9 @@
<div class="form-field">
<label for="authRefUri">
AuthRef URI
<span class="required">*</span>
@if (isAuthRefRequired()) {
<span class="required">*</span>
}
</label>
<input
id="authRefUri"
@@ -139,10 +141,10 @@
@if (currentStep() === 'scope') {
<div class="step-content">
<h2>Discovery Scope</h2>
<p class="step-description">Define which repositories, namespaces, or tag patterns StellaOps should use for this connector.</p>
<p class="step-description">Define which repositories, namespaces, or tag patterns StellaOps should use for this connector when discovery scope matters.</p>
<div class="scope-form">
@if (integrationType() === 'registry' || integrationType() === 'scm') {
@if (hasScopeSelectors()) {
<div class="form-field">
<label for="repositories">Repositories</label>
<textarea
@@ -188,7 +190,17 @@
</div>
}
<span class="field-hint">At least one owner, repository, namespace, branch, or tag scope is required before creation.</span>
@if (!hasScopeSelectors()) {
<div class="check-item status-warning">
<span class="check-status">i</span>
<div class="check-info">
<span class="check-name">Optional scope</span>
<span class="check-message">This connector can be created without extra discovery filters and tightened later.</span>
</div>
</div>
}
<span class="field-hint">{{ getScopeGuidance() }}</span>
</div>
</div>
}

View File

@@ -2,18 +2,21 @@ import { TestBed } from '@angular/core/testing';
import { IntegrationProvider, IntegrationType } from '../integration-hub/integration.models';
import { IntegrationWizardComponent } from './integration-wizard.component';
import { resolveSupportedProviders } from './models/integration.models';
import { IntegrationOnboardingType, resolveSupportedProviders } from './models/integration.models';
describe('IntegrationWizardComponent', () => {
let component: IntegrationWizardComponent;
function createComponent() {
component = TestBed.runInInjectionContext(() => new IntegrationWizardComponent());
(component as any).integrationType = () => 'scm';
(component as any).supportedProviders = () => resolveSupportedProviders('scm', [
function createComponent(
type: IntegrationOnboardingType = 'scm',
catalog = [
{ name: 'github-app', type: IntegrationType.Scm, provider: IntegrationProvider.GitHubApp },
]);
component.draft.update((draft) => ({ ...draft, type: 'scm' }));
],
) {
component = TestBed.runInInjectionContext(() => new IntegrationWizardComponent());
(component as any).integrationType = () => type;
(component as any).supportedProviders = () => resolveSupportedProviders(type, catalog);
component.draft.update((draft) => ({ ...draft, type }));
}
it('selects a supported provider and seeds its default endpoint', () => {
@@ -25,20 +28,32 @@ describe('IntegrationWizardComponent', () => {
expect(component.draft().name).toContain('GitHub App');
});
it('requires AuthRef URI and provider metadata before leaving the connection step', () => {
it('requires provider metadata but allows optional AuthRef for local-friendly providers', () => {
createComponent();
component.selectProvider(IntegrationProvider.GitHubApp);
component.currentStep.set('auth');
expect(component.canGoNext()).toBeFalse();
component.updateAuthRefUri('authref://vault/github#app');
component.updateConfigField('appId', '12345');
component.updateConfigField('installationId', '67890');
expect(component.canGoNext()).toBeTrue();
});
it('allows no-auth and no-scope onboarding for local object storage connectors', () => {
createComponent('storage', [
{ name: 's3-compatible', type: IntegrationType.ObjectStorage, provider: IntegrationProvider.S3Compatible },
]);
component.selectProvider(IntegrationProvider.S3Compatible);
component.currentStep.set('auth');
expect(component.canGoNext()).toBeTrue();
component.currentStep.set('scope');
expect(component.canGoNext()).toBeTrue();
});
it('emits a canonical create request instead of a UI-only draft', () => {
createComponent();
component.selectProvider(IntegrationProvider.GitHubApp);
@@ -73,4 +88,26 @@ describe('IntegrationWizardComponent', () => {
}),
}));
});
it('emits null authRefUri for optional-auth object storage connectors', () => {
createComponent('storage', [
{ name: 's3-compatible', type: IntegrationType.ObjectStorage, provider: IntegrationProvider.S3Compatible },
]);
component.selectProvider(IntegrationProvider.S3Compatible);
const emitSpy = jasmine.createSpy('emit');
spyOn(component.create, 'emit').and.callFake(emitSpy);
component.updateName('Local MinIO');
component.currentStep.set('review');
component.onSubmit();
expect(emitSpy).toHaveBeenCalledWith(jasmine.objectContaining({
name: 'Local MinIO',
type: IntegrationType.ObjectStorage,
provider: IntegrationProvider.S3Compatible,
endpoint: 'http://minio.stella-ops.local:9000',
authRefUri: null,
}));
});
});

View File

@@ -67,7 +67,7 @@ export class IntegrationWizardComponent {
readonly selectedProvider = computed(() => resolveProviderDefinition(this.draft().provider));
readonly deploymentTemplate = computed(() => null);
readonly copySafetyGuidance = computed(() => [
'Only AuthRef URIs are stored. Keep the underlying secret in a vault.',
'Use AuthRef URIs whenever the connector requires credentials; local public probes can be created without one.',
'Non-secret provider fields such as App ID or Installation ID are stored as connector metadata.',
'Use environment-specific endpoints so prod, stage, and lab connectors remain explicit.',
]);
@@ -302,11 +302,36 @@ export class IntegrationWizardComponent {
return 'CI/CD';
case 'host':
return 'Host';
case 'feed':
return 'Feed Mirror';
case 'secrets':
return 'Secrets';
case 'storage':
return 'Object Storage';
default:
return 'Integration';
}
}
isAuthRefRequired(): boolean {
return this.selectedProvider()?.authRefRequired ?? true;
}
hasScopeSelectors(): boolean {
const type = this.integrationType();
return type === 'registry' || type === 'scm';
}
isScopeRequired(): boolean {
return this.selectedProvider()?.scopeRequired ?? this.hasScopeSelectors();
}
getScopeGuidance(): string {
return this.isScopeRequired()
? 'At least one owner, repository, namespace, branch, or tag scope is required before creation.'
: 'Scope is optional. Leave it blank to create the connector with endpoint-level health probing only.';
}
copyToClipboard(text: string): void {
void navigator.clipboard.writeText(text);
}
@@ -330,7 +355,11 @@ export class IntegrationWizardComponent {
return false;
}
if (draft.endpoint.trim().length === 0 || draft.authRefUri.trim().length === 0) {
if (draft.endpoint.trim().length === 0) {
return false;
}
if (this.isAuthRefRequired() && draft.authRefUri.trim().length === 0) {
return false;
}
@@ -341,6 +370,10 @@ export class IntegrationWizardComponent {
private isScopeValid(): boolean {
const draft = this.draft();
if (!this.isScopeRequired()) {
return true;
}
return (
draft.organizationId.trim().length > 0 ||
draft.repositories.length > 0 ||
@@ -372,9 +405,23 @@ export class IntegrationWizardComponent {
private getPreflightChecks(): PreflightCheck[] {
const provider = this.selectedProvider();
const checks: PreflightCheck[] = [
{ id: 'authref', name: 'Credential indirection', description: 'Verify the connector uses an AuthRef URI instead of raw secrets.', status: 'pending' },
{
id: 'authref',
name: 'Credential indirection',
description: this.isAuthRefRequired()
? 'Verify the connector uses an AuthRef URI instead of raw secrets.'
: 'Prefer AuthRef URI indirection when the connector needs credentials; allow empty auth for local public probes.',
status: 'pending',
},
{ id: 'endpoint', name: 'Endpoint contract', description: 'Confirm the endpoint matches the provider health/test contract.', status: 'pending' },
{ id: 'scope', name: 'Discovery scope', description: 'Ensure at least one repository, namespace, branch, or owner scope is set.', status: 'pending' },
{
id: 'scope',
name: 'Discovery scope',
description: this.isScopeRequired()
? 'Ensure at least one repository, namespace, branch, or owner scope is set.'
: 'Scope is optional for this connector; endpoint-only health checks are allowed.',
status: 'pending',
},
{ id: 'schedule', name: 'Probe schedule', description: 'Validate the deterministic check cadence for this connector.', status: 'pending' },
];
@@ -397,6 +444,12 @@ export class IntegrationWizardComponent {
switch (check.id) {
case 'authref':
if (draft.authRefUri.trim().length === 0) {
return this.isAuthRefRequired()
? { status: 'error', message: 'This connector requires an authref:// URI instead of embedding secrets in the connector.' }
: { status: 'warning', message: 'No AuthRef configured; the connector will rely on anonymous or endpoint-only health probing.' };
}
return draft.authRefUri.trim().startsWith('authref://')
? { status: 'success', message: 'Credential indirection is configured via AuthRef URI.' }
: { status: 'error', message: 'Use an authref:// URI instead of embedding secrets in the connector.' };
@@ -405,9 +458,13 @@ export class IntegrationWizardComponent {
? { status: 'success', message: `Endpoint ${draft.endpoint.trim()} will be used for connector probes.` }
: { status: 'error', message: 'A provider endpoint is required.' };
case 'scope':
return this.isScopeValid()
? { status: 'success', message: 'Connector scope is explicitly defined.' }
: { status: 'error', message: 'Set an owner, repository, namespace, branch, or tag scope before creating the connector.' };
if (this.hasExplicitScopeInputs()) {
return this.isScopeValid()
? { status: 'success', message: 'Connector scope is explicitly defined.' }
: { status: 'error', message: 'Set an owner, repository, namespace, branch, or tag scope before creating the connector.' };
}
return { status: 'warning', message: 'No explicit discovery scope is configured; the connector can still be created for endpoint-level checks.' };
case 'schedule':
return this.isScheduleValid()
? { status: 'success', message: `Connector checks will run in ${draft.schedule.type} mode.` }
@@ -426,4 +483,15 @@ export class IntegrationWizardComponent {
: { status: 'error', message: 'Select a supported provider first.' };
}
}
private hasExplicitScopeInputs(): boolean {
const draft = this.draft();
return (
draft.organizationId.trim().length > 0 ||
draft.repositories.length > 0 ||
draft.branches.length > 0 ||
draft.namespaces.length > 0 ||
draft.tagPatterns.length > 0
);
}
}

View File

@@ -120,6 +120,69 @@ import {
}
</section>
<section class="category-section">
<div class="category-header">
<div>
<h2>Secrets & Config Stores</h2>
<p class="category-desc">Connect Vault and Consul surfaces that back secret indirection and local config checks.</p>
</div>
<button class="btn btn-primary" type="button" (click)="openWizard('secrets')" [disabled]="secretProviders().length === 0">
+ Add Secrets
</button>
</div>
@if (secretProviders().length > 0) {
<div class="provider-pills">
@for (provider of secretProviders(); track provider.provider) {
<span class="provider-pill">{{ provider.name }}</span>
}
</div>
} @else {
<p class="category-empty">No secrets-manager connector plugins are installed in this environment.</p>
}
</section>
<section class="category-section">
<div class="category-header">
<div>
<h2>Feed Mirrors</h2>
<p class="category-desc">Register the StellaOps, NVD, and OSV mirror connectors used in the local advisory lane.</p>
</div>
<button class="btn btn-primary" type="button" (click)="openWizard('feed')" [disabled]="feedProviders().length === 0">
+ Add Feed Mirror
</button>
</div>
@if (feedProviders().length > 0) {
<div class="provider-pills">
@for (provider of feedProviders(); track provider.provider) {
<span class="provider-pill">{{ provider.name }}</span>
}
</div>
} @else {
<p class="category-empty">No feed-mirror connector plugins are installed in this environment.</p>
}
</section>
<section class="category-section">
<div class="category-header">
<div>
<h2>Object Storage</h2>
<p class="category-desc">Connect S3-compatible storage for air-gap bundles, exports, and mirrored artifacts.</p>
</div>
<button class="btn btn-primary" type="button" (click)="openWizard('storage')" [disabled]="storageProviders().length === 0">
+ Add Storage
</button>
</div>
@if (storageProviders().length > 0) {
<div class="provider-pills">
@for (provider of storageProviders(); track provider.provider) {
<span class="provider-pill">{{ provider.name }}</span>
}
</div>
} @else {
<p class="category-empty">No object-storage connector plugins are installed in this environment.</p>
}
</section>
<section class="category-section">
<div class="category-header">
<div>
@@ -278,6 +341,9 @@ export class IntegrationsHubComponent implements OnInit {
readonly scmProviders = computed(() => resolveSupportedProviders('scm', this.supportedCatalog()));
readonly ciProviders = computed(() => resolveSupportedProviders('ci', this.supportedCatalog()));
readonly hostProviders = computed(() => resolveSupportedProviders('host', this.supportedCatalog()));
readonly feedProviders = computed(() => resolveSupportedProviders('feed', this.supportedCatalog()));
readonly secretProviders = computed(() => resolveSupportedProviders('secrets', this.supportedCatalog()));
readonly storageProviders = computed(() => resolveSupportedProviders('storage', this.supportedCatalog()));
ngOnInit(): void {
this.route.paramMap.subscribe((params) => {
@@ -372,6 +438,12 @@ export class IntegrationsHubComponent implements OnInit {
return this.ciProviders();
case 'host':
return this.hostProviders();
case 'feed':
return this.feedProviders();
case 'secrets':
return this.secretProviders();
case 'storage':
return this.storageProviders();
default:
return [];
}
@@ -383,6 +455,9 @@ export class IntegrationsHubComponent implements OnInit {
case 'scm':
case 'ci':
case 'host':
case 'feed':
case 'secrets':
case 'storage':
return type;
default:
return null;

View File

@@ -5,7 +5,15 @@ import {
SupportedProviderInfo,
} from '../../integration-hub/integration.models';
export type IntegrationOnboardingType = 'registry' | 'scm' | 'ci' | 'host' | 'advisory-vex';
export type IntegrationOnboardingType =
| 'registry'
| 'scm'
| 'ci'
| 'host'
| 'feed'
| 'secrets'
| 'storage'
| 'advisory-vex';
export type WizardStep = 'provider' | 'auth' | 'scope' | 'schedule' | 'preflight' | 'review';
export interface ProviderField {
@@ -25,8 +33,10 @@ export interface IntegrationProviderDefinition {
defaultEndpoint: string;
endpointHint: string;
authRefHint: string;
authRefRequired?: boolean;
organizationLabel?: string;
organizationHint?: string;
scopeRequired?: boolean;
exposeInUi: boolean;
configFields: ProviderField[];
}
@@ -73,8 +83,10 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
defaultEndpoint: 'https://harbor.local',
endpointHint: 'Use the Harbor base URL; StellaOps probes /api/v2.0/health.',
authRefHint: 'Reference a robot-account or username:password secret, for example authref://vault/harbor#robot-account.',
authRefRequired: false,
organizationLabel: 'Project / Namespace',
organizationHint: 'Optional Harbor project scope used for list and policy views.',
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
@@ -131,14 +143,16 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
{
provider: IntegrationProvider.DockerHub,
type: 'registry',
name: 'Docker Hub',
name: 'Docker Registry / Hub',
icon: 'DH',
description: 'Docker Hub registry with personal access token or username/password authentication.',
defaultEndpoint: 'https://registry.hub.docker.com',
endpointHint: 'Use the Docker Hub registry URL. Leave as default for Docker Hub Cloud.',
authRefHint: 'Reference a vault secret containing a Docker Hub access token or username:password.',
description: 'Open OCI registry or Docker Hub-compatible registry with optional credential-backed probes.',
defaultEndpoint: 'http://registry.stella-ops.local:5000',
endpointHint: 'Use the registry base URL. The local scratch lane uses registry.stella-ops.local:5000.',
authRefHint: 'Reference a vault secret containing username:password when the registry requires authentication.',
authRefRequired: false,
organizationLabel: 'Namespace / Organization',
organizationHint: 'Docker Hub namespace or organization to scope repository discovery.',
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
@@ -179,8 +193,10 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
defaultEndpoint: 'https://nexus.local',
endpointHint: 'Use the Nexus base URL with the Docker hosted or proxy repository port.',
authRefHint: 'Reference a vault secret containing a Nexus user token or username:password.',
authRefRequired: false,
organizationLabel: 'Repository Name',
organizationHint: 'Nexus Docker repository name to scope image discovery.',
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
@@ -222,8 +238,10 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
defaultEndpoint: 'https://github.com',
endpointHint: 'Use https://github.com for GitHub Cloud or your GitHub Enterprise Server base URL.',
authRefHint: 'Reference a vault secret that resolves to the app JWT or installation token.',
authRefRequired: false,
organizationLabel: 'Owner / Organization',
organizationHint: 'Optional owner used to scope repository discovery and policy views.',
scopeRequired: false,
exposeInUi: true,
configFields: [
{
@@ -277,8 +295,10 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
defaultEndpoint: 'https://gitea.local',
endpointHint: 'Use your Gitea or Forgejo instance base URL.',
authRefHint: 'Reference a vault secret containing a Gitea API token.',
authRefRequired: false,
organizationLabel: 'Organization / Owner',
organizationHint: 'Gitea organization or owner to scope repository discovery.',
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
@@ -336,8 +356,10 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
defaultEndpoint: 'https://jenkins.local',
endpointHint: 'Use the Jenkins base URL (including context path if configured).',
authRefHint: 'Reference a vault secret containing a Jenkins API token or username:apiToken.',
authRefRequired: false,
organizationLabel: 'Folder / View',
organizationHint: 'Optional Jenkins folder or view to scope job discovery.',
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
@@ -409,6 +431,8 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
defaultEndpoint: 'https://agent.local:9443',
endpointHint: 'Use the eBPF agent gRPC or HTTP endpoint on the target host.',
authRefHint: 'Reference a vault secret containing the agent mTLS client certificate or bearer token.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
@@ -421,6 +445,8 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
defaultEndpoint: 'https://agent.local:9443',
endpointHint: 'Use the ETW agent HTTP endpoint on the target Windows host.',
authRefHint: 'Reference a vault secret containing the agent bearer token.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
@@ -433,10 +459,96 @@ const ALL_PROVIDER_DEFINITIONS: readonly IntegrationProviderDefinition[] = [
defaultEndpoint: 'https://agent.local:9443',
endpointHint: 'Use the dyld interposer agent HTTP endpoint on the target macOS host.',
authRefHint: 'Reference a vault secret containing the agent bearer token.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
// ── InMemory (testing only, not exposed) ────────────────────────
{
provider: IntegrationProvider.StellaOpsMirror,
type: 'feed',
name: 'StellaOps Mirror',
icon: 'SM',
description: 'Local or air-gap StellaOps advisory mirror served by Concelier.',
defaultEndpoint: 'http://concelier.stella-ops.local',
endpointHint: 'Use the Concelier base URL that serves the StellaOps mirror health surface.',
authRefHint: 'AuthRef is optional for local mirror health probes.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
{
provider: IntegrationProvider.NvdMirror,
type: 'feed',
name: 'NVD Mirror',
icon: 'NVD',
description: 'Local or mirrored NVD feed surface served by Concelier.',
defaultEndpoint: 'http://concelier.stella-ops.local',
endpointHint: 'Use the Concelier base URL that serves the NVD mirror health surface.',
authRefHint: 'AuthRef is optional for local mirror health probes.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
{
provider: IntegrationProvider.OsvMirror,
type: 'feed',
name: 'OSV Mirror',
icon: 'OSV',
description: 'Local or mirrored OSV feed surface served by Concelier.',
defaultEndpoint: 'http://concelier.stella-ops.local',
endpointHint: 'Use the Concelier base URL that serves the OSV mirror health surface.',
authRefHint: 'AuthRef is optional for local mirror health probes.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
{
provider: IntegrationProvider.Vault,
type: 'secrets',
name: 'HashiCorp Vault',
icon: 'V',
description: 'Vault KV and secret-reference provider for local and production credential indirection.',
defaultEndpoint: 'http://vault.stella-ops.local:8200',
endpointHint: 'Use the Vault base URL; StellaOps validates the Vault health surface.',
authRefHint: 'The Vault connector itself does not require an AuthRef for the local dev server.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
{
provider: IntegrationProvider.Consul,
type: 'secrets',
name: 'HashiCorp Consul',
icon: 'C',
description: 'Consul KV and settings integration for the optional local service-discovery lane.',
defaultEndpoint: 'http://consul.stella-ops.local:8500',
endpointHint: 'Use the Consul HTTP API base URL.',
authRefHint: 'The local single-node Consul profile does not require an AuthRef.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
{
provider: IntegrationProvider.S3Compatible,
type: 'storage',
name: 'S3-Compatible Storage',
icon: 'S3',
description: 'S3-compatible object storage used for exports, bundles, and mirrored artifacts.',
defaultEndpoint: 'http://minio.stella-ops.local:9000',
endpointHint: 'Use the storage base URL. The local MinIO lane probes /minio/health/live automatically.',
authRefHint: 'AuthRef is optional for the default local MinIO health probe.',
authRefRequired: false,
scopeRequired: false,
exposeInUi: true,
configFields: [],
},
{
provider: IntegrationProvider.InMemory,
type: 'registry',
@@ -455,6 +567,9 @@ export const REGISTRY_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) =>
export const SCM_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'scm');
export const CI_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'ci');
export const HOST_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'host');
export const FEED_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'feed');
export const SECRETS_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'secrets');
export const STORAGE_PROVIDERS = ALL_PROVIDER_DEFINITIONS.filter((provider) => provider.type === 'storage');
export const scheduleOptions = [
{ value: 'manual', label: 'Manual', description: 'Create the connector now and trigger checks on demand.' },
@@ -499,6 +614,12 @@ export function toBackendIntegrationType(type: IntegrationOnboardingType | null)
return IntegrationType.CiCd;
case 'host':
return IntegrationType.RuntimeHost;
case 'feed':
return IntegrationType.FeedMirror;
case 'secrets':
return IntegrationType.SecretsManager;
case 'storage':
return IntegrationType.ObjectStorage;
default:
return null;
}