From 689d5b68706fd5f5b190b0e4d988efc59fb51acb Mon Sep 17 00:00:00 2001 From: Jarvis Date: Mon, 13 Jul 2026 00:48:53 -0500 Subject: [PATCH] fix(#709): route Discord threads and approvals --- apps/gateway/src/chat/chat.gateway.ts | 29 ++++++++++++++++++++++++++- plugins/discord/src/index.ts | 7 +++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/apps/gateway/src/chat/chat.gateway.ts b/apps/gateway/src/chat/chat.gateway.ts index 566096a..9f55d4a 100644 --- a/apps/gateway/src/chat/chat.gateway.ts +++ b/apps/gateway/src/chat/chat.gateway.ts @@ -639,9 +639,36 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa * Creates it if absent — safe to call concurrently since a duplicate insert * would fail on the PK constraint and be caught here. */ + @SubscribeMessage('discord:approve') + async handleDiscordApproval( + @ConnectedSocket() client: Socket, + @MessageBody() envelope: DiscordIngressEnvelope, + ): Promise { + if (!client.data.discordService) return; + const ingress = this.resolveDiscordIngress(client, envelope, 'approve'); + const command = ingress?.content.match(/^\/approve\s+([a-z][\w-]*)$/i)?.[1]; + const serviceUserId = process.env['DISCORD_SERVICE_USER_ID']; + if (!ingress || !command || !serviceUserId) return; + const approval = await this.commandExecutor.createApproval( + { command, conversationId: ingress.conversationId }, + { + userId: serviceUserId, + tenantId: process.env['DISCORD_SERVICE_TENANT_ID'] ?? serviceUserId, + }, + ); + client.emit('discord:approval', { + correlationId: ingress.correlationId, + command, + success: approval !== null, + approvalId: approval?.approvalId, + expiresAt: approval?.expiresAt, + }); + } + private resolveDiscordIngress( client: Socket, envelope: DiscordIngressEnvelope, + operation: 'send' | 'approve' = 'send', ): DiscordIngressPayload | null { const payload = verifyDiscordIngressEnvelope( envelope, @@ -662,7 +689,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa payload.guildId, payload.channelId, payload.userId, - 'send', + operation, ); if (!binding) { this.logger.warn(`Rejected unpaired Discord ingress from ${client.id}`); diff --git a/plugins/discord/src/index.ts b/plugins/discord/src/index.ts index c752c03..eaf0bd9 100644 --- a/plugins/discord/src/index.ts +++ b/plugins/discord/src/index.ts @@ -327,15 +327,18 @@ export class DiscordPlugin { }, this.config.serviceToken, ); - this.socket.emit('message', envelope); + this.socket.emit(content.startsWith('/approve ') ? 'discord:approve' : 'message', envelope); } private isAllowedMessage(message: DiscordMessage): boolean { const guildId = message.guildId; + const parentChannelId = 'parentId' in message.channel ? message.channel.parentId : null; + // Threads inherit their authorization boundary from their configured parent. + const authorizationChannelId = parentChannelId ?? message.channelId; return ( guildId !== null && includesId(this.config.allowedGuildIds, guildId) && - includesId(this.config.allowedChannelIds, message.channelId) && + includesId(this.config.allowedChannelIds, authorizationChannelId) && includesId(this.config.allowedUserIds, message.author.id) ); }