@ihazz/bitrix24 0.2.5 → 1.0.1

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.
@@ -1,43 +1,69 @@
1
1
  import type { Bitrix24AccountConfig } from './types.js';
2
2
  import type { PluginRuntime, ChannelPairingAdapter } from './runtime.js';
3
+ import { stripChannelPrefix } from './utils.js';
3
4
 
4
5
  /**
5
6
  * Normalize an allowFrom entry — strip platform prefixes.
6
7
  * "bitrix24:42" → "42", "b24:42" → "42", "bx24:42" → "42", "42" → "42"
7
8
  */
8
9
  export function normalizeAllowEntry(entry: string): string {
9
- return entry.trim().replace(/^(bitrix24|b24|bx24):/i, '');
10
+ return stripChannelPrefix(entry.trim());
11
+ }
12
+
13
+ export function normalizeAllowList(entries: string[] | undefined): string[] {
14
+ if (!Array.isArray(entries)) {
15
+ return [];
16
+ }
17
+
18
+ return [...new Set(
19
+ entries
20
+ .map((entry) => normalizeAllowEntry(String(entry)))
21
+ .filter(Boolean),
22
+ )];
23
+ }
24
+
25
+ /**
26
+ * Extract the owner user ID from a Bitrix24 inbound webhook URL.
27
+ * Format: https://{portal}/rest/{user_id}/{webhook_token}/
28
+ */
29
+ export function getWebhookUserId(webhookUrl: string | undefined): string | null {
30
+ if (!webhookUrl) return null;
31
+
32
+ try {
33
+ const url = new URL(webhookUrl);
34
+ const pathParts = url.pathname.split('/').filter(Boolean);
35
+ const restIndex = pathParts.indexOf('rest');
36
+
37
+ if (restIndex < 0 || pathParts.length <= restIndex + 2) {
38
+ return null;
39
+ }
40
+
41
+ const userId = pathParts[restIndex + 1];
42
+ return userId ? normalizeAllowEntry(userId) : null;
43
+ } catch {
44
+ return null;
45
+ }
10
46
  }
11
47
 
12
48
  /**
13
49
  * Check whether a sender is allowed to communicate with the bot.
14
50
  *
15
- * @returns `true` if the sender is allowed, `false` otherwise.
51
+ * Pairing requires runtime state, so the synchronous helper cannot approve access.
16
52
  */
17
53
  export function checkAccess(
18
54
  senderId: string,
19
55
  config: Bitrix24AccountConfig,
56
+ _options?: { dialogId?: string; isDirect?: boolean },
20
57
  ): boolean {
21
- const policy = config.dmPolicy ?? 'pairing';
22
-
23
- switch (policy) {
24
- case 'open':
25
- return true;
26
-
27
- case 'allowlist': {
28
- const allowList = config.allowFrom;
29
- if (!allowList || allowList.length === 0) return false;
30
- const normalized = allowList.map(normalizeAllowEntry);
31
- return normalized.includes(String(senderId));
32
- }
58
+ const dmPolicy = config.dmPolicy ?? 'webhookUser';
59
+ const identity = resolveAccessIdentity(senderId);
33
60
 
34
- case 'pairing':
35
- // Pairing requires runtime — use checkAccessWithPairing() instead
36
- return false;
37
-
38
- default:
39
- return false;
61
+ if (dmPolicy === 'webhookUser') {
62
+ const webhookUserId = getWebhookUserId(config.webhookUrl);
63
+ return webhookUserId === identity;
40
64
  }
65
+
66
+ return false;
41
67
  }
42
68
 
43
69
  export type AccessResult = 'allow' | 'deny' | 'pairing';
@@ -49,6 +75,8 @@ export type AccessResult = 'allow' | 'deny' | 'pairing';
49
75
  */
50
76
  export async function checkAccessWithPairing(params: {
51
77
  senderId: string;
78
+ dialogId?: string;
79
+ isDirect?: boolean;
52
80
  config: Bitrix24AccountConfig;
53
81
  runtime: PluginRuntime;
54
82
  accountId: string;
@@ -56,44 +84,70 @@ export async function checkAccessWithPairing(params: {
56
84
  sendReply: (text: string) => Promise<void>;
57
85
  logger?: { debug: (...args: unknown[]) => void };
58
86
  }): 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';
87
+ const {
88
+ senderId,
89
+ dialogId,
90
+ isDirect,
91
+ config,
92
+ runtime,
93
+ accountId,
94
+ pairingAdapter,
95
+ sendReply,
96
+ logger,
97
+ } = params;
98
+ const identity = resolveAccessIdentity(senderId);
99
+ const dmPolicy = config.dmPolicy ?? 'webhookUser';
63
100
 
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));
101
+ if (dmPolicy === 'webhookUser') {
102
+ const webhookUserId = getWebhookUserId(config.webhookUrl);
69
103
 
70
- if (merged.includes(normalizedSender)) return 'allow';
104
+ if (webhookUserId && webhookUserId === identity) {
105
+ return 'allow';
106
+ }
71
107
 
72
- if (policy === 'allowlist') {
73
- logger?.debug('Access denied (allowlist)', { senderId });
108
+ logger?.debug('Access denied (webhookUser)', { senderId, dialogId, webhookUserId, identity });
74
109
  return 'deny';
75
110
  }
76
111
 
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
- });
112
+ const storeAllowFrom = await runtime.channel.pairing.readAllowFromStore('bitrix24', '', accountId);
113
+ const approved = [...new Set([
114
+ ...normalizeAllowList(config.allowFrom),
115
+ ...normalizeAllowList(storeAllowFrom),
116
+ ])];
85
117
 
86
- if (created) {
87
- const reply = runtime.channel.pairing.buildPairingReply({
88
- code,
118
+ if (approved.includes(identity)) return 'allow';
119
+
120
+ try {
121
+ const { code, created } = await runtime.channel.pairing.upsertPairingRequest({
89
122
  channel: 'bitrix24',
123
+ id: identity,
90
124
  accountId,
125
+ meta: {},
126
+ pairingAdapter,
91
127
  });
92
- // buildPairingReply returns a string directly, not an object
93
- const replyText = typeof reply === 'string' ? reply : (reply as { text?: string })?.text ?? String(reply);
94
- await sendReply(replyText);
128
+
129
+ if (created) {
130
+ const reply = runtime.channel.pairing.buildPairingReply({
131
+ code,
132
+ channel: 'bitrix24',
133
+ accountId,
134
+ });
135
+ const replyText = typeof reply === 'string'
136
+ ? reply
137
+ : (reply && typeof reply === 'object' && 'text' in reply && typeof (reply as Record<string, unknown>).text === 'string')
138
+ ? (reply as Record<string, unknown>).text as string
139
+ : String(reply);
140
+ await sendReply(replyText);
141
+ }
142
+
143
+ logger?.debug('Pairing request handled', { senderId, dialogId, identity, code, created });
144
+ return 'pairing';
145
+ } catch (err) {
146
+ logger?.debug('Pairing request failed, falling back to deny', { senderId, dialogId, identity, error: err });
147
+ return 'deny';
95
148
  }
149
+ }
96
150
 
97
- logger?.debug('Pairing request handled', { senderId, code, created });
98
- return 'pairing';
151
+ function resolveAccessIdentity(senderId: string): string {
152
+ return normalizeAllowEntry(String(senderId));
99
153
  }