@0xmaxma/claude-gateway 1.3.7 → 1.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -281,6 +281,42 @@ export class DiscordModule implements ChannelModule {
281
281
  this.client.on('interactionCreate', async (interaction: ButtonInteraction) => {
282
282
  try {
283
283
  if (!interaction.isButton?.()) return;
284
+
285
+ // Cancel button: send ESC sentinel to dismiss the pending menu cleanly.
286
+ if ((interaction.customId ?? '') === 'menu:cancel') {
287
+ const isDM = !interaction.guildId;
288
+ const isThread = interaction.channel?.isThread?.() ?? false;
289
+ const context: DiscordMessageContext = {
290
+ guildId: interaction.guildId ?? null,
291
+ channelId: interaction.channelId,
292
+ threadId: isThread ? interaction.channelId : null,
293
+ userId: interaction.user.id,
294
+ username: interaction.user.username,
295
+ messageId: interaction.message?.id ?? '',
296
+ isDM,
297
+ isThread,
298
+ };
299
+ const access = loadAccessFn();
300
+ const result = gate(access, context, saveAccessFn, () => randomBytes(3).toString('hex'));
301
+ if (result.action !== 'deliver') {
302
+ await interaction.reply({ content: 'Not authorized.', ephemeral: true }).catch(() => {});
303
+ return;
304
+ }
305
+ await interaction.update({ components: [] }).catch(() => {});
306
+ const inbound: InboundMessage = {
307
+ channel: 'discord',
308
+ accountId: interaction.client.user?.id ?? 'discord',
309
+ senderId: interaction.user.id,
310
+ chatId: interaction.channelId,
311
+ chatType: isDM ? 'direct' : 'group',
312
+ text: '__MENU_CANCEL__',
313
+ messageId: interaction.message?.id ?? '',
314
+ ts: Date.now(),
315
+ };
316
+ await handler(inbound);
317
+ return;
318
+ }
319
+
284
320
  const m = /^choice:(\d+)$/.exec(interaction.customId ?? '');
285
321
  if (!m) return;
286
322
 
@@ -88,6 +88,11 @@ export function buildChoiceComponents(options: Array<{ label: string }>): unknow
88
88
  });
89
89
  rows.push({ type: 1, components: buttons }); // ActionRow
90
90
  }
91
+ // Always append a cancel button (Danger style) so the user can dismiss the
92
+ // menu cleanly. Sends ESC to the PTY without injecting text into Claude's context.
93
+ if (rows.length > 0 && rows.length < 5) {
94
+ rows.push({ type: 1, components: [{ type: 2, style: 4, label: '❌ Cancel', custom_id: 'menu:cancel' }] });
95
+ }
91
96
  return rows;
92
97
  }
93
98
 
@@ -33,5 +33,9 @@ export function parseMenuFileContent(raw: string): MenuMessage | null {
33
33
  text: `${i + 1}. ${label}`.slice(0, 60),
34
34
  callback_data: `choice:${i + 1}`,
35
35
  }]);
36
+ // Always append a cancel button so the user can dismiss the menu without
37
+ // sending a numbered reply. Tapping it sends ESC to the PTY, clearing
38
+ // pendingMenu cleanly without injecting spurious text into Claude's context.
39
+ inline_keyboard.push([{ text: '❌ Cancel', callback_data: 'menu:cancel' }]);
36
40
  return { text, inline_keyboard };
37
41
  }
@@ -1153,6 +1153,13 @@ bot.command('clear', async ctx => {
1153
1153
  ).catch(() => {})
1154
1154
  })
1155
1155
 
1156
+ // Shared auth check for all callback_query:data handlers.
1157
+ // Mirrors the text-reply path: sender must be in allowFrom.
1158
+ function isCallbackAuthorized(ctx: Context): boolean {
1159
+ const access = loadAccess()
1160
+ return access.allowFrom.includes(String(ctx.from?.id ?? ''))
1161
+ }
1162
+
1156
1163
  // Inline-button handler for permission requests. Callback data is
1157
1164
  // `perm:allow:<id>`, `perm:deny:<id>`, or `perm:more:<id>`.
1158
1165
  // Security mirrors the text-reply path: allowFrom must contain the sender.
@@ -1165,8 +1172,7 @@ bot.on('callback_query:data', async ctx => {
1165
1172
  // text path: the tapper must be in allowFrom.
1166
1173
  const choiceMatch = /^choice:(\d+)$/.exec(data)
1167
1174
  if (choiceMatch) {
1168
- const access = loadAccess()
1169
- if (!access.allowFrom.includes(String(ctx.from.id))) {
1175
+ if (!isCallbackAuthorized(ctx)) {
1170
1176
  await ctx.answerCallbackQuery({ text: 'Not authorized.' }).catch(() => {})
1171
1177
  return
1172
1178
  }
@@ -1198,11 +1204,42 @@ bot.on('callback_query:data', async ctx => {
1198
1204
  return
1199
1205
  }
1200
1206
 
1207
+ // Handle interactive-menu cancel: dismiss the pending menu without sending
1208
+ // any text to the session. Posts a special sentinel that the PTY wrapper
1209
+ // translates to ESC, clearing pendingMenu cleanly.
1210
+ if (data === 'menu:cancel') {
1211
+ if (!isCallbackAuthorized(ctx)) {
1212
+ await ctx.answerCallbackQuery({ text: 'Not authorized.' }).catch(() => {})
1213
+ return
1214
+ }
1215
+ const chat_id = String(ctx.callbackQuery.message?.chat.id ?? ctx.from.id)
1216
+ await ctx.editMessageReplyMarkup({ reply_markup: undefined }).catch(() => {})
1217
+ await ctx.answerCallbackQuery({ text: '✓ Cancelled' }).catch(() => {})
1218
+ const callbackUrl = process.env.CLAUDE_CHANNEL_CALLBACK
1219
+ if (callbackUrl) {
1220
+ fetch(callbackUrl, {
1221
+ method: 'POST',
1222
+ headers: { 'Content-Type': 'application/json' },
1223
+ body: JSON.stringify({
1224
+ content: '__MENU_CANCEL__',
1225
+ meta: {
1226
+ chat_id,
1227
+ user: ctx.from.username ?? String(ctx.from.id),
1228
+ user_id: String(ctx.from.id),
1229
+ ts: new Date().toISOString(),
1230
+ },
1231
+ }),
1232
+ }).catch(err => {
1233
+ process.stderr.write(`telegram channel: menu cancel callback POST failed: ${err}\n`)
1234
+ })
1235
+ }
1236
+ return
1237
+ }
1238
+
1201
1239
  // Handle model selection callback: model:<model_id>
1202
1240
  const modelMatch = /^model:(.+)$/.exec(data)
1203
1241
  if (modelMatch) {
1204
- const access = loadAccess()
1205
- if (!access.allowFrom.includes(String(ctx.from.id))) {
1242
+ if (!isCallbackAuthorized(ctx)) {
1206
1243
  await ctx.answerCallbackQuery({ text: 'Not authorized.' }).catch(() => {})
1207
1244
  return
1208
1245
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xmaxma/claude-gateway",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
4
4
  "description": "Multi-agent gateway for Claude",
5
5
  "repository": {
6
6
  "type": "git",