0ddeaadeee
api_key-принципал стал полноправным REST-принципалом → мог начеканить 24-ч
COLLAB-токен, который AuthenticationExtension не сверял с api_keys, и ревокация
не доставала websocket-редактирование. Блокировать /collab-token для api_key
нельзя (это правки агентов). Решение fail-closed:
- JwtCollabPayload расширен полем principal ('session'|'api_key') + apiKeyId.
generateCollabToken штампует его в КАЖДЫЙ токен: 'api_key'+apiKeyId, когда
чеканит api-key-принципал (внешний MCP-агент), иначе 'session'. Дискриминатор
ключуется на ОРИГИНЕ (наличии api-key), НЕ на actor:'agent' — внутренний
session-backed AI-агент остаётся 'session' и на no-check пути.
- /auth/collab-token читает подписью-выведенные req.raw.authType/apiKeyId
(не тело) и прокидывает origin через getCollabToken → generateCollabToken.
- doAuthenticate: при principal='api_key' на connect прогоняет
ApiKeyService.validate (отозванный ключ → новых collab-подключений нет; инфра
пробрасывается). api_key без apiKeyId — reject. Claimless-токен доверяется
только В ПРЕДЕЛАХ 24-ч grace-окна роллаута (легаси session-токен, api_key его
начеканить не мог); после окна всякий валидный токен обязан нести
дискриминатор, поэтому claimless — баг и отвергается (не молча-доверие 24ч).
CollaborationModule импортирует ApiKeyModule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
77 lines
2.7 KiB
TypeScript
77 lines
2.7 KiB
TypeScript
import { Logger, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
|
import { AuthenticationExtension } from './extensions/authentication.extension';
|
|
import { PersistenceExtension } from './extensions/persistence.extension';
|
|
import { CollaborationGateway } from './collaboration.gateway';
|
|
import { HttpAdapterHost } from '@nestjs/core';
|
|
import { CollabWsAdapter } from './adapter/collab-ws.adapter';
|
|
import { IncomingMessage } from 'http';
|
|
import { WebSocket } from 'ws';
|
|
import { TokenModule } from '../core/auth/token.module';
|
|
import { HistoryProcessor } from './processors/history.processor';
|
|
import { LoggerExtension } from './extensions/logger.extension';
|
|
import { CollaborationHandler } from './collaboration.handler';
|
|
import { CollabHistoryService } from './services/collab-history.service';
|
|
import { WatcherModule } from '../core/watcher/watcher.module';
|
|
import { TransclusionService } from '../core/page/transclusion/transclusion.service';
|
|
import { TransclusionModule } from '../core/page/transclusion/transclusion.module';
|
|
import { StorageModule } from '../integrations/storage/storage.module';
|
|
import { EnvironmentModule } from '../integrations/environment/environment.module';
|
|
import { ApiKeyModule } from '../core/api-key/api-key.module';
|
|
|
|
@Module({
|
|
providers: [
|
|
CollaborationGateway,
|
|
AuthenticationExtension,
|
|
PersistenceExtension,
|
|
LoggerExtension,
|
|
HistoryProcessor,
|
|
CollabHistoryService,
|
|
CollaborationHandler,
|
|
TransclusionService,
|
|
],
|
|
exports: [CollaborationGateway],
|
|
imports: [
|
|
TokenModule,
|
|
ApiKeyModule,
|
|
WatcherModule,
|
|
StorageModule.forRootAsync({
|
|
imports: [EnvironmentModule],
|
|
}),
|
|
TransclusionModule,
|
|
],
|
|
})
|
|
export class CollaborationModule implements OnModuleInit, OnModuleDestroy {
|
|
private readonly logger = new Logger(CollaborationModule.name);
|
|
private collabWsAdapter: CollabWsAdapter;
|
|
private path = '/collab';
|
|
|
|
constructor(
|
|
private readonly collaborationGateway: CollaborationGateway,
|
|
private readonly httpAdapterHost: HttpAdapterHost,
|
|
) {}
|
|
|
|
onModuleInit() {
|
|
this.collabWsAdapter = new CollabWsAdapter();
|
|
const httpServer = this.httpAdapterHost.httpAdapter.getHttpServer();
|
|
|
|
const wss = this.collabWsAdapter.handleUpgrade(this.path, httpServer);
|
|
|
|
wss.on('connection', (client: WebSocket, request: IncomingMessage) => {
|
|
this.collaborationGateway.handleConnection(client, request);
|
|
|
|
client.on('error', (error) => {
|
|
this.logger.error('WebSocket client error:', error);
|
|
});
|
|
});
|
|
|
|
wss.on('error', (error) =>
|
|
this.logger.error('WebSocket server error:', error),
|
|
);
|
|
}
|
|
|
|
async onModuleDestroy(): Promise<void> {
|
|
await this.collaborationGateway?.destroy(this.collabWsAdapter);
|
|
this.collabWsAdapter?.destroy();
|
|
}
|
|
}
|