feat: add local provider account connector

This commit is contained in:
thanhnv
2026-07-10 23:05:24 +09:00
parent adce7717f7
commit 7cb592a32f
9 changed files with 380 additions and 3 deletions
@@ -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');
}
}
}