@wolpertingerlabs/drawlatch 1.0.0-alpha.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.
- package/CONNECTIONS.md +197 -0
- package/INGESTORS.md +790 -0
- package/LICENSE +21 -0
- package/README.md +685 -0
- package/dist/cli/generate-keys.d.ts +15 -0
- package/dist/cli/generate-keys.js +107 -0
- package/dist/connections/anthropic.json +16 -0
- package/dist/connections/bluesky.json +26 -0
- package/dist/connections/devin.json +15 -0
- package/dist/connections/discord-bot.json +24 -0
- package/dist/connections/discord-oauth.json +16 -0
- package/dist/connections/github.json +25 -0
- package/dist/connections/google-ai.json +15 -0
- package/dist/connections/google.json +28 -0
- package/dist/connections/hex.json +14 -0
- package/dist/connections/lichess.json +15 -0
- package/dist/connections/linear.json +29 -0
- package/dist/connections/mastodon.json +25 -0
- package/dist/connections/notion.json +33 -0
- package/dist/connections/openai.json +16 -0
- package/dist/connections/openrouter.json +16 -0
- package/dist/connections/reddit.json +28 -0
- package/dist/connections/slack.json +23 -0
- package/dist/connections/stripe.json +25 -0
- package/dist/connections/telegram.json +26 -0
- package/dist/connections/trello.json +25 -0
- package/dist/connections/twitch.json +28 -0
- package/dist/connections/x.json +27 -0
- package/dist/mcp/server.d.ts +13 -0
- package/dist/mcp/server.js +258 -0
- package/dist/remote/ingestors/base-ingestor.d.ts +65 -0
- package/dist/remote/ingestors/base-ingestor.js +132 -0
- package/dist/remote/ingestors/discord/discord-gateway.d.ts +58 -0
- package/dist/remote/ingestors/discord/discord-gateway.js +341 -0
- package/dist/remote/ingestors/discord/index.d.ts +3 -0
- package/dist/remote/ingestors/discord/index.js +3 -0
- package/dist/remote/ingestors/discord/types.d.ts +56 -0
- package/dist/remote/ingestors/discord/types.js +68 -0
- package/dist/remote/ingestors/index.d.ts +16 -0
- package/dist/remote/ingestors/index.js +20 -0
- package/dist/remote/ingestors/manager.d.ts +65 -0
- package/dist/remote/ingestors/manager.js +201 -0
- package/dist/remote/ingestors/poll/index.d.ts +2 -0
- package/dist/remote/ingestors/poll/index.js +2 -0
- package/dist/remote/ingestors/poll/poll-ingestor.d.ts +78 -0
- package/dist/remote/ingestors/poll/poll-ingestor.js +283 -0
- package/dist/remote/ingestors/registry.d.ts +32 -0
- package/dist/remote/ingestors/registry.js +46 -0
- package/dist/remote/ingestors/ring-buffer.d.ts +33 -0
- package/dist/remote/ingestors/ring-buffer.js +62 -0
- package/dist/remote/ingestors/slack/index.d.ts +3 -0
- package/dist/remote/ingestors/slack/index.js +3 -0
- package/dist/remote/ingestors/slack/socket-mode.d.ts +48 -0
- package/dist/remote/ingestors/slack/socket-mode.js +267 -0
- package/dist/remote/ingestors/slack/types.d.ts +70 -0
- package/dist/remote/ingestors/slack/types.js +72 -0
- package/dist/remote/ingestors/types.d.ts +138 -0
- package/dist/remote/ingestors/types.js +13 -0
- package/dist/remote/ingestors/webhook/base-webhook-ingestor.d.ts +112 -0
- package/dist/remote/ingestors/webhook/base-webhook-ingestor.js +119 -0
- package/dist/remote/ingestors/webhook/github-types.d.ts +45 -0
- package/dist/remote/ingestors/webhook/github-types.js +65 -0
- package/dist/remote/ingestors/webhook/github-webhook-ingestor.d.ts +43 -0
- package/dist/remote/ingestors/webhook/github-webhook-ingestor.js +86 -0
- package/dist/remote/ingestors/webhook/index.d.ts +8 -0
- package/dist/remote/ingestors/webhook/index.js +12 -0
- package/dist/remote/ingestors/webhook/stripe-types.d.ts +57 -0
- package/dist/remote/ingestors/webhook/stripe-types.js +108 -0
- package/dist/remote/ingestors/webhook/stripe-webhook-ingestor.d.ts +47 -0
- package/dist/remote/ingestors/webhook/stripe-webhook-ingestor.js +90 -0
- package/dist/remote/ingestors/webhook/trello-types.d.ts +90 -0
- package/dist/remote/ingestors/webhook/trello-types.js +81 -0
- package/dist/remote/ingestors/webhook/trello-webhook-ingestor.d.ts +60 -0
- package/dist/remote/ingestors/webhook/trello-webhook-ingestor.js +126 -0
- package/dist/remote/server.d.ts +103 -0
- package/dist/remote/server.js +536 -0
- package/dist/shared/config.d.ts +213 -0
- package/dist/shared/config.js +269 -0
- package/dist/shared/connections.d.ts +72 -0
- package/dist/shared/connections.js +103 -0
- package/dist/shared/crypto/channel.d.ts +95 -0
- package/dist/shared/crypto/channel.js +175 -0
- package/dist/shared/crypto/index.d.ts +3 -0
- package/dist/shared/crypto/index.js +3 -0
- package/dist/shared/crypto/keys.d.ts +92 -0
- package/dist/shared/crypto/keys.js +143 -0
- package/dist/shared/logger.d.ts +30 -0
- package/dist/shared/logger.js +74 -0
- package/dist/shared/protocol/handshake.d.ts +116 -0
- package/dist/shared/protocol/handshake.js +214 -0
- package/dist/shared/protocol/index.d.ts +3 -0
- package/dist/shared/protocol/index.js +2 -0
- package/dist/shared/protocol/messages.d.ts +46 -0
- package/dist/shared/protocol/messages.js +8 -0
- package/package.json +105 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic bounded ring buffer for event storage.
|
|
3
|
+
*
|
|
4
|
+
* When the buffer is full, the oldest item is evicted to make room.
|
|
5
|
+
* Each pushed item receives a monotonically increasing ID that persists
|
|
6
|
+
* across evictions and clears — enabling cursor-based polling.
|
|
7
|
+
*/
|
|
8
|
+
export declare class RingBuffer<T> {
|
|
9
|
+
private readonly capacity;
|
|
10
|
+
private buffer;
|
|
11
|
+
private head;
|
|
12
|
+
private count;
|
|
13
|
+
constructor(capacity: number);
|
|
14
|
+
/**
|
|
15
|
+
* Push an item into the buffer.
|
|
16
|
+
* If the buffer is full, the oldest item is evicted.
|
|
17
|
+
*/
|
|
18
|
+
push(item: T): void;
|
|
19
|
+
/**
|
|
20
|
+
* Return all buffered items in chronological order (oldest first).
|
|
21
|
+
*/
|
|
22
|
+
toArray(): T[];
|
|
23
|
+
/**
|
|
24
|
+
* Return items where the `id` field is greater than `afterId`.
|
|
25
|
+
* Assumes items have a numeric `id` field (enforced at the call site).
|
|
26
|
+
*/
|
|
27
|
+
since(afterId: number): T[];
|
|
28
|
+
/** Current number of items in the buffer. */
|
|
29
|
+
get size(): number;
|
|
30
|
+
/** Remove all items. Does NOT reset any external ID counters. */
|
|
31
|
+
clear(): void;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=ring-buffer.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic bounded ring buffer for event storage.
|
|
3
|
+
*
|
|
4
|
+
* When the buffer is full, the oldest item is evicted to make room.
|
|
5
|
+
* Each pushed item receives a monotonically increasing ID that persists
|
|
6
|
+
* across evictions and clears — enabling cursor-based polling.
|
|
7
|
+
*/
|
|
8
|
+
export class RingBuffer {
|
|
9
|
+
capacity;
|
|
10
|
+
buffer;
|
|
11
|
+
head = 0; // next write position
|
|
12
|
+
count = 0; // number of items currently stored
|
|
13
|
+
constructor(capacity) {
|
|
14
|
+
this.capacity = capacity;
|
|
15
|
+
this.buffer = new Array(capacity);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Push an item into the buffer.
|
|
19
|
+
* If the buffer is full, the oldest item is evicted.
|
|
20
|
+
*/
|
|
21
|
+
push(item) {
|
|
22
|
+
this.buffer[this.head] = item;
|
|
23
|
+
this.head = (this.head + 1) % this.capacity;
|
|
24
|
+
if (this.count < this.capacity) {
|
|
25
|
+
this.count++;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Return all buffered items in chronological order (oldest first).
|
|
30
|
+
*/
|
|
31
|
+
toArray() {
|
|
32
|
+
if (this.count === 0)
|
|
33
|
+
return [];
|
|
34
|
+
const start = (this.head - this.count + this.capacity) % this.capacity;
|
|
35
|
+
const result = [];
|
|
36
|
+
for (let i = 0; i < this.count; i++) {
|
|
37
|
+
result.push(this.buffer[(start + i) % this.capacity]);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Return items where the `id` field is greater than `afterId`.
|
|
43
|
+
* Assumes items have a numeric `id` field (enforced at the call site).
|
|
44
|
+
*/
|
|
45
|
+
since(afterId) {
|
|
46
|
+
return this.toArray().filter((item) => {
|
|
47
|
+
const id = item.id;
|
|
48
|
+
return typeof id === 'number' && id > afterId;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Current number of items in the buffer. */
|
|
52
|
+
get size() {
|
|
53
|
+
return this.count;
|
|
54
|
+
}
|
|
55
|
+
/** Remove all items. Does NOT reset any external ID counters. */
|
|
56
|
+
clear() {
|
|
57
|
+
this.buffer = new Array(this.capacity);
|
|
58
|
+
this.head = 0;
|
|
59
|
+
this.count = 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=ring-buffer.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { SlackSocketModeIngestor } from './socket-mode.js';
|
|
2
|
+
export { type SlackMessageType, type SlackDisconnectReason, type SlackEnvelope, type SlackHello, type SlackDisconnect, type SlackConnectionsOpenResponse, extractSlackEventType, extractSlackChannelId, extractSlackUserId, } from './types.js';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack Socket Mode ingestor.
|
|
3
|
+
*
|
|
4
|
+
* Connects to Slack via Socket Mode WebSocket and streams real-time events
|
|
5
|
+
* (messages, slash commands, interactions, etc.) into the ring buffer.
|
|
6
|
+
*
|
|
7
|
+
* Implements the Socket Mode lifecycle:
|
|
8
|
+
* 1. POST apps.connections.open (app-level token) → get WebSocket URL
|
|
9
|
+
* 2. Connect to WebSocket → receive `hello`
|
|
10
|
+
* 3. Receive envelopes → acknowledge → buffer events
|
|
11
|
+
* 4. Handle `disconnect` messages → reconnect
|
|
12
|
+
*
|
|
13
|
+
* Uses the native WebSocket API (Node 22+) and native fetch.
|
|
14
|
+
*
|
|
15
|
+
* @see https://docs.slack.dev/apis/events-api/using-socket-mode
|
|
16
|
+
*/
|
|
17
|
+
import { BaseIngestor } from '../base-ingestor.js';
|
|
18
|
+
import type { WebSocketIngestorConfig } from '../types.js';
|
|
19
|
+
export declare class SlackSocketModeIngestor extends BaseIngestor {
|
|
20
|
+
private readonly wsConfig;
|
|
21
|
+
private ws;
|
|
22
|
+
private reconnectAttempts;
|
|
23
|
+
private readonly maxReconnectAttempts;
|
|
24
|
+
private reconnectTimer;
|
|
25
|
+
/** The URL for the apps.connections.open endpoint. */
|
|
26
|
+
private readonly connectUrl;
|
|
27
|
+
private readonly eventFilter;
|
|
28
|
+
private readonly channelIds;
|
|
29
|
+
private readonly userIds;
|
|
30
|
+
constructor(connectionAlias: string, secrets: Record<string, string>, wsConfig: WebSocketIngestorConfig, bufferSize?: number);
|
|
31
|
+
start(): Promise<void>;
|
|
32
|
+
stop(): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Call apps.connections.open to get a dynamic WebSocket URL, then connect.
|
|
35
|
+
*/
|
|
36
|
+
private openConnection;
|
|
37
|
+
private connectWebSocket;
|
|
38
|
+
private handleEnvelope;
|
|
39
|
+
private handleHello;
|
|
40
|
+
private handleDisconnect;
|
|
41
|
+
private handleEvent;
|
|
42
|
+
private acknowledge;
|
|
43
|
+
private initiateReconnect;
|
|
44
|
+
private scheduleReconnect;
|
|
45
|
+
private send;
|
|
46
|
+
private clearReconnectTimer;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=socket-mode.d.ts.map
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack Socket Mode ingestor.
|
|
3
|
+
*
|
|
4
|
+
* Connects to Slack via Socket Mode WebSocket and streams real-time events
|
|
5
|
+
* (messages, slash commands, interactions, etc.) into the ring buffer.
|
|
6
|
+
*
|
|
7
|
+
* Implements the Socket Mode lifecycle:
|
|
8
|
+
* 1. POST apps.connections.open (app-level token) → get WebSocket URL
|
|
9
|
+
* 2. Connect to WebSocket → receive `hello`
|
|
10
|
+
* 3. Receive envelopes → acknowledge → buffer events
|
|
11
|
+
* 4. Handle `disconnect` messages → reconnect
|
|
12
|
+
*
|
|
13
|
+
* Uses the native WebSocket API (Node 22+) and native fetch.
|
|
14
|
+
*
|
|
15
|
+
* @see https://docs.slack.dev/apis/events-api/using-socket-mode
|
|
16
|
+
*/
|
|
17
|
+
import { BaseIngestor } from '../base-ingestor.js';
|
|
18
|
+
import { registerIngestorFactory } from '../registry.js';
|
|
19
|
+
import { extractSlackEventType, extractSlackChannelId, extractSlackUserId, } from './types.js';
|
|
20
|
+
import { createLogger } from '../../../shared/logger.js';
|
|
21
|
+
const log = createLogger('slack-sm');
|
|
22
|
+
// ── Slack Socket Mode ingestor ──────────────────────────────────────────
|
|
23
|
+
export class SlackSocketModeIngestor extends BaseIngestor {
|
|
24
|
+
wsConfig;
|
|
25
|
+
ws = null;
|
|
26
|
+
reconnectAttempts = 0;
|
|
27
|
+
maxReconnectAttempts = 10;
|
|
28
|
+
reconnectTimer = null;
|
|
29
|
+
/** The URL for the apps.connections.open endpoint. */
|
|
30
|
+
connectUrl;
|
|
31
|
+
eventFilter;
|
|
32
|
+
channelIds;
|
|
33
|
+
userIds;
|
|
34
|
+
constructor(connectionAlias, secrets, wsConfig, bufferSize) {
|
|
35
|
+
super(connectionAlias, 'websocket', secrets, bufferSize);
|
|
36
|
+
this.wsConfig = wsConfig;
|
|
37
|
+
this.connectUrl = wsConfig.gatewayUrl;
|
|
38
|
+
this.eventFilter = wsConfig.eventFilter ?? [];
|
|
39
|
+
this.channelIds = new Set(wsConfig.channelIds ?? []);
|
|
40
|
+
this.userIds = new Set(wsConfig.userIds ?? []);
|
|
41
|
+
}
|
|
42
|
+
async start() {
|
|
43
|
+
this.state = 'starting';
|
|
44
|
+
await this.openConnection();
|
|
45
|
+
}
|
|
46
|
+
stop() {
|
|
47
|
+
this.state = 'stopped';
|
|
48
|
+
this.clearReconnectTimer();
|
|
49
|
+
if (this.ws) {
|
|
50
|
+
this.ws.close(1000, 'Shutting down');
|
|
51
|
+
this.ws = null;
|
|
52
|
+
}
|
|
53
|
+
return Promise.resolve();
|
|
54
|
+
}
|
|
55
|
+
// ── Connection establishment ────────────────────────────────────────
|
|
56
|
+
/**
|
|
57
|
+
* Call apps.connections.open to get a dynamic WebSocket URL, then connect.
|
|
58
|
+
*/
|
|
59
|
+
async openConnection() {
|
|
60
|
+
const appToken = this.secrets.SLACK_APP_TOKEN;
|
|
61
|
+
if (!appToken) {
|
|
62
|
+
this.state = 'error';
|
|
63
|
+
this.errorMessage = 'SLACK_APP_TOKEN not found in resolved secrets';
|
|
64
|
+
log.error(`${this.errorMessage} (${this.connectionAlias})`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let wsUrl;
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch(this.connectUrl, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: {
|
|
72
|
+
Authorization: `Bearer ${appToken}`,
|
|
73
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const body = (await response.json());
|
|
77
|
+
if (!body.ok || !body.url) {
|
|
78
|
+
this.state = 'error';
|
|
79
|
+
this.errorMessage = `apps.connections.open failed: ${body.error ?? 'no URL returned'}`;
|
|
80
|
+
log.error(`${this.errorMessage} (${this.connectionAlias})`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
wsUrl = body.url;
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
this.state = 'error';
|
|
87
|
+
this.errorMessage = `Failed to call apps.connections.open: ${err instanceof Error ? err.message : String(err)}`;
|
|
88
|
+
log.error(`${this.errorMessage} (${this.connectionAlias})`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.connectWebSocket(wsUrl);
|
|
92
|
+
}
|
|
93
|
+
// ── WebSocket connection ────────────────────────────────────────────
|
|
94
|
+
connectWebSocket(url) {
|
|
95
|
+
try {
|
|
96
|
+
this.ws = new WebSocket(url);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
this.state = 'error';
|
|
100
|
+
this.errorMessage = `Failed to create WebSocket: ${err instanceof Error ? err.message : String(err)}`;
|
|
101
|
+
log.error(`${this.errorMessage} (${this.connectionAlias})`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.ws.addEventListener('open', () => {
|
|
105
|
+
log.info(`WebSocket connected for ${this.connectionAlias}`);
|
|
106
|
+
});
|
|
107
|
+
this.ws.addEventListener('message', (event) => {
|
|
108
|
+
try {
|
|
109
|
+
const data = typeof event.data === 'string' ? event.data : String(event.data);
|
|
110
|
+
const envelope = JSON.parse(data);
|
|
111
|
+
this.handleEnvelope(envelope);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
log.error(`Failed to parse Socket Mode message:`, err);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
this.ws.addEventListener('close', (event) => {
|
|
118
|
+
log.info(`Connection closed for ${this.connectionAlias}: ${event.code} ${event.reason}`);
|
|
119
|
+
if (this.state !== 'stopped') {
|
|
120
|
+
this.scheduleReconnect();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
this.ws.addEventListener('error', () => {
|
|
124
|
+
// The 'close' event always follows 'error', so we handle reconnection there.
|
|
125
|
+
log.error(`WebSocket error for ${this.connectionAlias}`);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// ── Envelope dispatch ──────────────────────────────────────────────
|
|
129
|
+
handleEnvelope(envelope) {
|
|
130
|
+
switch (envelope.type) {
|
|
131
|
+
case 'hello':
|
|
132
|
+
this.handleHello(envelope);
|
|
133
|
+
break;
|
|
134
|
+
case 'disconnect':
|
|
135
|
+
this.handleDisconnect(envelope);
|
|
136
|
+
break;
|
|
137
|
+
case 'events_api':
|
|
138
|
+
case 'slash_commands':
|
|
139
|
+
case 'interactive':
|
|
140
|
+
this.handleEvent(envelope);
|
|
141
|
+
break;
|
|
142
|
+
default:
|
|
143
|
+
log.warn(`Unknown message type "${String(envelope.type)}" for ${this.connectionAlias}`);
|
|
144
|
+
// Still acknowledge if there's an envelope_id
|
|
145
|
+
if (envelope.envelope_id) {
|
|
146
|
+
this.acknowledge(envelope.envelope_id);
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// ── Hello ─────────────────────────────────────────────────────────
|
|
152
|
+
handleHello(hello) {
|
|
153
|
+
this.state = 'connected';
|
|
154
|
+
this.reconnectAttempts = 0;
|
|
155
|
+
log.info(`Connected for ${this.connectionAlias} ` +
|
|
156
|
+
`(app: ${hello.connection_info.app_id}, connections: ${hello.num_connections}, ` +
|
|
157
|
+
`refresh in ~${hello.debug_info.approximate_connection_time}s)`);
|
|
158
|
+
}
|
|
159
|
+
// ── Disconnect ────────────────────────────────────────────────────
|
|
160
|
+
handleDisconnect(disconnect) {
|
|
161
|
+
log.info(`Disconnect for ${this.connectionAlias}: ${disconnect.reason} ` +
|
|
162
|
+
`(host: ${disconnect.debug_info.host})`);
|
|
163
|
+
switch (disconnect.reason) {
|
|
164
|
+
case 'refresh_requested':
|
|
165
|
+
case 'warning':
|
|
166
|
+
// Normal refresh — reconnect immediately
|
|
167
|
+
this.initiateReconnect();
|
|
168
|
+
break;
|
|
169
|
+
case 'link_disabled':
|
|
170
|
+
// Socket Mode was disabled for the app — stop permanently
|
|
171
|
+
this.state = 'error';
|
|
172
|
+
this.errorMessage = 'Socket Mode was disabled for this app (link_disabled)';
|
|
173
|
+
log.error(`${this.errorMessage} (${this.connectionAlias})`);
|
|
174
|
+
if (this.ws) {
|
|
175
|
+
this.ws.close(1000, 'Link disabled');
|
|
176
|
+
this.ws = null;
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// ── Event handling ────────────────────────────────────────────────
|
|
182
|
+
handleEvent(envelope) {
|
|
183
|
+
// Always acknowledge first — Slack expects prompt acks
|
|
184
|
+
if (envelope.envelope_id) {
|
|
185
|
+
this.acknowledge(envelope.envelope_id);
|
|
186
|
+
}
|
|
187
|
+
// Determine the specific event type for filtering and buffering
|
|
188
|
+
const eventType = extractSlackEventType(envelope);
|
|
189
|
+
// Apply event type filter (empty filter = capture all)
|
|
190
|
+
if (this.eventFilter.length > 0 && !this.eventFilter.includes(eventType)) {
|
|
191
|
+
log.debug(`${this.connectionAlias} event filtered out by eventFilter: ${eventType}`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// Apply payload-level filters.
|
|
195
|
+
// Events without the filtered field pass through (same convention as Discord).
|
|
196
|
+
if (this.channelIds.size > 0) {
|
|
197
|
+
const channelId = extractSlackChannelId(envelope);
|
|
198
|
+
if (channelId && !this.channelIds.has(channelId))
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (this.userIds.size > 0) {
|
|
202
|
+
const userId = extractSlackUserId(envelope);
|
|
203
|
+
if (userId && !this.userIds.has(userId))
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Buffer the event (use envelope_id as idempotency key for retry dedup)
|
|
207
|
+
const idempotencyKey = envelope.envelope_id ? `slack:${envelope.envelope_id}` : undefined;
|
|
208
|
+
log.debug(`${this.connectionAlias} dispatching event: ${eventType} (envelope: ${envelope.type})`);
|
|
209
|
+
this.pushEvent(eventType, envelope.payload, idempotencyKey);
|
|
210
|
+
}
|
|
211
|
+
// ── Acknowledgment ────────────────────────────────────────────────
|
|
212
|
+
acknowledge(envelopeId) {
|
|
213
|
+
this.send({ envelope_id: envelopeId });
|
|
214
|
+
}
|
|
215
|
+
// ── Reconnection ──────────────────────────────────────────────────
|
|
216
|
+
initiateReconnect() {
|
|
217
|
+
this.clearReconnectTimer();
|
|
218
|
+
if (this.ws) {
|
|
219
|
+
this.ws.close(1000, 'Reconnecting');
|
|
220
|
+
this.ws = null;
|
|
221
|
+
}
|
|
222
|
+
// Reset attempts for intentional refreshes (Slack-initiated)
|
|
223
|
+
this.reconnectAttempts = 0;
|
|
224
|
+
this.scheduleReconnect();
|
|
225
|
+
}
|
|
226
|
+
scheduleReconnect() {
|
|
227
|
+
if (this.state === 'stopped')
|
|
228
|
+
return;
|
|
229
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
230
|
+
this.state = 'error';
|
|
231
|
+
this.errorMessage = `Max reconnect attempts (${this.maxReconnectAttempts}) exceeded`;
|
|
232
|
+
log.error(`${this.errorMessage} (${this.connectionAlias})`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
this.state = 'reconnecting';
|
|
236
|
+
this.reconnectAttempts++;
|
|
237
|
+
const backoff = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30_000);
|
|
238
|
+
log.info(`Reconnecting ${this.connectionAlias} in ${backoff}ms ` +
|
|
239
|
+
`(attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
240
|
+
this.reconnectTimer = setTimeout(() => {
|
|
241
|
+
this.reconnectTimer = null;
|
|
242
|
+
// Must re-fetch a new WebSocket URL each time — Slack URLs are single-use
|
|
243
|
+
void this.openConnection();
|
|
244
|
+
}, backoff);
|
|
245
|
+
}
|
|
246
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
247
|
+
send(data) {
|
|
248
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
249
|
+
this.ws.send(JSON.stringify(data));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
clearReconnectTimer() {
|
|
253
|
+
if (this.reconnectTimer) {
|
|
254
|
+
clearTimeout(this.reconnectTimer);
|
|
255
|
+
this.reconnectTimer = null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// ── Self-registration ────────────────────────────────────────────────────
|
|
260
|
+
registerIngestorFactory('websocket:slack', (connectionAlias, config, secrets, bufferSize) => {
|
|
261
|
+
if (!config.websocket) {
|
|
262
|
+
log.error(`Missing websocket config for ${connectionAlias}`);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
return new SlackSocketModeIngestor(connectionAlias, secrets, config.websocket, bufferSize);
|
|
266
|
+
});
|
|
267
|
+
//# sourceMappingURL=socket-mode.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack-specific types and constants for the Socket Mode ingestor.
|
|
3
|
+
*
|
|
4
|
+
* @see https://docs.slack.dev/apis/events-api/using-socket-mode
|
|
5
|
+
*/
|
|
6
|
+
/** Top-level message types received over the Socket Mode WebSocket. */
|
|
7
|
+
export type SlackMessageType = 'hello' | 'events_api' | 'slash_commands' | 'interactive' | 'disconnect';
|
|
8
|
+
/** Reasons a Socket Mode disconnect message may be sent. */
|
|
9
|
+
export type SlackDisconnectReason = 'link_disabled' | 'warning' | 'refresh_requested';
|
|
10
|
+
/**
|
|
11
|
+
* Envelope wrapper for all Socket Mode messages.
|
|
12
|
+
* Every non-hello/disconnect message has an `envelope_id` that must be acknowledged.
|
|
13
|
+
*/
|
|
14
|
+
export interface SlackEnvelope {
|
|
15
|
+
/** Message type — determines how to process the payload. */
|
|
16
|
+
type: SlackMessageType;
|
|
17
|
+
/** Unique ID for this envelope. Send it back to acknowledge receipt. */
|
|
18
|
+
envelope_id?: string;
|
|
19
|
+
/** The actual event/command/interaction payload. */
|
|
20
|
+
payload?: unknown;
|
|
21
|
+
/** Whether Slack accepts a response payload in the acknowledgment. */
|
|
22
|
+
accepts_response_payload?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Hello message received immediately after WebSocket connection. */
|
|
25
|
+
export interface SlackHello {
|
|
26
|
+
type: 'hello';
|
|
27
|
+
connection_info: {
|
|
28
|
+
app_id: string;
|
|
29
|
+
};
|
|
30
|
+
num_connections: number;
|
|
31
|
+
debug_info: {
|
|
32
|
+
host: string;
|
|
33
|
+
started: string;
|
|
34
|
+
build_number: number;
|
|
35
|
+
/** Approximate seconds until this connection will be refreshed. */
|
|
36
|
+
approximate_connection_time: number;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Disconnect message sent by Slack before closing the connection. */
|
|
40
|
+
export interface SlackDisconnect {
|
|
41
|
+
type: 'disconnect';
|
|
42
|
+
reason: SlackDisconnectReason;
|
|
43
|
+
debug_info: {
|
|
44
|
+
host: string;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Shape of the `apps.connections.open` API response. */
|
|
48
|
+
export interface SlackConnectionsOpenResponse {
|
|
49
|
+
ok: boolean;
|
|
50
|
+
url?: string;
|
|
51
|
+
error?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Extract the Slack event type from an envelope payload.
|
|
55
|
+
* For `events_api` envelopes, the event type is at `payload.event.type`.
|
|
56
|
+
* For `slash_commands`, we use the command name.
|
|
57
|
+
* For `interactive`, we use the interaction type.
|
|
58
|
+
*/
|
|
59
|
+
export declare function extractSlackEventType(envelope: SlackEnvelope): string;
|
|
60
|
+
/**
|
|
61
|
+
* Extract a channel ID from a Slack envelope payload.
|
|
62
|
+
* Checks `payload.event.channel`, `payload.channel_id`, and `payload.channel.id`.
|
|
63
|
+
*/
|
|
64
|
+
export declare function extractSlackChannelId(envelope: SlackEnvelope): string | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Extract a user ID from a Slack envelope payload.
|
|
67
|
+
* Checks `payload.event.user`, `payload.user_id`, and `payload.user.id`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function extractSlackUserId(envelope: SlackEnvelope): string | undefined;
|
|
70
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack-specific types and constants for the Socket Mode ingestor.
|
|
3
|
+
*
|
|
4
|
+
* @see https://docs.slack.dev/apis/events-api/using-socket-mode
|
|
5
|
+
*/
|
|
6
|
+
// ── Payload helpers ─────────────────────────────────────────────────────
|
|
7
|
+
/**
|
|
8
|
+
* Extract the Slack event type from an envelope payload.
|
|
9
|
+
* For `events_api` envelopes, the event type is at `payload.event.type`.
|
|
10
|
+
* For `slash_commands`, we use the command name.
|
|
11
|
+
* For `interactive`, we use the interaction type.
|
|
12
|
+
*/
|
|
13
|
+
export function extractSlackEventType(envelope) {
|
|
14
|
+
const payload = envelope.payload;
|
|
15
|
+
if (!payload)
|
|
16
|
+
return envelope.type;
|
|
17
|
+
switch (envelope.type) {
|
|
18
|
+
case 'events_api': {
|
|
19
|
+
const event = payload.event;
|
|
20
|
+
return typeof event?.type === 'string' ? event.type : 'events_api';
|
|
21
|
+
}
|
|
22
|
+
case 'slash_commands':
|
|
23
|
+
return typeof payload.command === 'string' ? payload.command : 'slash_command';
|
|
24
|
+
case 'interactive':
|
|
25
|
+
return typeof payload.type === 'string' ? payload.type : 'interactive';
|
|
26
|
+
default:
|
|
27
|
+
return envelope.type;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Extract a channel ID from a Slack envelope payload.
|
|
32
|
+
* Checks `payload.event.channel`, `payload.channel_id`, and `payload.channel.id`.
|
|
33
|
+
*/
|
|
34
|
+
export function extractSlackChannelId(envelope) {
|
|
35
|
+
const payload = envelope.payload;
|
|
36
|
+
if (!payload)
|
|
37
|
+
return undefined;
|
|
38
|
+
// events_api: event.channel
|
|
39
|
+
const event = payload.event;
|
|
40
|
+
if (typeof event?.channel === 'string')
|
|
41
|
+
return event.channel;
|
|
42
|
+
// slash_commands / interactive: channel_id
|
|
43
|
+
if (typeof payload.channel_id === 'string')
|
|
44
|
+
return payload.channel_id;
|
|
45
|
+
// interactive: channel.id
|
|
46
|
+
const channel = payload.channel;
|
|
47
|
+
if (typeof channel?.id === 'string')
|
|
48
|
+
return channel.id;
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Extract a user ID from a Slack envelope payload.
|
|
53
|
+
* Checks `payload.event.user`, `payload.user_id`, and `payload.user.id`.
|
|
54
|
+
*/
|
|
55
|
+
export function extractSlackUserId(envelope) {
|
|
56
|
+
const payload = envelope.payload;
|
|
57
|
+
if (!payload)
|
|
58
|
+
return undefined;
|
|
59
|
+
// events_api: event.user
|
|
60
|
+
const event = payload.event;
|
|
61
|
+
if (typeof event?.user === 'string')
|
|
62
|
+
return event.user;
|
|
63
|
+
// slash_commands / interactive: user_id
|
|
64
|
+
if (typeof payload.user_id === 'string')
|
|
65
|
+
return payload.user_id;
|
|
66
|
+
// interactive: user.id
|
|
67
|
+
const user = payload.user;
|
|
68
|
+
if (typeof user?.id === 'string')
|
|
69
|
+
return user.id;
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=types.js.map
|