@ihazz/bitrix24 0.1.5 → 0.2.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ihazz/bitrix24",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "Bitrix24 Messenger channel for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,11 +1,12 @@
1
1
  import type { Bitrix24AccountConfig } from './types.js';
2
+ import type { PluginRuntime, ChannelPairingAdapter } from './runtime.js';
2
3
 
3
4
  /**
4
5
  * Normalize an allowFrom entry — strip platform prefixes.
5
6
  * "bitrix24:42" → "42", "b24:42" → "42", "bx24:42" → "42", "42" → "42"
6
7
  */
7
8
  export function normalizeAllowEntry(entry: string): string {
8
- return entry.trim().replace(/^(bitrix24|b24|bx24):/, '');
9
+ return entry.trim().replace(/^(bitrix24|b24|bx24):/i, '');
9
10
  }
10
11
 
11
12
  /**
@@ -17,7 +18,7 @@ export function checkAccess(
17
18
  senderId: string,
18
19
  config: Bitrix24AccountConfig,
19
20
  ): boolean {
20
- const policy = config.dmPolicy ?? 'open';
21
+ const policy = config.dmPolicy ?? 'pairing';
21
22
 
22
23
  switch (policy) {
23
24
  case 'open':
@@ -31,10 +32,66 @@ export function checkAccess(
31
32
  }
32
33
 
33
34
  case 'pairing':
34
- // Pairing mode is post-MVP for now, treat as open
35
- return true;
35
+ // Pairing requires runtimeuse checkAccessWithPairing() instead
36
+ return false;
36
37
 
37
38
  default:
38
39
  return false;
39
40
  }
40
41
  }
42
+
43
+ export type AccessResult = 'allow' | 'deny' | 'pairing';
44
+
45
+ /**
46
+ * Pairing-aware access check.
47
+ * Merges config allowFrom with file-based allowFrom store.
48
+ * For pairing mode, upserts a pairing request and sends the reply.
49
+ */
50
+ export async function checkAccessWithPairing(params: {
51
+ senderId: string;
52
+ config: Bitrix24AccountConfig;
53
+ runtime: PluginRuntime;
54
+ accountId: string;
55
+ pairingAdapter: ChannelPairingAdapter;
56
+ sendReply: (text: string) => Promise<void>;
57
+ logger?: { debug: (...args: unknown[]) => void };
58
+ }): Promise<AccessResult> {
59
+ const { senderId, config, runtime, accountId, pairingAdapter, sendReply, logger } = params;
60
+ const policy = config.dmPolicy ?? 'pairing';
61
+
62
+ if (policy === 'open') return 'allow';
63
+
64
+ // Read file-based allowFrom store and merge with config
65
+ const storeAllowFrom = await runtime.channel.pairing.readAllowFromStore('bitrix24', '', accountId);
66
+ const configAllowFrom = (config.allowFrom ?? []).map(normalizeAllowEntry);
67
+ const merged = [...new Set([...configAllowFrom, ...storeAllowFrom])];
68
+ const normalizedSender = normalizeAllowEntry(String(senderId));
69
+
70
+ if (merged.includes(normalizedSender)) return 'allow';
71
+
72
+ if (policy === 'allowlist') {
73
+ logger?.debug('Access denied (allowlist)', { senderId });
74
+ return 'deny';
75
+ }
76
+
77
+ // policy === 'pairing'
78
+ const { code, created } = await runtime.channel.pairing.upsertPairingRequest({
79
+ channel: 'bitrix24',
80
+ id: senderId,
81
+ accountId,
82
+ meta: {},
83
+ pairingAdapter,
84
+ });
85
+
86
+ if (created) {
87
+ const reply = runtime.channel.pairing.buildPairingReply({
88
+ code,
89
+ channel: 'bitrix24',
90
+ accountId,
91
+ });
92
+ await sendReply(reply.text);
93
+ }
94
+
95
+ logger?.debug('Pairing request handled', { senderId, code, created });
96
+ return 'pairing';
97
+ }
package/src/api.ts CHANGED
@@ -287,6 +287,98 @@ export class Bitrix24Api {
287
287
  return result.result;
288
288
  }
289
289
 
290
+ // ─── File / Disk methods ─────────────────────────────────────────────
291
+
292
+ /**
293
+ * Get file info including DOWNLOAD_URL.
294
+ * Uses the user's access token (file belongs to the user's disk).
295
+ */
296
+ async getFileInfo(
297
+ clientEndpoint: string,
298
+ accessToken: string,
299
+ fileId: number,
300
+ ): Promise<{ DOWNLOAD_URL: string; [key: string]: unknown }> {
301
+ const result = await this.callWithToken<{ DOWNLOAD_URL: string; [key: string]: unknown }>(
302
+ clientEndpoint,
303
+ 'disk.file.get',
304
+ accessToken,
305
+ { id: fileId },
306
+ );
307
+ return result.result;
308
+ }
309
+
310
+ async getFileInfoViaWebhook(
311
+ webhookUrl: string,
312
+ fileId: number,
313
+ ): Promise<{ DOWNLOAD_URL: string; [key: string]: unknown }> {
314
+ const result = await this.callWebhook<{ DOWNLOAD_URL: string; [key: string]: unknown }>(
315
+ webhookUrl,
316
+ 'disk.file.get',
317
+ { id: fileId },
318
+ );
319
+ return result.result;
320
+ }
321
+
322
+ /**
323
+ * Get the disk folder ID for a chat (needed for file uploads).
324
+ */
325
+ async getChatFolder(
326
+ clientEndpoint: string,
327
+ accessToken: string,
328
+ chatId: number,
329
+ ): Promise<number> {
330
+ const result = await this.callWithToken<{ ID: number; [key: string]: unknown }>(
331
+ clientEndpoint,
332
+ 'im.disk.folder.get',
333
+ accessToken,
334
+ { CHAT_ID: chatId },
335
+ );
336
+ return result.result.ID;
337
+ }
338
+
339
+ /**
340
+ * Upload a file to a disk folder (base64 encoded).
341
+ * Returns the disk file ID.
342
+ */
343
+ async uploadFile(
344
+ clientEndpoint: string,
345
+ accessToken: string,
346
+ folderId: number,
347
+ fileName: string,
348
+ content: Buffer,
349
+ ): Promise<number> {
350
+ const result = await this.callWithToken<{ ID: number; [key: string]: unknown }>(
351
+ clientEndpoint,
352
+ 'disk.folder.uploadfile',
353
+ accessToken,
354
+ {
355
+ id: folderId,
356
+ data: { NAME: fileName },
357
+ fileContent: [fileName, content.toString('base64')],
358
+ generateUniqueName: true,
359
+ },
360
+ );
361
+ return result.result.ID;
362
+ }
363
+
364
+ /**
365
+ * Publish an uploaded file to a chat.
366
+ */
367
+ async commitFileToChat(
368
+ clientEndpoint: string,
369
+ accessToken: string,
370
+ chatId: number,
371
+ diskId: number,
372
+ ): Promise<boolean> {
373
+ const result = await this.callWithToken<boolean>(
374
+ clientEndpoint,
375
+ 'im.disk.file.commit',
376
+ accessToken,
377
+ { CHAT_ID: chatId, DISK_ID: diskId },
378
+ );
379
+ return result.result;
380
+ }
381
+
290
382
  destroy(): void {
291
383
  this.rateLimiter.destroy();
292
384
  }
package/src/channel.ts CHANGED
@@ -1,12 +1,16 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { basename } from 'node:path';
2
3
  import type { IncomingMessage, ServerResponse } from 'node:http';
3
4
  import { listAccountIds, resolveAccount, getConfig } from './config.js';
4
5
  import { Bitrix24Api } from './api.js';
5
6
  import { SendService } from './send-service.js';
7
+ import { MediaService } from './media-service.js';
8
+ import type { DownloadedMedia } from './media-service.js';
6
9
  import { InboundHandler } from './inbound-handler.js';
7
- import { checkAccess } from './access-control.js';
10
+ import { normalizeAllowEntry, checkAccessWithPairing } from './access-control.js';
8
11
  import { defaultLogger } from './utils.js';
9
12
  import { getBitrix24Runtime } from './runtime.js';
13
+ import type { ChannelPairingAdapter } from './runtime.js';
10
14
  import { OPENCLAW_COMMANDS } from './commands.js';
11
15
  import type {
12
16
  B24MsgContext,
@@ -28,25 +32,39 @@ interface Logger {
28
32
  interface GatewayState {
29
33
  api: Bitrix24Api;
30
34
  sendService: SendService;
35
+ mediaService: MediaService;
31
36
  inboundHandler: InboundHandler;
32
37
  }
33
38
 
34
39
  let gatewayState: GatewayState | null = null;
35
40
 
41
+ // ─── Default command keyboard ────────────────────────────────────────────────
42
+
43
+ /** Default keyboard shown with command responses and welcome messages. */
44
+ export const DEFAULT_COMMAND_KEYBOARD: B24Keyboard = [
45
+ { TEXT: 'Help', COMMAND: 'help', BG_COLOR_TOKEN: 'primary', DISPLAY: 'LINE' },
46
+ { TEXT: 'Status', COMMAND: 'status', DISPLAY: 'LINE' },
47
+ { TEXT: 'Commands', COMMAND: 'commands', DISPLAY: 'LINE' },
48
+ { TYPE: 'NEWLINE' },
49
+ { TEXT: 'New session', COMMAND: 'new', DISPLAY: 'LINE' },
50
+ { TEXT: 'Models', COMMAND: 'models', DISPLAY: 'LINE' },
51
+ ];
52
+
36
53
  // ─── Keyboard / Button conversion ────────────────────────────────────────────
37
54
 
38
- interface TelegramButton {
55
+ /** Generic button format used by OpenClaw channelData. */
56
+ export interface ChannelButton {
39
57
  text: string;
40
58
  callback_data?: string;
41
59
  style?: string;
42
60
  }
43
61
 
44
62
  /**
45
- * Convert Telegram-style button rows to B24 flat KEYBOARD array.
46
- * Telegram format: Array<Array<{ text, callback_data, style }>>
47
- * B24 format: flat array with { TYPE: 'NEWLINE' } separators between rows.
63
+ * Convert OpenClaw button rows to B24 flat KEYBOARD array.
64
+ * Input: Array<Array<{ text, callback_data, style }>>
65
+ * Output: flat array with { TYPE: 'NEWLINE' } separators between rows.
48
66
  */
49
- function convertButtonsToKeyboard(rows: TelegramButton[][]): B24Keyboard {
67
+ export function convertButtonsToKeyboard(rows: ChannelButton[][]): B24Keyboard {
50
68
  const keyboard: B24Keyboard = [];
51
69
 
52
70
  for (let i = 0; i < rows.length; i++) {
@@ -86,9 +104,9 @@ function convertButtonsToKeyboard(rows: TelegramButton[][]): B24Keyboard {
86
104
 
87
105
  /**
88
106
  * Extract B24 keyboard from a dispatcher payload's channelData.
89
- * Checks bitrix24-specific data first, then falls back to Telegram button format.
107
+ * Checks bitrix24-specific data first, then falls back to OpenClaw generic button format.
90
108
  */
91
- function extractKeyboardFromPayload(
109
+ export function extractKeyboardFromPayload(
92
110
  payload: { channelData?: Record<string, unknown> },
93
111
  ): B24Keyboard | undefined {
94
112
  const cd = payload.channelData;
@@ -100,8 +118,8 @@ function extractKeyboardFromPayload(
100
118
  return b24Data.keyboard;
101
119
  }
102
120
 
103
- // Translate from Telegram button format
104
- const tgData = cd.telegram as { buttons?: TelegramButton[][] } | undefined;
121
+ // Translate from OpenClaw generic button format (channelData.telegram key)
122
+ const tgData = cd.telegram as { buttons?: ChannelButton[][] } | undefined;
105
123
  if (tgData?.buttons?.length) {
106
124
  return convertButtonsToKeyboard(tgData.buttons);
107
125
  }
@@ -270,7 +288,7 @@ export const bitrix24Plugin = {
270
288
 
271
289
  capabilities: {
272
290
  chatTypes: ['direct', 'group'] as const,
273
- media: false,
291
+ media: true,
274
292
  reactions: false,
275
293
  threads: false,
276
294
  nativeCommands: true,
@@ -283,12 +301,33 @@ export const bitrix24Plugin = {
283
301
  },
284
302
 
285
303
  security: {
286
- resolveDmPolicy: (account: { config?: { dmPolicy?: string } }) =>
287
- account.config?.dmPolicy ?? 'open',
304
+ resolveDmPolicy: (params: { cfg?: Record<string, unknown>; accountId?: string; account?: { config?: Record<string, unknown> } }) => ({
305
+ policy: (params.account?.config?.dmPolicy as string) ?? 'pairing',
306
+ allowFrom: (params.account?.config?.allowFrom as string[]) ?? [],
307
+ policyPath: 'channels.bitrix24.dmPolicy',
308
+ allowFromPath: 'channels.bitrix24.',
309
+ approveHint: 'openclaw pairing approve bitrix24 <CODE>',
310
+ normalizeEntry: (raw: string) => raw.replace(/^(bitrix24|b24|bx24):/i, ''),
311
+ }),
288
312
  normalizeAllowFrom: (entry: string) =>
289
- entry.replace(/^(bitrix24|b24|bx24):/, ''),
313
+ entry.replace(/^(bitrix24|b24|bx24):/i, ''),
290
314
  },
291
315
 
316
+ pairing: {
317
+ idLabel: 'bitrix24UserId',
318
+ normalizeAllowEntry: (entry: string) => normalizeAllowEntry(entry),
319
+ notifyApproval: async (params: { cfg: Record<string, unknown>; id: string; runtime?: unknown }) => {
320
+ const { config: acctCfg } = resolveAccount(params.cfg);
321
+ if (!acctCfg.webhookUrl) return;
322
+ const api = new Bitrix24Api();
323
+ try {
324
+ await api.sendMessage(acctCfg.webhookUrl, params.id, '\u2705 OpenClaw access approved.');
325
+ } finally {
326
+ api.destroy();
327
+ }
328
+ },
329
+ } satisfies ChannelPairingAdapter,
330
+
292
331
  outbound: {
293
332
  deliveryMode: 'direct' as const,
294
333
 
@@ -409,6 +448,7 @@ export const bitrix24Plugin = {
409
448
  const clientId = createHash('md5').update(config.webhookUrl).digest('hex');
410
449
  const api = new Bitrix24Api({ logger, clientId });
411
450
  const sendService = new SendService(api, logger);
451
+ const mediaService = new MediaService(api, logger);
412
452
 
413
453
  // Register or update bot on the B24 portal
414
454
  const botId = await ensureBotRegistered(api, config, logger);
@@ -435,6 +475,65 @@ export const bitrix24Plugin = {
435
475
  const runtime = getBitrix24Runtime();
436
476
  const cfg = runtime.config.loadConfig();
437
477
 
478
+ // Pairing-aware access control
479
+ const accessResult = await checkAccessWithPairing({
480
+ senderId: msgCtx.senderId,
481
+ config,
482
+ runtime,
483
+ accountId: ctx.accountId,
484
+ pairingAdapter: bitrix24Plugin.pairing,
485
+ sendReply: async (text: string) => {
486
+ const replySendCtx = {
487
+ webhookUrl: config.webhookUrl,
488
+ clientEndpoint: msgCtx.clientEndpoint,
489
+ botToken: msgCtx.botToken,
490
+ dialogId: msgCtx.chatId,
491
+ };
492
+ await sendService.sendText(replySendCtx, text, { convertMarkdown: false });
493
+ },
494
+ logger,
495
+ });
496
+
497
+ if (accessResult !== 'allow') {
498
+ logger.debug(`Message blocked (${accessResult})`, { senderId: msgCtx.senderId });
499
+ return;
500
+ }
501
+
502
+ // Download media files if present
503
+ let mediaFields: Record<string, unknown> = {};
504
+ if (msgCtx.media.length > 0) {
505
+ const downloaded = (await Promise.all(
506
+ msgCtx.media.map((m) =>
507
+ mediaService.downloadMedia({
508
+ fileId: m.id,
509
+ fileName: m.name,
510
+ extension: m.extension,
511
+ clientEndpoint: msgCtx.clientEndpoint,
512
+ userToken: msgCtx.userToken,
513
+ webhookUrl: config.webhookUrl,
514
+ }),
515
+ ),
516
+ )).filter(Boolean) as DownloadedMedia[];
517
+
518
+ if (downloaded.length > 0) {
519
+ mediaFields = {
520
+ MediaPath: downloaded[0].path,
521
+ MediaType: downloaded[0].contentType,
522
+ MediaUrl: downloaded[0].path,
523
+ MediaPaths: downloaded.map((m) => m.path),
524
+ MediaUrls: downloaded.map((m) => m.path),
525
+ MediaTypes: downloaded.map((m) => m.contentType),
526
+ };
527
+ }
528
+ }
529
+
530
+ // Use placeholder body for media-only messages
531
+ let body = msgCtx.text;
532
+ if (!body && msgCtx.media.length > 0) {
533
+ const hasImage = msgCtx.media.some((m) => m.type === 'image');
534
+ body = hasImage ? '<media:image>' : '<media:document>';
535
+ }
536
+
438
537
  // Resolve which agent handles this conversation
439
538
  const route = runtime.channel.routing.resolveAgentRoute({
440
539
  cfg,
@@ -454,9 +553,9 @@ export const bitrix24Plugin = {
454
553
 
455
554
  // Build and finalize inbound context for OpenClaw agent
456
555
  const inboundCtx = runtime.channel.reply.finalizeInboundContext({
457
- Body: msgCtx.text,
458
- BodyForAgent: msgCtx.text,
459
- RawBody: msgCtx.text,
556
+ Body: body,
557
+ BodyForAgent: body,
558
+ RawBody: body,
460
559
  From: `bitrix24:${msgCtx.chatId}`,
461
560
  To: `bitrix24:${msgCtx.chatId}`,
462
561
  SessionKey: route.sessionKey,
@@ -473,6 +572,7 @@ export const bitrix24Plugin = {
473
572
  CommandAuthorized: true,
474
573
  OriginatingChannel: 'bitrix24',
475
574
  OriginatingTo: `bitrix24:${msgCtx.chatId}`,
575
+ ...mediaFields,
476
576
  });
477
577
 
478
578
  const sendCtx = {
@@ -489,6 +589,18 @@ export const bitrix24Plugin = {
489
589
  cfg,
490
590
  dispatcherOptions: {
491
591
  deliver: async (payload) => {
592
+ // Send media if present in reply
593
+ const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
594
+ for (const mediaUrl of mediaUrls) {
595
+ await mediaService.uploadMediaToChat({
596
+ localPath: mediaUrl,
597
+ fileName: basename(mediaUrl),
598
+ chatId: Number(msgCtx.chatInternalId),
599
+ clientEndpoint: msgCtx.clientEndpoint,
600
+ botToken: msgCtx.botToken,
601
+ });
602
+ }
603
+ // Send text if present
492
604
  if (payload.text) {
493
605
  const keyboard = extractKeyboardFromPayload(payload);
494
606
  await sendService.sendText(sendCtx, payload.text, keyboard ? { keyboard } : undefined);
@@ -529,14 +641,39 @@ export const bitrix24Plugin = {
529
641
 
530
642
  logger.info('Inbound command', { commandName, commandParams, senderId, dialogId });
531
643
 
532
- // Access control
533
- if (!checkAccess(senderId, config)) {
534
- logger.debug(`Access denied for command from user ${senderId}`);
644
+ let runtime;
645
+ let cfg;
646
+ try {
647
+ runtime = getBitrix24Runtime();
648
+ cfg = runtime.config.loadConfig();
649
+ } catch (err) {
650
+ logger.error('Failed to get runtime/config for command', err);
535
651
  return;
536
652
  }
537
653
 
538
- const runtime = getBitrix24Runtime();
539
- const cfg = runtime.config.loadConfig();
654
+ // Pairing-aware access control (commands don't send pairing replies)
655
+ let accessResult;
656
+ try {
657
+ accessResult = await checkAccessWithPairing({
658
+ senderId,
659
+ config,
660
+ runtime,
661
+ accountId: ctx.accountId,
662
+ pairingAdapter: bitrix24Plugin.pairing,
663
+ sendReply: async () => {},
664
+ logger,
665
+ });
666
+ } catch (err) {
667
+ logger.error('Access check failed for command', err);
668
+ return;
669
+ }
670
+
671
+ if (accessResult !== 'allow') {
672
+ logger.debug(`Command blocked (${accessResult})`, { senderId });
673
+ return;
674
+ }
675
+
676
+ logger.debug('Command access allowed, resolving route', { commandText });
540
677
 
541
678
  const route = runtime.channel.routing.resolveAgentRoute({
542
679
  cfg,
@@ -545,8 +682,11 @@ export const bitrix24Plugin = {
545
682
  peer: { kind: isDm ? 'direct' : 'group', id: dialogId },
546
683
  });
547
684
 
548
- // Native commands use a separate slash-command session (like Telegram)
549
- const slashSessionKey = `bitrix24:slash:${senderId}`;
685
+ logger.debug('Command route resolved', { sessionKey: route.sessionKey });
686
+
687
+ // Each command invocation gets a unique ephemeral session
688
+ // so the gateway doesn't treat it as "already handled".
689
+ const slashSessionKey = `bitrix24:slash:${senderId}:${Date.now()}`;
550
690
 
551
691
  const inboundCtx = runtime.channel.reply.finalizeInboundContext({
552
692
  Body: commandText,
@@ -580,15 +720,23 @@ export const bitrix24Plugin = {
580
720
  dialogId,
581
721
  };
582
722
 
723
+ logger.debug('Dispatching command to agent', { commandText, slashSessionKey });
724
+
583
725
  try {
584
726
  await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
585
727
  ctx: inboundCtx,
586
728
  cfg,
587
729
  dispatcherOptions: {
588
730
  deliver: async (payload) => {
731
+ logger.debug('Command deliver callback', {
732
+ hasText: !!payload.text,
733
+ textLen: payload.text?.length ?? 0,
734
+ hasMedia: !!(payload.mediaUrl || payload.mediaUrls?.length),
735
+ });
589
736
  if (payload.text) {
590
- const keyboard = extractKeyboardFromPayload(payload);
591
- await sendService.sendText(sendCtx, payload.text, keyboard ? { keyboard } : undefined);
737
+ // Use agent-provided keyboard if any, otherwise re-attach default command keyboard
738
+ const keyboard = extractKeyboardFromPayload(payload) ?? DEFAULT_COMMAND_KEYBOARD;
739
+ await sendService.sendText(sendCtx, payload.text, { keyboard });
592
740
  }
593
741
  },
594
742
  onReplyStart: async () => {
@@ -601,6 +749,7 @@ export const bitrix24Plugin = {
601
749
  },
602
750
  },
603
751
  });
752
+ logger.debug('Command dispatch completed', { commandText });
604
753
  } catch (err) {
605
754
  logger.error('Error dispatching command to agent', err);
606
755
  }
@@ -619,7 +768,8 @@ export const bitrix24Plugin = {
619
768
  botEntry.client_endpoint,
620
769
  botEntry.access_token,
621
770
  dialogId,
622
- `${config.botName ?? 'OpenClaw'} ready. Send me a message to get started.`,
771
+ `${config.botName ?? 'OpenClaw'} ready. Send me a message or pick a command below.`,
772
+ { KEYBOARD: DEFAULT_COMMAND_KEYBOARD },
623
773
  );
624
774
  } catch (err) {
625
775
  logger.error('Failed to send welcome message', err);
@@ -628,7 +778,7 @@ export const bitrix24Plugin = {
628
778
  },
629
779
  });
630
780
 
631
- gatewayState = { api, sendService, inboundHandler };
781
+ gatewayState = { api, sendService, mediaService, inboundHandler };
632
782
 
633
783
  logger.info(`[${ctx.accountId}] Bitrix24 channel started`);
634
784
 
package/src/commands.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * OpenClaw bot commands to register with Bitrix24.
3
3
  *
4
- * Mirrors the native commands registered by the Telegram plugin via setMyCommands.
4
+ * Standard OpenClaw bot commands registered via imbot.command.register.
5
5
  */
6
6
 
7
7
  export interface BotCommandDef {
@@ -7,7 +7,7 @@ const AccountSchema = z.object({
7
7
  botCode: z.string().optional().default('openclaw'),
8
8
  botAvatar: z.string().optional(),
9
9
  callbackUrl: z.string().url().optional(),
10
- dmPolicy: z.enum(['open', 'allowlist', 'pairing']).optional().default('open'),
10
+ dmPolicy: z.enum(['open', 'allowlist', 'pairing']).optional().default('pairing'),
11
11
  allowFrom: z.array(z.string()).optional(),
12
12
  showTyping: z.boolean().optional().default(true),
13
13
  streamUpdates: z.boolean().optional().default(false),
@@ -11,7 +11,6 @@ import type {
11
11
  B24MediaItem,
12
12
  } from './types.js';
13
13
  import { Dedup } from './dedup.js';
14
- import { checkAccess } from './access-control.js';
15
14
  import type { Bitrix24AccountConfig } from './types.js';
16
15
  import { defaultLogger } from './utils.js';
17
16
 
@@ -109,14 +108,6 @@ export class InboundHandler {
109
108
  return true;
110
109
  }
111
110
 
112
- const senderId = String(params.FROM_USER_ID);
113
-
114
- // Access control
115
- if (!checkAccess(senderId, this.config)) {
116
- this.logger.debug(`Access denied for user ${senderId}`);
117
- return true;
118
- }
119
-
120
111
  // Extract bot entry
121
112
  const botEntry = extractBotEntry(event.data.BOT);
122
113
  if (!botEntry) {
@@ -183,5 +174,6 @@ function normalizeFiles(files?: Record<string, B24File>): B24MediaItem[] {
183
174
  extension: file.extension,
184
175
  size: file.size,
185
176
  type: file.image ? 'image' as const : 'file' as const,
177
+ urlDownload: file.urlDownload || undefined,
186
178
  }));
187
179
  }