fix(#709): route Discord threads and approvals
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
Jarvis
2026-07-13 00:48:53 -05:00
parent 25ed367655
commit 689d5b6870
2 changed files with 33 additions and 3 deletions

View File

@@ -639,9 +639,36 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
* Creates it if absent — safe to call concurrently since a duplicate insert * Creates it if absent — safe to call concurrently since a duplicate insert
* would fail on the PK constraint and be caught here. * would fail on the PK constraint and be caught here.
*/ */
@SubscribeMessage('discord:approve')
async handleDiscordApproval(
@ConnectedSocket() client: Socket,
@MessageBody() envelope: DiscordIngressEnvelope,
): Promise<void> {
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( private resolveDiscordIngress(
client: Socket, client: Socket,
envelope: DiscordIngressEnvelope, envelope: DiscordIngressEnvelope,
operation: 'send' | 'approve' = 'send',
): DiscordIngressPayload | null { ): DiscordIngressPayload | null {
const payload = verifyDiscordIngressEnvelope( const payload = verifyDiscordIngressEnvelope(
envelope, envelope,
@@ -662,7 +689,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
payload.guildId, payload.guildId,
payload.channelId, payload.channelId,
payload.userId, payload.userId,
'send', operation,
); );
if (!binding) { if (!binding) {
this.logger.warn(`Rejected unpaired Discord ingress from ${client.id}`); this.logger.warn(`Rejected unpaired Discord ingress from ${client.id}`);

View File

@@ -327,15 +327,18 @@ export class DiscordPlugin {
}, },
this.config.serviceToken, this.config.serviceToken,
); );
this.socket.emit('message', envelope); this.socket.emit(content.startsWith('/approve ') ? 'discord:approve' : 'message', envelope);
} }
private isAllowedMessage(message: DiscordMessage): boolean { private isAllowedMessage(message: DiscordMessage): boolean {
const guildId = message.guildId; 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 ( return (
guildId !== null && guildId !== null &&
includesId(this.config.allowedGuildIds, guildId) && includesId(this.config.allowedGuildIds, guildId) &&
includesId(this.config.allowedChannelIds, message.channelId) && includesId(this.config.allowedChannelIds, authorizationChannelId) &&
includesId(this.config.allowedUserIds, message.author.id) includesId(this.config.allowedUserIds, message.author.id)
); );
} }