@ihazz/bitrix24 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -47,15 +47,25 @@ Add to your `openclaw.json`:
47
47
  "webhookUrl": "https://your-portal.bitrix24.com/rest/1/abc123xyz456/",
48
48
  "botName": "OpenClaw",
49
49
  "botCode": "openclaw",
50
- "callbackPath": "/hooks/bitrix24",
51
50
  "callbackUrl": "https://your-server.com/hooks/bitrix24",
52
51
  "dmPolicy": "open",
52
+ "allowFrom": ["*"],
53
53
  "showTyping": true
54
54
  }
55
55
  }
56
56
  }
57
57
  ```
58
58
 
59
+ Set allow in plugin section:
60
+ ```json
61
+ {
62
+ "plugins": {
63
+ "allow": [
64
+ "bitrix24"
65
+ ],
66
+ }
67
+ }
68
+ ```
59
69
  Only `webhookUrl` is required. The gateway will not start without it.
60
70
 
61
71
  ### Configuration Options
@@ -65,8 +75,7 @@ Only `webhookUrl` is required. The gateway will not start without it.
65
75
  | `webhookUrl` | — | Bitrix24 REST webhook URL (**required**) |
66
76
  | `botName` | `"OpenClaw"` | Bot display name (shown in welcome message) |
67
77
  | `botCode` | `"openclaw"` | Unique bot code for `imbot.register` |
68
- | `callbackPath` | `"/hooks/bitrix24"` | Webhook endpoint path for incoming B24 events |
69
- | `callbackUrl` | — | Full public URL for bot registration (e.g. `https://your-server.com/hooks/bitrix24`).|
78
+ | `callbackUrl` | — | Full public URL for bot EVENT_HANDLER (e.g. `https://your-server.com/hooks/bitrix24`). Path is auto-extracted for route registration. |
70
79
  | `dmPolicy` | `"open"` | Access policy: `"open"` / `"allowlist"` / `"pairing"` |
71
80
  | `allowFrom` | — | Allowed B24 user IDs (when `dmPolicy: "allowlist"`) |
72
81
  | `showTyping` | `true` | Send typing indicator before responding |
package/index.ts CHANGED
@@ -28,9 +28,10 @@ export default {
28
28
 
29
29
  api.registerChannel({ plugin: bitrix24Plugin });
30
30
 
31
- // Register HTTP webhook route on the OpenClaw gateway
31
+ // Register HTTP webhook route derive path from callbackUrl
32
32
  const channels = api.config?.channels as Record<string, Record<string, unknown>> | undefined;
33
- const callbackPath = (channels?.bitrix24?.callbackPath as string) ?? '/hooks/bitrix24';
33
+ const callbackUrl = channels?.bitrix24?.callbackUrl as string | undefined;
34
+ const callbackPath = callbackUrl ? new URL(callbackUrl).pathname : '/hooks/bitrix24';
34
35
 
35
36
  api.registerHttpRoute({
36
37
  path: callbackPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ihazz/bitrix24",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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
@@ -253,6 +253,33 @@ export class Bitrix24Api {
253
253
  return result.result;
254
254
  }
255
255
 
256
+ async registerCommand(
257
+ webhookUrl: string,
258
+ params: {
259
+ BOT_ID: number;
260
+ COMMAND: string;
261
+ COMMON?: 'Y' | 'N';
262
+ HIDDEN?: 'Y' | 'N';
263
+ EXTRANET_SUPPORT?: 'Y' | 'N';
264
+ LANG: Array<{ LANGUAGE_ID: string; TITLE: string; PARAMS?: string }>;
265
+ EVENT_COMMAND_ADD: string;
266
+ },
267
+ ): Promise<number> {
268
+ const result = await this.callWebhook<number>(
269
+ webhookUrl,
270
+ 'imbot.command.register',
271
+ params,
272
+ );
273
+ return result.result;
274
+ }
275
+
276
+ async unregisterCommand(webhookUrl: string, commandId: number): Promise<boolean> {
277
+ const result = await this.callWebhook<boolean>(webhookUrl, 'imbot.command.unregister', {
278
+ COMMAND_ID: commandId,
279
+ });
280
+ return result.result;
281
+ }
282
+
256
283
  async unregisterBot(webhookUrl: string, botId: number): Promise<boolean> {
257
284
  const result = await this.callWebhook<boolean>(webhookUrl, 'imbot.unregister', {
258
285
  BOT_ID: botId,
@@ -260,6 +287,86 @@ export class Bitrix24Api {
260
287
  return result.result;
261
288
  }
262
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
+ /**
311
+ * Get the disk folder ID for a chat (needed for file uploads).
312
+ */
313
+ async getChatFolder(
314
+ clientEndpoint: string,
315
+ accessToken: string,
316
+ chatId: number,
317
+ ): Promise<number> {
318
+ const result = await this.callWithToken<{ ID: number; [key: string]: unknown }>(
319
+ clientEndpoint,
320
+ 'im.disk.folder.get',
321
+ accessToken,
322
+ { CHAT_ID: chatId },
323
+ );
324
+ return result.result.ID;
325
+ }
326
+
327
+ /**
328
+ * Upload a file to a disk folder (base64 encoded).
329
+ * Returns the disk file ID.
330
+ */
331
+ async uploadFile(
332
+ clientEndpoint: string,
333
+ accessToken: string,
334
+ folderId: number,
335
+ fileName: string,
336
+ content: Buffer,
337
+ ): Promise<number> {
338
+ const result = await this.callWithToken<{ ID: number; [key: string]: unknown }>(
339
+ clientEndpoint,
340
+ 'disk.folder.uploadfile',
341
+ accessToken,
342
+ {
343
+ id: folderId,
344
+ data: { NAME: fileName },
345
+ fileContent: [fileName, content.toString('base64')],
346
+ generateUniqueName: true,
347
+ },
348
+ );
349
+ return result.result.ID;
350
+ }
351
+
352
+ /**
353
+ * Publish an uploaded file to a chat.
354
+ */
355
+ async commitFileToChat(
356
+ clientEndpoint: string,
357
+ accessToken: string,
358
+ chatId: number,
359
+ diskId: number,
360
+ ): Promise<boolean> {
361
+ const result = await this.callWithToken<boolean>(
362
+ clientEndpoint,
363
+ 'im.disk.file.commit',
364
+ accessToken,
365
+ { CHAT_ID: chatId, DISK_ID: diskId },
366
+ );
367
+ return result.result;
368
+ }
369
+
263
370
  destroy(): void {
264
371
  this.rateLimiter.destroy();
265
372
  }