@qwen-code/channel-base 0.14.0-nightly.20260402.a5f17ee39

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.
@@ -0,0 +1,115 @@
1
+ /**
2
+ * BlockStreamer — progressive multi-message delivery for channels.
3
+ *
4
+ * Accumulates text chunks from the agent's streaming response and emits
5
+ * completed "blocks" (paragraphs / sections) as separate channel messages
6
+ * while the agent is still working. This gives users a natural conversation
7
+ * flow instead of waiting 30–120 seconds for a single wall of text.
8
+ *
9
+ * Emission triggers:
10
+ * 1. Buffer ≥ maxChars → force-split at best break point
11
+ * 2. Buffer ≥ minChars AND a paragraph boundary (\n\n) exists → emit up to boundary
12
+ * 3. Idle timer fires (no chunk for idleMs) AND buffer ≥ minChars → emit buffer
13
+ * 4. flush() called (response complete) → emit everything remaining
14
+ *
15
+ * All sends are serialized — the next block waits for the previous send to complete.
16
+ */
17
+ export class BlockStreamer {
18
+ buffer = '';
19
+ idleTimer = null;
20
+ sending = Promise.resolve();
21
+ opts;
22
+ /** Number of blocks emitted so far. */
23
+ blockCount = 0;
24
+ constructor(opts) {
25
+ this.opts = opts;
26
+ }
27
+ /** Feed a new text chunk from the agent stream. */
28
+ push(chunk) {
29
+ this.buffer += chunk;
30
+ this.clearIdleTimer();
31
+ this.checkEmit();
32
+ if (this.buffer.length > 0 && this.opts.idleMs > 0) {
33
+ this.idleTimer = setTimeout(() => this.onIdle(), this.opts.idleMs);
34
+ }
35
+ }
36
+ /** Flush all remaining buffered text. Awaits all pending sends. */
37
+ async flush() {
38
+ this.clearIdleTimer();
39
+ if (this.buffer.length > 0) {
40
+ this.emitBlock(this.buffer);
41
+ this.buffer = '';
42
+ }
43
+ await this.sending;
44
+ }
45
+ // ---------------------------------------------------------------------------
46
+ // Internal
47
+ // ---------------------------------------------------------------------------
48
+ checkEmit() {
49
+ // 1. Force-split if buffer exceeds maxChars
50
+ while (this.buffer.length >= this.opts.maxChars) {
51
+ const bp = this.findBreakPoint(this.buffer, this.opts.maxChars);
52
+ this.emitBlock(this.buffer.slice(0, bp));
53
+ this.buffer = this.buffer.slice(bp);
54
+ }
55
+ // 2. Emit at paragraph boundary if we have enough text
56
+ if (this.buffer.length >= this.opts.minChars) {
57
+ const bp = this.findBlockBoundary(this.buffer);
58
+ if (bp > 0) {
59
+ this.emitBlock(this.buffer.slice(0, bp));
60
+ this.buffer = this.buffer.slice(bp);
61
+ }
62
+ }
63
+ }
64
+ onIdle() {
65
+ this.idleTimer = null;
66
+ if (this.buffer.length >= this.opts.minChars) {
67
+ this.emitBlock(this.buffer);
68
+ this.buffer = '';
69
+ }
70
+ }
71
+ emitBlock(text) {
72
+ const trimmed = text.trim();
73
+ if (!trimmed)
74
+ return;
75
+ this.blockCount++;
76
+ this.sending = this.sending
77
+ .then(() => this.opts.send(trimmed))
78
+ .catch(() => { });
79
+ }
80
+ /**
81
+ * Find the last paragraph boundary (\n\n) in the buffer.
82
+ * Returns the position after the boundary, or -1 if no suitable boundary
83
+ * exists at or after minChars.
84
+ */
85
+ findBlockBoundary(text) {
86
+ const last = text.lastIndexOf('\n\n');
87
+ if (last < 0 || last < this.opts.minChars)
88
+ return -1;
89
+ return last + 2;
90
+ }
91
+ /**
92
+ * Find the best break point at or before maxPos.
93
+ * Prefers paragraph break > newline > space > maxPos.
94
+ */
95
+ findBreakPoint(text, maxPos) {
96
+ const sub = text.slice(0, maxPos);
97
+ const para = sub.lastIndexOf('\n\n');
98
+ if (para > 0)
99
+ return para + 2;
100
+ const nl = sub.lastIndexOf('\n');
101
+ if (nl > 0)
102
+ return nl + 1;
103
+ const sp = sub.lastIndexOf(' ');
104
+ if (sp > 0)
105
+ return sp + 1;
106
+ return maxPos;
107
+ }
108
+ clearIdleTimer() {
109
+ if (this.idleTimer !== null) {
110
+ clearTimeout(this.idleTimer);
111
+ this.idleTimer = null;
112
+ }
113
+ }
114
+ }
115
+ //# sourceMappingURL=BlockStreamer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BlockStreamer.js","sourceRoot":"","sources":["../src/BlockStreamer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ import type { ChannelConfig, Envelope } from './types.js';
2
+ import { GroupGate } from './GroupGate.js';
3
+ import { SenderGate } from './SenderGate.js';
4
+ import { SessionRouter } from './SessionRouter.js';
5
+ import type { AcpBridge, ToolCallEvent } from './AcpBridge.js';
6
+ export interface ChannelBaseOptions {
7
+ router?: SessionRouter;
8
+ }
9
+ /** Handler for a slash command. Return true if handled, false to forward to agent. */
10
+ type CommandHandler = (envelope: Envelope, args: string) => Promise<boolean>;
11
+ export declare abstract class ChannelBase {
12
+ protected config: ChannelConfig;
13
+ protected bridge: AcpBridge;
14
+ protected groupGate: GroupGate;
15
+ protected gate: SenderGate;
16
+ protected router: SessionRouter;
17
+ protected name: string;
18
+ private instructedSessions;
19
+ private commands;
20
+ /** Per-session promise chain to serialize prompt + send (followup mode). */
21
+ private sessionQueues;
22
+ /** Per-session active prompt tracking for dispatch modes. */
23
+ private activePrompts;
24
+ /** Per-session message buffer for collect mode. */
25
+ private collectBuffers;
26
+ constructor(name: string, config: ChannelConfig, bridge: AcpBridge, options?: ChannelBaseOptions);
27
+ abstract connect(): Promise<void>;
28
+ abstract sendMessage(chatId: string, text: string): Promise<void>;
29
+ abstract disconnect(): void;
30
+ /** Replace the bridge instance (used after crash recovery restart). */
31
+ setBridge(bridge: AcpBridge): void;
32
+ onToolCall(_chatId: string, _event: ToolCallEvent): void;
33
+ /**
34
+ * Called when a prompt actually begins processing (inside the session queue).
35
+ * Override to show a platform-specific working indicator (e.g., typing, reaction).
36
+ * Not called for buffered messages (collect mode) or gated/blocked messages.
37
+ */
38
+ protected onPromptStart(_chatId: string, _sessionId: string, _messageId?: string): void;
39
+ /**
40
+ * Called when a prompt finishes (response sent or cancelled).
41
+ * Override to hide the working indicator.
42
+ */
43
+ protected onPromptEnd(_chatId: string, _sessionId: string, _messageId?: string): void;
44
+ /**
45
+ * Called for each text chunk as the agent streams its response.
46
+ * Override to implement progressive display (e.g., updating an AI card in-place).
47
+ * Default: no-op (chunks are collected internally and delivered via onResponseComplete).
48
+ */
49
+ protected onResponseChunk(_chatId: string, _chunk: string, _sessionId: string): void;
50
+ /**
51
+ * Called when the agent's full response is ready.
52
+ * Override to customize delivery (e.g., finalize an AI card).
53
+ * Default: calls sendMessage() with the full response text.
54
+ */
55
+ protected onResponseComplete(chatId: string, fullText: string, _sessionId: string): Promise<void>;
56
+ /**
57
+ * Register a slash command handler. Subclasses can call this to add
58
+ * platform-specific commands (e.g., /start for Telegram).
59
+ * Overrides shared commands if the same name is registered.
60
+ */
61
+ protected registerCommand(name: string, handler: CommandHandler): void;
62
+ /** Register shared slash commands. Called from constructor. */
63
+ private registerSharedCommands;
64
+ /** Check if a message text matches a registered local command. */
65
+ protected isLocalCommand(text: string): boolean;
66
+ /**
67
+ * Parse a slash command from message text.
68
+ * Returns { command, args } or null if not a slash command.
69
+ */
70
+ private parseCommand;
71
+ handleInbound(envelope: Envelope): Promise<void>;
72
+ protected onPairingRequired(chatId: string, code: string | null): Promise<void>;
73
+ }
74
+ export {};
@@ -0,0 +1,332 @@
1
+ import { BlockStreamer } from './BlockStreamer.js';
2
+ import { GroupGate } from './GroupGate.js';
3
+ import { SenderGate } from './SenderGate.js';
4
+ import { PairingStore } from './PairingStore.js';
5
+ import { SessionRouter } from './SessionRouter.js';
6
+ export class ChannelBase {
7
+ config;
8
+ bridge;
9
+ groupGate;
10
+ gate;
11
+ router;
12
+ name;
13
+ instructedSessions = new Set();
14
+ commands = new Map();
15
+ /** Per-session promise chain to serialize prompt + send (followup mode). */
16
+ sessionQueues = new Map();
17
+ /** Per-session active prompt tracking for dispatch modes. */
18
+ activePrompts = new Map();
19
+ /** Per-session message buffer for collect mode. */
20
+ collectBuffers = new Map();
21
+ constructor(name, config, bridge, options) {
22
+ this.name = name;
23
+ this.config = config;
24
+ this.bridge = bridge;
25
+ this.groupGate = new GroupGate(config.groupPolicy, config.groups);
26
+ const pairingStore = config.senderPolicy === 'pairing' ? new PairingStore(name) : undefined;
27
+ this.gate = new SenderGate(config.senderPolicy, config.allowedUsers, pairingStore);
28
+ this.router =
29
+ options?.router ||
30
+ new SessionRouter(bridge, config.cwd, config.sessionScope);
31
+ this.registerSharedCommands();
32
+ // When running standalone (no gateway), register toolCall listener directly.
33
+ // In gateway mode, the ChannelManager dispatches events instead.
34
+ if (!options?.router) {
35
+ bridge.on('toolCall', (event) => {
36
+ const target = this.router.getTarget(event.sessionId);
37
+ if (target) {
38
+ this.onToolCall(target.chatId, event);
39
+ }
40
+ });
41
+ }
42
+ }
43
+ /** Replace the bridge instance (used after crash recovery restart). */
44
+ setBridge(bridge) {
45
+ this.bridge = bridge;
46
+ }
47
+ onToolCall(_chatId, _event) { }
48
+ /**
49
+ * Called when a prompt actually begins processing (inside the session queue).
50
+ * Override to show a platform-specific working indicator (e.g., typing, reaction).
51
+ * Not called for buffered messages (collect mode) or gated/blocked messages.
52
+ */
53
+ onPromptStart(_chatId, _sessionId, _messageId) { }
54
+ /**
55
+ * Called when a prompt finishes (response sent or cancelled).
56
+ * Override to hide the working indicator.
57
+ */
58
+ onPromptEnd(_chatId, _sessionId, _messageId) { }
59
+ /**
60
+ * Called for each text chunk as the agent streams its response.
61
+ * Override to implement progressive display (e.g., updating an AI card in-place).
62
+ * Default: no-op (chunks are collected internally and delivered via onResponseComplete).
63
+ */
64
+ onResponseChunk(_chatId, _chunk, _sessionId) { }
65
+ /**
66
+ * Called when the agent's full response is ready.
67
+ * Override to customize delivery (e.g., finalize an AI card).
68
+ * Default: calls sendMessage() with the full response text.
69
+ */
70
+ async onResponseComplete(chatId, fullText, _sessionId) {
71
+ await this.sendMessage(chatId, fullText);
72
+ }
73
+ /**
74
+ * Register a slash command handler. Subclasses can call this to add
75
+ * platform-specific commands (e.g., /start for Telegram).
76
+ * Overrides shared commands if the same name is registered.
77
+ */
78
+ registerCommand(name, handler) {
79
+ this.commands.set(name.toLowerCase(), handler);
80
+ }
81
+ /** Register shared slash commands. Called from constructor. */
82
+ registerSharedCommands() {
83
+ const clearHandler = async (envelope) => {
84
+ const removedIds = this.router.removeSession(this.name, envelope.senderId, envelope.chatId);
85
+ if (removedIds.length > 0) {
86
+ for (const id of removedIds) {
87
+ this.instructedSessions.delete(id);
88
+ }
89
+ await this.sendMessage(envelope.chatId, 'Session cleared. Your next message will start a fresh conversation.');
90
+ }
91
+ else {
92
+ await this.sendMessage(envelope.chatId, 'No active session to clear.');
93
+ }
94
+ return true;
95
+ };
96
+ this.registerCommand('clear', clearHandler);
97
+ this.registerCommand('reset', clearHandler);
98
+ this.registerCommand('new', clearHandler);
99
+ this.registerCommand('help', async (envelope) => {
100
+ const lines = [
101
+ 'Commands:',
102
+ '/help — Show this help',
103
+ '/clear — Clear your session (aliases: /reset, /new)',
104
+ '/status — Show session info',
105
+ ];
106
+ // Platform-specific commands (registered by adapters, not shared ones)
107
+ const sharedCmds = new Set(['help', 'clear', 'reset', 'new', 'status']);
108
+ const platformCmds = [...this.commands.keys()].filter((c) => !sharedCmds.has(c));
109
+ if (platformCmds.length > 0) {
110
+ for (const cmd of platformCmds) {
111
+ lines.push(`/${cmd}`);
112
+ }
113
+ }
114
+ const agentCommands = this.bridge.availableCommands;
115
+ if (agentCommands.length > 0) {
116
+ lines.push('', 'Agent commands (forwarded to Qwen Code):');
117
+ for (const cmd of agentCommands) {
118
+ lines.push(`/${cmd.name} — ${cmd.description}`);
119
+ }
120
+ }
121
+ lines.push('', 'Send any text to chat with the agent.');
122
+ await this.sendMessage(envelope.chatId, lines.join('\n'));
123
+ return true;
124
+ });
125
+ this.registerCommand('status', async (envelope) => {
126
+ const hasSession = this.router.hasSession(this.name, envelope.senderId, envelope.chatId);
127
+ const policy = this.config.senderPolicy;
128
+ const lines = [
129
+ `Session: ${hasSession ? 'active' : 'none'}`,
130
+ `Access: ${policy}`,
131
+ `Channel: ${this.name}`,
132
+ ];
133
+ await this.sendMessage(envelope.chatId, lines.join('\n'));
134
+ return true;
135
+ });
136
+ }
137
+ /** Check if a message text matches a registered local command. */
138
+ isLocalCommand(text) {
139
+ const parsed = this.parseCommand(text);
140
+ return parsed !== null && this.commands.has(parsed.command);
141
+ }
142
+ /**
143
+ * Parse a slash command from message text.
144
+ * Returns { command, args } or null if not a slash command.
145
+ */
146
+ parseCommand(text) {
147
+ if (!text.startsWith('/'))
148
+ return null;
149
+ // Handle /command@botname format (Telegram groups)
150
+ const match = text.match(/^\/([a-zA-Z0-9_]+)(?:@\S+)?\s*(.*)/s);
151
+ if (!match)
152
+ return null;
153
+ return { command: match[1].toLowerCase(), args: match[2].trim() };
154
+ }
155
+ async handleInbound(envelope) {
156
+ // 1. Group gate: policy + allowlist + mention gating
157
+ const groupResult = this.groupGate.check(envelope);
158
+ if (!groupResult.allowed) {
159
+ return; // silently drop — no pairing, no reply
160
+ }
161
+ // 2. Sender gate: allowlist / pairing / open
162
+ const result = this.gate.check(envelope.senderId, envelope.senderName);
163
+ if (!result.allowed) {
164
+ if (result.pairingCode !== undefined) {
165
+ await this.onPairingRequired(envelope.chatId, result.pairingCode);
166
+ }
167
+ return;
168
+ }
169
+ // 3. Slash command handling — before session/agent routing
170
+ const parsed = this.parseCommand(envelope.text);
171
+ if (parsed) {
172
+ const handler = this.commands.get(parsed.command);
173
+ if (handler) {
174
+ const handled = await handler(envelope, parsed.args);
175
+ if (handled)
176
+ return;
177
+ }
178
+ // Unrecognized commands fall through to the agent
179
+ }
180
+ const sessionId = await this.router.resolve(this.name, envelope.senderId, envelope.chatId, envelope.threadId, this.config.cwd);
181
+ // Prepend referenced (quoted) message text for reply context
182
+ let promptText = envelope.text;
183
+ if (envelope.referencedText) {
184
+ promptText = `[Replying to: "${envelope.referencedText}"]\n\n${promptText}`;
185
+ }
186
+ // Resolve attachments: extract image for bridge, append file paths to text
187
+ let imageBase64 = envelope.imageBase64;
188
+ let imageMimeType = envelope.imageMimeType;
189
+ if (envelope.attachments?.length) {
190
+ const filePaths = [];
191
+ for (const att of envelope.attachments) {
192
+ if (att.type === 'image' && att.data && !imageBase64) {
193
+ imageBase64 = att.data;
194
+ imageMimeType = att.mimeType;
195
+ }
196
+ else if (att.filePath) {
197
+ const label = att.type === 'file' ? 'file' : att.type;
198
+ const name = att.fileName ? ` "${att.fileName}"` : '';
199
+ filePaths.push(`User sent a ${label}${name}. It has been saved to: ${att.filePath}`);
200
+ }
201
+ }
202
+ if (filePaths.length > 0) {
203
+ promptText = promptText + '\n\n' + filePaths.join('\n');
204
+ }
205
+ }
206
+ // Prepend channel instructions on first message of a session
207
+ if (this.config.instructions && !this.instructedSessions.has(sessionId)) {
208
+ promptText = `${this.config.instructions}\n\n${promptText}`;
209
+ this.instructedSessions.add(sessionId);
210
+ }
211
+ // Resolve dispatch mode: per-group override → channel config → default
212
+ const groupCfg = envelope.isGroup
213
+ ? this.config.groups[envelope.chatId] || this.config.groups['*']
214
+ : undefined;
215
+ const mode = groupCfg?.dispatchMode || this.config.dispatchMode || 'steer';
216
+ const active = this.activePrompts.get(sessionId);
217
+ if (active) {
218
+ // A prompt is already running for this session
219
+ switch (mode) {
220
+ case 'collect': {
221
+ // Buffer the message; it will be coalesced when the active prompt finishes
222
+ let buffer = this.collectBuffers.get(sessionId);
223
+ if (!buffer) {
224
+ buffer = [];
225
+ this.collectBuffers.set(sessionId, buffer);
226
+ }
227
+ buffer.push({ text: promptText, envelope });
228
+ return;
229
+ }
230
+ case 'steer': {
231
+ // Cancel the running prompt, then fall through to send a new one
232
+ active.cancelled = true;
233
+ await this.bridge.cancelSession(sessionId).catch(() => { });
234
+ // Wait for the active prompt to finish winding down
235
+ await active.done;
236
+ // Prepend a cancellation note so the agent understands context
237
+ promptText = `[The user sent a new message while you were working. Their previous request has been cancelled.]\n\n${promptText}`;
238
+ break;
239
+ }
240
+ case 'followup': {
241
+ // Chain onto the session queue (existing sequential behavior)
242
+ break;
243
+ }
244
+ default: {
245
+ // Exhaustive check — should never happen
246
+ const _exhaustive = mode;
247
+ throw new Error(`Unknown dispatch mode: ${_exhaustive}`);
248
+ }
249
+ }
250
+ }
251
+ // Run the prompt (with followup-mode serialization for safety)
252
+ const prev = this.sessionQueues.get(sessionId) ?? Promise.resolve();
253
+ const useBlockStreaming = this.config.blockStreaming === 'on';
254
+ const current = prev.then(async () => {
255
+ // Register this prompt as active
256
+ let doneResolve = () => { };
257
+ const done = new Promise((r) => {
258
+ doneResolve = r;
259
+ });
260
+ const promptState = { cancelled: false, done, resolve: doneResolve };
261
+ this.activePrompts.set(sessionId, promptState);
262
+ this.onPromptStart(envelope.chatId, sessionId, envelope.messageId);
263
+ const streamer = useBlockStreaming
264
+ ? new BlockStreamer({
265
+ minChars: this.config.blockStreamingChunk?.minChars ?? 400,
266
+ maxChars: this.config.blockStreamingChunk?.maxChars ?? 1000,
267
+ idleMs: this.config.blockStreamingCoalesce?.idleMs ?? 1500,
268
+ send: (text) => this.sendMessage(envelope.chatId, text),
269
+ })
270
+ : null;
271
+ const onChunk = (sid, chunk) => {
272
+ if (sid === sessionId) {
273
+ this.onResponseChunk(envelope.chatId, chunk, sessionId);
274
+ streamer?.push(chunk);
275
+ }
276
+ };
277
+ this.bridge.on('textChunk', onChunk);
278
+ try {
279
+ const response = await this.bridge.prompt(sessionId, promptText, {
280
+ imageBase64,
281
+ imageMimeType,
282
+ });
283
+ // If cancelled (steer mode), skip sending the response
284
+ if (!promptState.cancelled && response) {
285
+ if (streamer) {
286
+ await streamer.flush();
287
+ }
288
+ else {
289
+ await this.onResponseComplete(envelope.chatId, response, sessionId);
290
+ }
291
+ }
292
+ }
293
+ finally {
294
+ this.bridge.off('textChunk', onChunk);
295
+ this.onPromptEnd(envelope.chatId, sessionId, envelope.messageId);
296
+ this.activePrompts.delete(sessionId);
297
+ // Signal any steer waiter that we're done
298
+ promptState.resolve();
299
+ // Drain collect buffer if any messages accumulated
300
+ const buffer = this.collectBuffers.get(sessionId);
301
+ if (buffer && buffer.length > 0) {
302
+ this.collectBuffers.delete(sessionId);
303
+ const coalesced = buffer.map((b) => b.text).join('\n\n');
304
+ const lastEnvelope = buffer[buffer.length - 1].envelope;
305
+ // Re-enter handleInbound with the coalesced message
306
+ const syntheticEnvelope = {
307
+ ...lastEnvelope,
308
+ text: coalesced,
309
+ // Clear attachments/references — already resolved in original text
310
+ referencedText: undefined,
311
+ attachments: undefined,
312
+ imageBase64: undefined,
313
+ imageMimeType: undefined,
314
+ };
315
+ // Queue the coalesced prompt (don't await to avoid deadlock on the queue)
316
+ this.handleInbound(syntheticEnvelope).catch(() => { });
317
+ }
318
+ }
319
+ });
320
+ this.sessionQueues.set(sessionId, current.catch(() => { }));
321
+ await current;
322
+ }
323
+ async onPairingRequired(chatId, code) {
324
+ if (code) {
325
+ await this.sendMessage(chatId, `Your pairing code is: ${code}\n\nAsk the bot operator to approve you with:\n qwen channel pairing approve ${this.name} ${code}`);
326
+ }
327
+ else {
328
+ await this.sendMessage(chatId, 'Too many pending pairing requests. Please try again later.');
329
+ }
330
+ }
331
+ }
332
+ //# sourceMappingURL=ChannelBase.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChannelBase.js","sourceRoot":"","sources":["../src/ChannelBase.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { GroupPolicy, GroupConfig, Envelope } from './types.js';
2
+ export interface GroupCheckResult {
3
+ allowed: boolean;
4
+ reason?: 'disabled' | 'not_allowlisted' | 'mention_required';
5
+ }
6
+ export declare class GroupGate {
7
+ private policy;
8
+ private groups;
9
+ constructor(policy?: GroupPolicy, groups?: Record<string, GroupConfig>);
10
+ /**
11
+ * Full group check: policy + allowlist + mention gating.
12
+ * Evaluation order:
13
+ * 1. groupPolicy (disabled → drop)
14
+ * 2. group allowlist (allowlist mode, no match → drop)
15
+ * 3. mention gating (requireMention + not mentioned → drop silently)
16
+ *
17
+ * Mention gating runs before sender gate so that unmentioned messages
18
+ * in groups don't trigger pairing flows.
19
+ */
20
+ check(envelope: Envelope): GroupCheckResult;
21
+ }
@@ -0,0 +1,41 @@
1
+ export class GroupGate {
2
+ policy;
3
+ groups;
4
+ constructor(policy = 'disabled', groups = {}) {
5
+ this.policy = policy;
6
+ this.groups = groups;
7
+ }
8
+ /**
9
+ * Full group check: policy + allowlist + mention gating.
10
+ * Evaluation order:
11
+ * 1. groupPolicy (disabled → drop)
12
+ * 2. group allowlist (allowlist mode, no match → drop)
13
+ * 3. mention gating (requireMention + not mentioned → drop silently)
14
+ *
15
+ * Mention gating runs before sender gate so that unmentioned messages
16
+ * in groups don't trigger pairing flows.
17
+ */
18
+ check(envelope) {
19
+ if (!envelope.isGroup) {
20
+ return { allowed: true };
21
+ }
22
+ if (this.policy === 'disabled') {
23
+ return { allowed: false, reason: 'disabled' };
24
+ }
25
+ if (this.policy === 'allowlist') {
26
+ // In allowlist mode, "*" is only a default config — not a wildcard allow.
27
+ // The group must be explicitly listed by ID.
28
+ if (!this.groups[envelope.chatId]) {
29
+ return { allowed: false, reason: 'not_allowlisted' };
30
+ }
31
+ }
32
+ // Per-group config, falling back to "*" defaults, then built-in defaults
33
+ const groupConfig = this.groups[envelope.chatId] || this.groups['*'] || {};
34
+ const requireMention = groupConfig.requireMention ?? true;
35
+ if (requireMention && !envelope.isMentioned && !envelope.isReplyToBot) {
36
+ return { allowed: false, reason: 'mention_required' };
37
+ }
38
+ return { allowed: true };
39
+ }
40
+ }
41
+ //# sourceMappingURL=GroupGate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GroupGate.js","sourceRoot":"","sources":["../src/GroupGate.ts"],"names":[],"mappings":"AAOA,MAAM,OAAO,SAAS;IACZ,MAAM,CAAc;IACpB,MAAM,CAA8B;IAE5C,YACE,SAAsB,UAAU,EAChC,SAAsC,EAAE;QAExC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,QAAkB;QACtB,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;YACtB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC3B,CAAC;QAED,IAAI,IAAI,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YAC/B,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;QAChD,CAAC;QAED,IAAI,IAAI,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YAChC,0EAA0E;YAC1E,6CAA6C;YAC7C,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAClC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;YACvD,CAAC;QACH,CAAC;QAED,yEAAyE;QACzE,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QAC3E,MAAM,cAAc,GAAG,WAAW,CAAC,cAAc,IAAI,IAAI,CAAC;QAE1D,IAAI,cAAc,IAAI,CAAC,QAAQ,CAAC,WAAW,IAAI,CAAC,QAAQ,CAAC,YAAY,EAAE,CAAC;YACtE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;QACxD,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;CACF"}
@@ -0,0 +1,31 @@
1
+ export interface PairingRequest {
2
+ senderId: string;
3
+ senderName: string;
4
+ code: string;
5
+ createdAt: number;
6
+ }
7
+ export declare class PairingStore {
8
+ private dir;
9
+ private pendingPath;
10
+ private allowlistPath;
11
+ constructor(channelName: string);
12
+ isApproved(senderId: string): boolean;
13
+ /**
14
+ * Create a pairing request for an unknown sender.
15
+ * Returns the code if created, or null if the pending cap is reached.
16
+ * If the sender already has a non-expired pending request, returns that code.
17
+ */
18
+ createRequest(senderId: string, senderName: string): string | null;
19
+ /**
20
+ * Approve a pairing request by code.
21
+ * Returns the sender ID if found, or null if not found / expired.
22
+ */
23
+ approve(code: string): PairingRequest | null;
24
+ listPending(): PairingRequest[];
25
+ getAllowlist(): string[];
26
+ private ensureDir;
27
+ private readPending;
28
+ private writePending;
29
+ private readAllowlist;
30
+ private writeAllowlist;
31
+ }