Add provider auth broker profiles

This commit is contained in:
ZacharyZhang-NY committed 2026-05-23 19:23:55 -04:00
1 parent c862cd1319
commit c6be8a4ee4
10 files changed
+370 -37

No files matched your search

@@ -0,0 +1,59 @@
export const providerAuthBrokerSchema = `
create table if not exists provider_auth_profiles (
id text primary key,
org_id text not null references organizations(id) on delete cascade,
provider_id text not null references providers(id) on delete cascade,
credential_id text not null references credentials(id) on delete restrict,
provider_key text not null,
auth_type text not null,
profile_kind text not null,
display_hint text not null,
source_type text not null,
source_ref text,
capability_json text not null,
active integer not null check(active in (0, 1)),
created_at text not null
);
create index if not exists idx_provider_auth_profiles_org_provider
on provider_auth_profiles(org_id, provider_id, created_at);
create index if not exists idx_provider_auth_profiles_credential
on provider_auth_profiles(credential_id);
create unique index if not exists idx_provider_auth_profiles_active
on provider_auth_profiles(provider_id) where active = 1;
insert into provider_auth_profiles(
id, org_id, provider_id, credential_id, provider_key, auth_type,
profile_kind, display_hint, source_type, source_ref, capability_json,
active, created_at
)
select
'authp_' || lower(hex(randomblob(16))),
providers.org_id,
providers.id,
credentials.id,
providers.provider_key,
providers.auth_type,
providers.auth_type,
credentials.display_hint,
'migrated_credential',
null,
'{"providerKey":"' || providers.provider_key || '","authType":"' || providers.auth_type || '","capabilityTags":[]}',
1,
credentials.created_at
from providers
join credentials
on credentials.id = providers.credential_id
and credentials.org_id = providers.org_id
and credentials.provider_key = providers.provider_key
where providers.credential_id is not null
and not exists (
select 1
from provider_auth_profiles existing_profiles
where existing_profiles.provider_id = providers.id
and existing_profiles.active = 1
);
insert into schema_migrations(version, name, applied_at)
values (21, 'provider_auth_broker', datetime('now'));
`;
@@ -19,6 +19,7 @@ import { operationsControlsSchema } from "./migrations/017-operations-controls";
import { sdlcControlsSchema } from "./migrations/018-sdlc-controls";
import { auditReplaySchema } from "./migrations/019-audit-replay";
import { rollbackSnapshotsSchema } from "./migrations/020-rollback-snapshots";
import { providerAuthBrokerSchema } from "./migrations/021-provider-auth-broker";
interface Migration {
version: number;
@@ -47,6 +48,7 @@ const MIGRATIONS: Migration[] = [
{ version: 18, name: "sdlc_controls", sql: sdlcControlsSchema },
{ version: 19, name: "audit_replay", sql: auditReplaySchema },
{ version: 20, name: "rollback_snapshots", sql: rollbackSnapshotsSchema },
{ version: 21, name: "provider_auth_broker", sql: providerAuthBrokerSchema },
];
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
@@ -0,0 +1,128 @@
import type { DatabaseSync } from "node:sqlite";
import { createHash } from "node:crypto";
import type { ProviderAuthType } from "../../shared/domain";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
import { SecretVault } from "./secret-vault";
export interface ProviderSecretVault {
encryptSecret(secret: string): Buffer;
decryptSecret(ciphertext: Buffer): string;
}
export interface CreateProviderAuthProfileInput {
orgId: string;
providerId: string;
providerKey: string;
authType: ProviderAuthType;
secret: string;
profileKind: string;
sourceType: string;
sourceRef: string | null;
capabilityTags: string[];
}
export interface ProviderAuthProfileRef {
authProfileId: string;
credentialId: string;
displayHint: string;
}
export class AuthBrokerService {
private readonly audit: AuditService;
constructor(
private readonly db: DatabaseSync,
private readonly vault: ProviderSecretVault = new SecretVault(),
) {
this.audit = new AuditService(db);
}
createProviderProfile(input: CreateProviderAuthProfileInput): ProviderAuthProfileRef {
const secret = input.secret.trim();
if (!secret) throw new Error("Credential is required.");
const credentialId = createId("cred");
const authProfileId = createId("authp");
const displayHint = credentialDisplayHint(secret);
const now = new Date().toISOString();
const capabilityJson = JSON.stringify({
providerKey: input.providerKey,
authType: input.authType,
capabilityTags: input.capabilityTags,
});
this.db.prepare(`
insert into credentials(id, org_id, provider_key, kind, ciphertext, display_hint, created_at)
values (?, ?, ?, ?, ?, ?, ?)
`).run(credentialId, input.orgId, input.providerKey, input.authType, this.vault.encryptSecret(secret), displayHint, now);
this.db.prepare(`
update provider_auth_profiles
set active = 0
where org_id = ? and provider_id = ? and active = 1
`).run(input.orgId, input.providerId);
this.db.prepare(`
insert into provider_auth_profiles(
id, org_id, provider_id, credential_id, provider_key, auth_type,
profile_kind, display_hint, source_type, source_ref, capability_json,
active, created_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)
`).run(
authProfileId,
input.orgId,
input.providerId,
credentialId,
input.providerKey,
input.authType,
input.profileKind,
displayHint,
input.sourceType,
input.sourceRef,
capabilityJson,
now,
);
this.audit.record({
orgId: input.orgId,
entityType: "provider_auth_profile",
entityId: authProfileId,
actorType: "system",
actorId: null,
action: "created",
payload: {
providerId: input.providerId,
providerKey: input.providerKey,
authType: input.authType,
profileKind: input.profileKind,
},
});
return { authProfileId, credentialId, displayHint };
}
loadProviderSecret(input: {
orgId: string;
providerId: string;
providerKey: string;
credentialId: string;
}): string {
const row = this.db.prepare(`
select credentials.ciphertext
from provider_auth_profiles
join credentials on credentials.id = provider_auth_profiles.credential_id
where provider_auth_profiles.org_id = ?
and provider_auth_profiles.provider_id = ?
and provider_auth_profiles.provider_key = ?
and provider_auth_profiles.credential_id = ?
and provider_auth_profiles.active = 1
and credentials.org_id = provider_auth_profiles.org_id
and credentials.provider_key = provider_auth_profiles.provider_key
`).get(input.orgId, input.providerId, input.providerKey, input.credentialId) as { ciphertext: Uint8Array } | undefined;
if (!row) throw new Error("Provider credential was not found.");
return this.vault.decryptSecret(Buffer.from(row.ciphertext));
}
}
function credentialDisplayHint(secret: string): string {
const clean = secret.trim();
if (clean.startsWith("{")) return `json:${createHash("sha256").update(clean).digest("hex").slice(0, 10)}`;
if (clean.length <= 10) return `${clean.slice(0, 2)}...`;
return `${clean.slice(0, 6)}...${clean.slice(-4)}`;
}
@@ -2,10 +2,11 @@ import type { DatabaseSync } from "node:sqlite";
import { createHash } from "node:crypto";
import { BrowserWindow, session } from "electron";
import type { CreateProviderResponseInput, SaveProviderCredentialInput, UpdateProviderPolicyInput } from "../../shared/commands";
import { FEATURE_FLAG_KEY, PROVIDER_AUTH_TYPE, PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, PROVIDER_REQUEST_STATE, SLO_SIGNAL_KEY, type ProviderExecutionMode } from "../../shared/domain";
import { FEATURE_FLAG_KEY, PROVIDER_AUTH_TYPE, PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, PROVIDER_REQUEST_STATE, SLO_SIGNAL_KEY, type ProviderAuthType, type ProviderExecutionMode } from "../../shared/domain";
import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
import { AuthBrokerService, type ProviderSecretVault } from "./auth-broker-service";
import { OperationsService } from "./operations-service";
import { SecretVault } from "./secret-vault";
import { providerAdapters, type ProviderAdapterRegistry } from "./provider-adapters";
@@ -16,52 +17,46 @@ const HEALTH_STATE_FAILED = "failed";
const HEALTH_STATE_UNCHECKED = "unchecked";
const WORKSPACE_SCOPES = new Set(["organization", "workspace"]);
function displayHint(secret: string): string {
const clean = secret.trim();
if (clean.length <= 10) return `${clean.slice(0, 2)}...`;
return `${clean.slice(0, 6)}...${clean.slice(-4)}`;
}
export class ProviderService {
private readonly audit: AuditService;
private readonly authBroker: AuthBrokerService;
private readonly operations: OperationsService;
constructor(
private readonly db: DatabaseSync,
private readonly adapters: ProviderAdapterRegistry = providerAdapters,
private readonly vault: SecretVault = new SecretVault(),
vault: ProviderSecretVault = new SecretVault(),
) {
this.audit = new AuditService(db);
this.authBroker = new AuthBrokerService(db, vault);
this.operations = new OperationsService(db);
}
saveCredential(input: SaveProviderCredentialInput): void {
const provider = this.db.prepare("select * from providers where id = ? and org_id = ?")
.get(input.providerId, input.orgId) as { provider_key: string; auth_type: string } | undefined;
.get(input.providerId, input.orgId) as { provider_key: string; auth_type: ProviderAuthType } | undefined;
if (!provider) throw new Error("Provider was not found.");
const secret = input.secret.trim();
if (!secret) throw new Error("Credential is required.");
const adapter = this.requireAdapter(provider.provider_key, provider.auth_type);
adapter.parseAuthProfile(secret);
const credentialId = createId("cred");
const now = new Date().toISOString();
this.db.prepare(`
insert into credentials(id, org_id, provider_key, kind, ciphertext, display_hint, created_at)
values (?, ?, ?, ?, ?, ?, ?)
`).run(
credentialId,
input.orgId,
provider.provider_key,
provider.auth_type,
this.vault.encryptSecret(secret),
displayHint(secret),
now,
);
const catalogEntry = this.requireCatalogEntry(provider.provider_key);
const authProfile = this.authBroker.createProviderProfile({
orgId: input.orgId,
providerId: input.providerId,
providerKey: provider.provider_key,
authType: provider.auth_type,
secret,
profileKind: provider.auth_type,
sourceType: "manual_secret",
sourceRef: null,
capabilityTags: catalogEntry.capabilityTags,
});
this.db.prepare(`
update providers
set credential_id = ?, model = ?, enabled = 1, auth_state = ?, health_state = ?, last_error = null, last_checked_at = null
where id = ? and org_id = ?
`).run(credentialId, input.model, AUTH_STATE_CONFIGURED, HEALTH_STATE_UNCHECKED, input.providerId, input.orgId);
`).run(authProfile.credentialId, input.model, AUTH_STATE_CONFIGURED, HEALTH_STATE_UNCHECKED, input.providerId, input.orgId);
this.audit.record({
orgId: input.orgId,
entityType: "provider",
@@ -69,7 +64,7 @@ export class ProviderService {
actorType: "system",
actorId: null,
action: "credential_saved",
payload: { providerKey: provider.provider_key, authType: provider.auth_type },
payload: { providerKey: provider.provider_key, authType: provider.auth_type, authProfileId: authProfile.authProfileId },
});
}
@@ -78,7 +73,7 @@ export class ProviderService {
const adapter = this.requireAdapter(provider.provider_key, provider.auth_type);
const credential = this.loadCredential(orgId, provider);
try {
const profile = adapter.parseAuthProfile(this.vault.decryptSecret(Buffer.from(credential.ciphertext)));
const profile = adapter.parseAuthProfile(credential.secret);
await adapter.checkHealth({ authProfile: profile, model: provider.model });
this.recordProviderHealth(provider.id, orgId, HEALTH_STATE_HEALTHY, null);
this.audit.record({
@@ -213,7 +208,7 @@ export class ProviderService {
);
try {
const authProfile = adapter.parseAuthProfile(this.vault.decryptSecret(Buffer.from(credential.ciphertext)));
const authProfile = adapter.parseAuthProfile(credential.secret);
const result = await adapter.createResponse({
authProfile,
model: provider.model,
@@ -299,15 +294,16 @@ export class ProviderService {
return provider;
}
private loadCredential(orgId: string, provider: ProviderRow): { ciphertext: Uint8Array } {
private loadCredential(orgId: string, provider: ProviderRow): { secret: string } {
if (!provider.credential_id) throw new Error("Provider credential is required.");
const credential = this.db.prepare(`
select ciphertext
from credentials
where id = ? and org_id = ? and provider_key = ?
`).get(provider.credential_id, orgId, provider.provider_key) as { ciphertext: Uint8Array } | undefined;
if (!credential) throw new Error("Provider credential was not found.");
return credential;
return {
secret: this.authBroker.loadProviderSecret({
orgId,
providerId: provider.id,
providerKey: provider.provider_key,
credentialId: provider.credential_id,
}),
};
}
private requireAdapter(providerKey: string, authType: string): ProviderAdapterRegistry[string] {
@@ -317,6 +313,12 @@ export class ProviderService {
return adapter;
}
private requireCatalogEntry(providerKey: string): (typeof PROVIDER_CATALOG)[number] {
const entry = PROVIDER_CATALOG.find((item) => item.key === providerKey);
if (!entry) throw new Error(`Provider catalog entry missing for ${providerKey}.`);
return entry;
}
private attachNavigationPolicy(win: BrowserWindow, signInUrl: string): void {
const allowedHost = new URL(signInUrl).hostname.toLowerCase();
const allow = (url: string) => {
@@ -358,7 +360,7 @@ export class ProviderService {
interface ProviderRow {
id: string;
provider_key: string;
auth_type: string;
auth_type: ProviderAuthType;
model: string | null;
credential_id: string | null;
enabled: number;
@@ -43,6 +43,8 @@ export class SnapshotService {
.map((workspace) => ({ ...workspace, isPrimary: toBool(workspace.isPrimary) }));
const providers = list<AppSnapshot["providers"][number]>(this.db, "providers", "label asc")
.map((provider) => ({ ...provider, enabled: toBool(provider.enabled), backgroundEnabled: toBool(provider.backgroundEnabled) }));
const providerAuthProfiles = list<AppSnapshot["providerAuthProfiles"][number]>(this.db, "provider_auth_profiles", "created_at desc")
.map((profile) => ({ ...profile, active: toBool(profile.active) }));
const skillBindings = list<AppSnapshot["skillBindings"][number]>(this.db, "skill_bindings", "priority asc")
.map((binding) => ({ ...binding, enabled: toBool(binding.enabled) }));
const files = list<AppSnapshot["files"][number]>(this.db, "file_objects", "created_at desc")
@@ -90,6 +92,7 @@ export class SnapshotService {
fileAccessGrants: list(this.db, "file_access_grants", "created_at desc"),
scheduledPosts: list(this.db, "scheduled_posts", "scheduled_for asc"),
providers,
providerAuthProfiles,
providerCatalog: PROVIDER_CATALOG,
providerRequests: list(this.db, "provider_requests", "created_at desc"),
skillDefinitions: list(this.db, "skill_definitions", "created_at asc"),
+3
View File
@@ -4,6 +4,7 @@ import type { ComplianceExportRecord, LegalHoldRecord, RetentionFreezeRecord } f
import type { AccessDecisionRecord, EmployeeIdentityRecord, RoleGrantRecord, ServiceIdentityRecord } from "./identity-domain";
import type { ApprovalAssignmentRecord, ApprovalRoutingRuleRecord, HiringPolicyRecord, OrgDepartmentRecord } from "./org-governance-domain";
import type { ChaosDrillRecord, ChaosDrillRunRecord, FeatureFlagRecord, OperationalEventRecord, SloTargetRecord } from "./operations-domain";
import type { ProviderAuthProfileRecord } from "./provider-auth-domain";
import type { ProviderDataPolicy, ProviderExecutionMode } from "./provider-policy-domain";
import type { RollbackSnapshotRecord } from "./rollback-domain";
import type { EvalCaseRecord, EvalRunRecord, EvalSuiteRecord, PrBotGateRecord, SdlcReviewAgentRecord, SdlcReviewRunRecord } from "./sdlc-domain";
@@ -13,6 +14,7 @@ export { COMPLIANCE_EXPORT_STATE, COMPLIANCE_EXPORT_TYPE, type ComplianceExportR
export { ACCESS_DECISION, IDENTITY_SOURCE, ROLE_KEY, type AccessDecision, type AccessDecisionRecord, type EmployeeIdentityRecord, type IdentitySource, type PrincipalType, type RoleGrantRecord, type RoleKey, type ServiceIdentityRecord } from "./identity-domain";
export { ROUTE_TO_TYPE, type ApprovalAssignmentRecord, type ApprovalRoutingRuleRecord, type HiringPolicyRecord, type OrgDepartmentRecord, type RouteToType } from "./org-governance-domain";
export { CHAOS_DRILL_SCENARIO, CHAOS_DRILL_STATE, FEATURE_FLAG_KEY, SLO_SIGNAL_KEY, type ChaosDrillRecord, type ChaosDrillRunRecord, type ChaosDrillScenario, type ChaosDrillState, type FeatureFlagKey, type FeatureFlagRecord, type OperationalEventRecord, type SloSignalKey, type SloTargetRecord } from "./operations-domain";
export type { ProviderAuthProfileRecord } from "./provider-auth-domain";
export { PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, type ProviderDataPolicy, type ProviderExecutionMode } from "./provider-policy-domain";
export type { RollbackSnapshotRecord } from "./rollback-domain";
export { EVAL_SUITE_KEY, PR_BOT_GATE_KEY, SDLC_REVIEW_AGENT_KEY, SDLC_REVIEW_TARGET_TYPE, SDLC_RUN_STATE, type EvalCaseRecord, type EvalRunRecord, type EvalSuiteKey, type EvalSuiteRecord, type PrBotGateKey, type PrBotGateRecord, type SdlcReviewAgentKey, type SdlcReviewAgentRecord, type SdlcReviewRunRecord, type SdlcReviewTargetType, type SdlcRunState } from "./sdlc-domain";
@@ -465,6 +467,7 @@ export interface AppSnapshot {
fileAccessGrants: FileAccessGrantRecord[];
scheduledPosts: ScheduledPostRecord[];
providers: ProviderRecord[];
providerAuthProfiles: ProviderAuthProfileRecord[];
providerCatalog: ProviderCatalogEntry[];
providerRequests: ProviderRequestRecord[];
skillDefinitions: SkillDefinitionRecord[];
@@ -0,0 +1,17 @@
import type { ProviderAuthType } from "./domain";
export interface ProviderAuthProfileRecord {
id: string;
orgId: string;
providerId: string;
credentialId: string;
providerKey: string;
authType: ProviderAuthType;
profileKind: string;
displayHint: string;
sourceType: string;
sourceRef: string | null;
capabilityJson: string;
active: boolean;
createdAt: string;
}
@@ -23,7 +23,7 @@ describe("domain services", () => {
const connection = createConnection();
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]);
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]);
connection.close();
});
@@ -0,0 +1,79 @@
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { describe, expect, test } from "vitest";
import { providerAuthBrokerSchema } from "../../src/main/database/migrations/021-provider-auth-broker";
function createDatabase(): DatabaseSync {
return new DatabaseSync(join(tmpdir(), `agentcompany-auth-broker-migration-${randomUUID()}.db`));
}
describe("provider auth broker migration", () => {
test("backfills active auth profiles from configured credentials", () => {
const db = createDatabase();
db.exec(`
create table schema_migrations(version integer primary key, name text not null, applied_at text not null);
create table organizations(id text primary key);
create table providers(
id text primary key,
org_id text not null,
provider_key text not null,
auth_type text not null,
credential_id text
);
create table credentials(
id text primary key,
org_id text not null,
provider_key text not null,
kind text not null,
ciphertext blob not null,
display_hint text not null,
created_at text not null
);
insert into schema_migrations(version, name, applied_at) values (20, 'rollback_snapshots', datetime('now'));
insert into organizations(id) values ('org_1');
insert into credentials(id, org_id, provider_key, kind, ciphertext, display_hint, created_at)
values ('cred_1', 'org_1', 'openai', 'api_key', x'736563726574', 'sk-liv...ider', '2026-05-23T01:02:03.000Z');
insert into providers(id, org_id, provider_key, auth_type, credential_id)
values ('prov_1', 'org_1', 'openai', 'api_key', 'cred_1');
`);
db.exec(providerAuthBrokerSchema);
const profile = db.prepare(`
select org_id, provider_id, credential_id, provider_key, auth_type,
profile_kind, display_hint, source_type, capability_json, active, created_at
from provider_auth_profiles
`).get() as {
org_id: string;
provider_id: string;
credential_id: string;
provider_key: string;
auth_type: string;
profile_kind: string;
display_hint: string;
source_type: string;
capability_json: string;
active: number;
created_at: string;
};
const migration = db.prepare("select version, name from schema_migrations where version = 21").get();
expect(profile).toMatchObject({
org_id: "org_1",
provider_id: "prov_1",
credential_id: "cred_1",
provider_key: "openai",
auth_type: "api_key",
profile_kind: "api_key",
display_hint: "sk-liv...ider",
source_type: "migrated_credential",
active: 1,
created_at: "2026-05-23T01:02:03.000Z",
});
expect(JSON.parse(profile.capability_json)).toEqual({ providerKey: "openai", authType: "api_key", capabilityTags: [] });
expect(migration).toEqual({ version: 21, name: "provider_auth_broker" });
db.close();
});
});
@@ -41,6 +41,46 @@ describe("provider service", () => {
}
});
test("creates active auth broker profiles during credential rotation", () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);
const snapshot = new SnapshotService(connection.db);
const providers = new ProviderService(connection.db, providerAdapters, testVault);
let orgId = "";
let openAiProviderId = "";
connection.transaction(() => {
orgId = createOrganization(organization);
openAiProviderId = providerId(connection, orgId, "openai");
providers.saveCredential({ orgId, providerId: openAiProviderId, secret: "sk-live-provider-one", model: "gpt-5.4" });
providers.saveCredential({ orgId, providerId: openAiProviderId, secret: "sk-live-provider-two", model: "gpt-5.4" });
});
const rows = connection.db.prepare(`
select credential_id, display_hint, source_type, capability_json, active
from provider_auth_profiles
where org_id = ? and provider_id = ?
order by active desc, created_at desc
`).all(orgId, openAiProviderId) as Array<{
credential_id: string;
display_hint: string;
source_type: string;
capability_json: string;
active: number;
}>;
const state = snapshot.load();
const activeProfile = state.providerAuthProfiles.find((profile) => profile.providerId === openAiProviderId && profile.active);
const provider = state.providers.find((item) => item.id === openAiProviderId);
expect(rows.map((row) => row.active)).toEqual([1, 0]);
expect(rows[0]).toMatchObject({ display_hint: "sk-liv...-two", source_type: "manual_secret" });
expect(JSON.parse(rows[0].capability_json)).toMatchObject({ providerKey: "openai", capabilityTags: ["responses", "tools", "background"] });
expect(provider?.credentialId).toBe(rows[0].credential_id);
expect(activeProfile?.credentialId).toBe(rows[0].credential_id);
expect(state.auditEvents.some((event) => event.entityType === "provider_auth_profile" && event.action === "created")).toBe(true);
connection.close();
});
test("checks provider health through its adapter", async () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);