feat: add local provider account connector
This commit is contained in:
@@ -5,9 +5,10 @@ import { SettingsModule } from './settings/settings.module.js';
|
||||
import { KillSwitchModule } from './kill-switch/kill-switch.module.js';
|
||||
import { ApprovalsModule } from './approvals/approvals.module.js';
|
||||
import { ChatModule } from './chat/chat.module.js';
|
||||
import { ProviderAuthModule } from './provider-auth/provider-auth.module.js';
|
||||
|
||||
@Module({
|
||||
imports: [TelemetryModule, SettingsModule, KillSwitchModule, ApprovalsModule, ChatModule],
|
||||
imports: [TelemetryModule, SettingsModule, KillSwitchModule, ApprovalsModule, ChatModule, ProviderAuthModule],
|
||||
controllers: [HealthController],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Controller, Get, Headers, Inject, Param, Post } from '@nestjs/common';
|
||||
import { ok } from '../common/api-response.js';
|
||||
import { actorFromHeaders } from '../common/auth-context.js';
|
||||
import { ProviderAuthService } from './provider-auth.service.js';
|
||||
|
||||
@Controller('api/v1/provider-auth')
|
||||
export class ProviderAuthController {
|
||||
constructor(@Inject(ProviderAuthService) private readonly service: ProviderAuthService) {}
|
||||
|
||||
@Get()
|
||||
async status(@Headers() headers: Record<string, string | string[] | undefined>) {
|
||||
return ok(await this.service.status(actorFromHeaders(headers)));
|
||||
}
|
||||
|
||||
@Post(':provider/login')
|
||||
async login(
|
||||
@Param('provider') provider: string,
|
||||
@Headers() headers: Record<string, string | string[] | undefined>,
|
||||
) {
|
||||
return ok(await this.service.login(provider, actorFromHeaders(headers)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ProviderAuthController } from './provider-auth.controller.js';
|
||||
import { ProviderAuthService } from './provider-auth.service.js';
|
||||
|
||||
@Module({
|
||||
controllers: [ProviderAuthController],
|
||||
providers: [ProviderAuthService],
|
||||
})
|
||||
export class ProviderAuthModule {}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { ForbiddenException, Injectable, ServiceUnavailableException } from '@nestjs/common';
|
||||
import type { SettingsActor } from '../settings/settings.service.js';
|
||||
|
||||
export interface ProviderAuthStatus {
|
||||
id: 'codex' | 'claude';
|
||||
label: string;
|
||||
available: boolean;
|
||||
loggedIn: boolean;
|
||||
authenticating: boolean;
|
||||
authMethod: string;
|
||||
}
|
||||
|
||||
interface BridgeStatusResponse {
|
||||
success: boolean;
|
||||
providers: ProviderAuthStatus[];
|
||||
}
|
||||
|
||||
interface BridgeLoginResponse {
|
||||
success: boolean;
|
||||
reason: string;
|
||||
provider: ProviderAuthStatus;
|
||||
}
|
||||
|
||||
const PROVIDERS = new Set(['codex', 'claude']);
|
||||
|
||||
@Injectable()
|
||||
export class ProviderAuthService {
|
||||
private readonly bridgeUrl = (process.env.CASAN_AUTH_BRIDGE_URL || 'http://host.docker.internal:20130').replace(/\/$/, '');
|
||||
private readonly bridgeToken = process.env.CASAN_AUTH_BRIDGE_TOKEN || '';
|
||||
|
||||
async status(actor: SettingsActor): Promise<BridgeStatusResponse> {
|
||||
this.requireRead(actor);
|
||||
return this.bridgeRequest<BridgeStatusResponse>('/v1/auth/providers', 'GET');
|
||||
}
|
||||
|
||||
async login(provider: string, actor: SettingsActor): Promise<BridgeLoginResponse> {
|
||||
this.requireAdmin(actor);
|
||||
if (!PROVIDERS.has(provider)) throw new ForbiddenException('PROVIDER_AUTH_UNKNOWN_PROVIDER');
|
||||
return this.bridgeRequest<BridgeLoginResponse>(`/v1/auth/${provider}/login`, 'POST');
|
||||
}
|
||||
|
||||
private async bridgeRequest<T>(path: string, method: 'GET' | 'POST'): Promise<T> {
|
||||
if (!this.bridgeToken) throw new ServiceUnavailableException('PROVIDER_AUTH_BRIDGE_NOT_CONFIGURED');
|
||||
try {
|
||||
const response = await fetch(`${this.bridgeUrl}${path}`, {
|
||||
method,
|
||||
headers: { 'X-CASAN-Bridge-Token': this.bridgeToken },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
const payload = await response.json() as T & { reason?: string };
|
||||
if (!response.ok) throw new ServiceUnavailableException(payload.reason || 'PROVIDER_AUTH_BRIDGE_FAILED');
|
||||
return payload;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof ServiceUnavailableException) throw error;
|
||||
throw new ServiceUnavailableException('PROVIDER_AUTH_BRIDGE_UNREACHABLE');
|
||||
}
|
||||
}
|
||||
|
||||
private requireRead(actor: SettingsActor) {
|
||||
if (!['viewer', 'auditor', 'operator', 'project-admin', 'org-admin'].includes(actor.role)) {
|
||||
throw new ForbiddenException('PROVIDER_AUTH_READ_DENIED');
|
||||
}
|
||||
}
|
||||
|
||||
private requireAdmin(actor: SettingsActor) {
|
||||
if (!['project-admin', 'org-admin'].includes(actor.role)) {
|
||||
throw new ForbiddenException('PROVIDER_AUTH_ADMIN_REQUIRED');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user