@johpaz/hive 1.1.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/CONTRIBUTING.md +44 -0
- package/README.md +310 -0
- package/package.json +96 -0
- package/packages/cli/package.json +28 -0
- package/packages/cli/src/commands/agent-run.ts +168 -0
- package/packages/cli/src/commands/agents.ts +398 -0
- package/packages/cli/src/commands/chat.ts +142 -0
- package/packages/cli/src/commands/config.ts +50 -0
- package/packages/cli/src/commands/cron.ts +161 -0
- package/packages/cli/src/commands/dev.ts +95 -0
- package/packages/cli/src/commands/doctor.ts +133 -0
- package/packages/cli/src/commands/gateway.ts +443 -0
- package/packages/cli/src/commands/logs.ts +57 -0
- package/packages/cli/src/commands/mcp.ts +175 -0
- package/packages/cli/src/commands/message.ts +77 -0
- package/packages/cli/src/commands/onboard.ts +1868 -0
- package/packages/cli/src/commands/security.ts +144 -0
- package/packages/cli/src/commands/service.ts +50 -0
- package/packages/cli/src/commands/sessions.ts +116 -0
- package/packages/cli/src/commands/skills.ts +187 -0
- package/packages/cli/src/commands/update.ts +25 -0
- package/packages/cli/src/index.ts +185 -0
- package/packages/cli/src/utils/token.ts +6 -0
- package/packages/code-bridge/README.md +78 -0
- package/packages/code-bridge/package.json +18 -0
- package/packages/code-bridge/src/index.ts +95 -0
- package/packages/code-bridge/src/process-manager.ts +212 -0
- package/packages/code-bridge/src/schemas.ts +133 -0
- package/packages/core/package.json +46 -0
- package/packages/core/src/agent/agent-loop.ts +369 -0
- package/packages/core/src/agent/compaction.ts +140 -0
- package/packages/core/src/agent/context-compiler.ts +378 -0
- package/packages/core/src/agent/context-guard.ts +91 -0
- package/packages/core/src/agent/context.ts +138 -0
- package/packages/core/src/agent/conversation-store.ts +198 -0
- package/packages/core/src/agent/curator.ts +158 -0
- package/packages/core/src/agent/hooks.ts +166 -0
- package/packages/core/src/agent/index.ts +116 -0
- package/packages/core/src/agent/llm-client.ts +503 -0
- package/packages/core/src/agent/native-tools.ts +505 -0
- package/packages/core/src/agent/prompt-builder.ts +532 -0
- package/packages/core/src/agent/providers/index.ts +167 -0
- package/packages/core/src/agent/providers.ts +1 -0
- package/packages/core/src/agent/reflector.ts +170 -0
- package/packages/core/src/agent/service.ts +64 -0
- package/packages/core/src/agent/stuck-loop.ts +133 -0
- package/packages/core/src/agent/supervisor.ts +39 -0
- package/packages/core/src/agent/tracer.ts +102 -0
- package/packages/core/src/agent/workspace.ts +110 -0
- package/packages/core/src/canvas/canvas-manager.test.ts +161 -0
- package/packages/core/src/canvas/canvas-manager.ts +319 -0
- package/packages/core/src/canvas/canvas-tools.ts +420 -0
- package/packages/core/src/canvas/emitter.ts +115 -0
- package/packages/core/src/canvas/index.ts +2 -0
- package/packages/core/src/channels/base.ts +138 -0
- package/packages/core/src/channels/discord.ts +260 -0
- package/packages/core/src/channels/index.ts +7 -0
- package/packages/core/src/channels/manager.ts +383 -0
- package/packages/core/src/channels/slack.ts +287 -0
- package/packages/core/src/channels/telegram.ts +502 -0
- package/packages/core/src/channels/webchat.ts +128 -0
- package/packages/core/src/channels/whatsapp.ts +375 -0
- package/packages/core/src/config/index.ts +12 -0
- package/packages/core/src/config/loader.ts +529 -0
- package/packages/core/src/events/event-bus.ts +169 -0
- package/packages/core/src/gateway/index.ts +5 -0
- package/packages/core/src/gateway/initializer.ts +290 -0
- package/packages/core/src/gateway/lane-queue.ts +169 -0
- package/packages/core/src/gateway/resolver.ts +108 -0
- package/packages/core/src/gateway/router.ts +124 -0
- package/packages/core/src/gateway/server.ts +3317 -0
- package/packages/core/src/gateway/session.ts +95 -0
- package/packages/core/src/gateway/slash-commands.ts +192 -0
- package/packages/core/src/heartbeat/index.ts +157 -0
- package/packages/core/src/index.ts +19 -0
- package/packages/core/src/integrations/catalog.ts +286 -0
- package/packages/core/src/integrations/env.ts +64 -0
- package/packages/core/src/integrations/index.ts +2 -0
- package/packages/core/src/memory/index.ts +1 -0
- package/packages/core/src/memory/notes.ts +68 -0
- package/packages/core/src/plugins/api.ts +128 -0
- package/packages/core/src/plugins/index.ts +2 -0
- package/packages/core/src/plugins/loader.ts +365 -0
- package/packages/core/src/resilience/circuit-breaker.ts +225 -0
- package/packages/core/src/security/google-chat.ts +269 -0
- package/packages/core/src/security/index.ts +192 -0
- package/packages/core/src/security/pairing.ts +250 -0
- package/packages/core/src/security/rate-limit.ts +270 -0
- package/packages/core/src/security/signal.ts +321 -0
- package/packages/core/src/state/store.ts +312 -0
- package/packages/core/src/storage/bun-sqlite-store.ts +188 -0
- package/packages/core/src/storage/crypto.ts +101 -0
- package/packages/core/src/storage/db-context.ts +333 -0
- package/packages/core/src/storage/onboarding.ts +1087 -0
- package/packages/core/src/storage/schema.ts +541 -0
- package/packages/core/src/storage/seed.ts +571 -0
- package/packages/core/src/storage/sqlite.ts +387 -0
- package/packages/core/src/storage/usage.ts +212 -0
- package/packages/core/src/tools/bridge-events.ts +74 -0
- package/packages/core/src/tools/browser.ts +275 -0
- package/packages/core/src/tools/codebridge.ts +421 -0
- package/packages/core/src/tools/coordinator-tools.ts +179 -0
- package/packages/core/src/tools/cron.ts +611 -0
- package/packages/core/src/tools/exec.ts +140 -0
- package/packages/core/src/tools/fs.ts +364 -0
- package/packages/core/src/tools/index.ts +12 -0
- package/packages/core/src/tools/memory.ts +176 -0
- package/packages/core/src/tools/notify.ts +113 -0
- package/packages/core/src/tools/project-management.ts +376 -0
- package/packages/core/src/tools/project.ts +375 -0
- package/packages/core/src/tools/read.ts +158 -0
- package/packages/core/src/tools/web.ts +436 -0
- package/packages/core/src/tools/workspace.ts +171 -0
- package/packages/core/src/utils/benchmark.ts +80 -0
- package/packages/core/src/utils/crypto.ts +73 -0
- package/packages/core/src/utils/date.ts +42 -0
- package/packages/core/src/utils/index.ts +4 -0
- package/packages/core/src/utils/logger.ts +388 -0
- package/packages/core/src/utils/retry.ts +70 -0
- package/packages/core/src/voice/index.ts +583 -0
- package/packages/core/tsconfig.json +9 -0
- package/packages/mcp/package.json +26 -0
- package/packages/mcp/src/config.ts +13 -0
- package/packages/mcp/src/index.ts +1 -0
- package/packages/mcp/src/logger.ts +42 -0
- package/packages/mcp/src/manager.ts +434 -0
- package/packages/mcp/src/transports/index.ts +67 -0
- package/packages/mcp/src/transports/sse.ts +241 -0
- package/packages/mcp/src/transports/websocket.ts +159 -0
- package/packages/skills/package.json +21 -0
- package/packages/skills/src/bundled/agent_management/SKILL.md +24 -0
- package/packages/skills/src/bundled/browser_automation/SKILL.md +30 -0
- package/packages/skills/src/bundled/context_compact/SKILL.md +35 -0
- package/packages/skills/src/bundled/cron_manager/SKILL.md +52 -0
- package/packages/skills/src/bundled/file_manager/SKILL.md +76 -0
- package/packages/skills/src/bundled/http_client/SKILL.md +24 -0
- package/packages/skills/src/bundled/memory/SKILL.md +42 -0
- package/packages/skills/src/bundled/project_management/SKILL.md +26 -0
- package/packages/skills/src/bundled/shell/SKILL.md +43 -0
- package/packages/skills/src/bundled/system_notify/SKILL.md +52 -0
- package/packages/skills/src/bundled/voice/SKILL.md +25 -0
- package/packages/skills/src/bundled/web_search/SKILL.md +29 -0
- package/packages/skills/src/index.ts +1 -0
- package/packages/skills/src/loader.ts +282 -0
- package/packages/tools/package.json +43 -0
- package/packages/tools/src/browser/browser.test.ts +111 -0
- package/packages/tools/src/browser/index.ts +272 -0
- package/packages/tools/src/canvas/index.ts +220 -0
- package/packages/tools/src/cron/cron.test.ts +164 -0
- package/packages/tools/src/cron/index.ts +304 -0
- package/packages/tools/src/filesystem/filesystem.test.ts +240 -0
- package/packages/tools/src/filesystem/index.ts +379 -0
- package/packages/tools/src/git/index.ts +239 -0
- package/packages/tools/src/index.ts +4 -0
- package/packages/tools/src/shell/detect-env.ts +70 -0
- package/packages/tools/tsconfig.json +9 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import http, { type IncomingMessage, type ServerResponse, type Server } from "http";
|
|
2
|
+
import { BaseChannel, type ChannelConfig, type IncomingMessage as HiveIncomingMessage, type OutboundMessage } from "../channels/base.ts";
|
|
3
|
+
import { logger } from "../utils/logger.ts";
|
|
4
|
+
import { pairingService } from "./pairing.ts";
|
|
5
|
+
|
|
6
|
+
export interface GoogleChatConfig extends ChannelConfig {
|
|
7
|
+
projectId?: string;
|
|
8
|
+
serviceAccountKey?: string;
|
|
9
|
+
webhookPort?: number;
|
|
10
|
+
webhookPath?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface GoogleChatEvent {
|
|
14
|
+
type: string;
|
|
15
|
+
eventTime: string;
|
|
16
|
+
space: {
|
|
17
|
+
name: string;
|
|
18
|
+
displayName: string;
|
|
19
|
+
type: "ROOM" | "DM";
|
|
20
|
+
};
|
|
21
|
+
message?: {
|
|
22
|
+
name: string;
|
|
23
|
+
sender: {
|
|
24
|
+
name: string;
|
|
25
|
+
displayName: string;
|
|
26
|
+
};
|
|
27
|
+
createTime: string;
|
|
28
|
+
text: string;
|
|
29
|
+
thread?: {
|
|
30
|
+
name: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
user?: {
|
|
34
|
+
name: string;
|
|
35
|
+
displayName: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class GoogleChatChannel extends BaseChannel {
|
|
40
|
+
name = "google-chat";
|
|
41
|
+
accountId: string;
|
|
42
|
+
config: GoogleChatConfig;
|
|
43
|
+
|
|
44
|
+
private server?: Server;
|
|
45
|
+
private log = logger.child("google-chat");
|
|
46
|
+
private spaceCache: Map<string, { space: string; thread?: string }> = new Map();
|
|
47
|
+
|
|
48
|
+
constructor(accountId: string, config: GoogleChatConfig) {
|
|
49
|
+
super();
|
|
50
|
+
this.accountId = accountId;
|
|
51
|
+
this.config = {
|
|
52
|
+
...config,
|
|
53
|
+
dmPolicy: config.dmPolicy ?? "pairing",
|
|
54
|
+
allowFrom: config.allowFrom ?? [],
|
|
55
|
+
enabled: config.enabled ?? true,
|
|
56
|
+
webhookPort: config.webhookPort ?? 8080,
|
|
57
|
+
webhookPath: config.webhookPath ?? "/webhook/google-chat",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async start(): Promise<void> {
|
|
62
|
+
this.server = http.createServer((req, res) => {
|
|
63
|
+
if (req.url === this.config.webhookPath && req.method === "POST") {
|
|
64
|
+
this.handleWebhook(req, res);
|
|
65
|
+
} else {
|
|
66
|
+
res.writeHead(404).end();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
this.server!.listen(this.config.webhookPort, () => {
|
|
72
|
+
this.running = true;
|
|
73
|
+
this.log.info(
|
|
74
|
+
`Google Chat webhook listening on port ${this.config.webhookPort}${this.config.webhookPath}`
|
|
75
|
+
);
|
|
76
|
+
resolve();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
this.server!.on("error", (error: Error) => {
|
|
80
|
+
this.log.error(`Server error: ${error.message}`);
|
|
81
|
+
reject(error);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async handleWebhook(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
87
|
+
let body = "";
|
|
88
|
+
req.on("data", (chunk) => (body += chunk));
|
|
89
|
+
req.on("end", async () => {
|
|
90
|
+
try {
|
|
91
|
+
const event: GoogleChatEvent = JSON.parse(body);
|
|
92
|
+
|
|
93
|
+
if (event.type === "ADDED_TO_SPACE") {
|
|
94
|
+
await this.handleAddedToSpace(event, res);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (event.type === "REMOVED_FROM_SPACE") {
|
|
99
|
+
this.log.info(`Removed from space: ${event.space.name}`);
|
|
100
|
+
res.writeHead(200).end();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (event.type === "MESSAGE" && event.message) {
|
|
105
|
+
await this.handleChatMessage(event, res);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
res.writeHead(200).end();
|
|
110
|
+
} catch (error) {
|
|
111
|
+
this.log.error(`Webhook error: ${(error as Error).message}`);
|
|
112
|
+
res.writeHead(500).end();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async handleAddedToSpace(
|
|
118
|
+
event: GoogleChatEvent,
|
|
119
|
+
res: ServerResponse
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
const message =
|
|
122
|
+
event.space.type === "DM"
|
|
123
|
+
? {
|
|
124
|
+
text: "¡Hola! Soy tu asistente AI. Envía un mensaje para comenzar.",
|
|
125
|
+
}
|
|
126
|
+
: {
|
|
127
|
+
text: "¡Gracias por añadirme al espacio! Mencióname con @bot para interactuar.",
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
131
|
+
res.end(JSON.stringify(message));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async handleChatMessage(event: GoogleChatEvent, res: ServerResponse): Promise<void> {
|
|
135
|
+
if (!event.message) {
|
|
136
|
+
res.writeHead(200).end();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const userId = event.message.sender.name.split("/").pop() ?? "unknown";
|
|
141
|
+
const spaceName = event.space.name;
|
|
142
|
+
const isDM = event.space.type === "DM";
|
|
143
|
+
const kind = isDM ? "direct" : "group";
|
|
144
|
+
const peerId = isDM ? userId : `${spaceName}:${userId}`;
|
|
145
|
+
|
|
146
|
+
if (event.message.text === "/myid") {
|
|
147
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
148
|
+
res.end(
|
|
149
|
+
JSON.stringify({
|
|
150
|
+
text: `🆔 Tu Google Chat ID es: ${userId}\n\nPara emparejar, solicita un código al administrador.`,
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (event.message.text.startsWith("/pair ")) {
|
|
157
|
+
const code = event.message.text.split(" ")[1]?.trim();
|
|
158
|
+
const result = pairingService.approve(code ?? "");
|
|
159
|
+
|
|
160
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
161
|
+
res.end(
|
|
162
|
+
JSON.stringify({
|
|
163
|
+
text: result.success
|
|
164
|
+
? "✅ ¡Emparejamiento exitoso!"
|
|
165
|
+
: `❌ ${result.error}`,
|
|
166
|
+
})
|
|
167
|
+
);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (this.config.dmPolicy === "pairing" && !pairingService.isAllowed("google-chat", userId)) {
|
|
172
|
+
this.log.debug(`Message from unpaired user: ${userId}`);
|
|
173
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
174
|
+
res.end(
|
|
175
|
+
JSON.stringify({
|
|
176
|
+
text:
|
|
177
|
+
"⛔ No estás emparejado.\n\n" +
|
|
178
|
+
"Tu ID: " +
|
|
179
|
+
userId +
|
|
180
|
+
"\n\n" +
|
|
181
|
+
"Solicita un código de emparejamiento al administrador.",
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!isDM && !this.isUserAllowed(peerId)) {
|
|
188
|
+
this.log.debug(`Message from unauthorized user: ${peerId}`);
|
|
189
|
+
res.writeHead(200).end();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const sessionId = this.formatSessionId(peerId, kind);
|
|
194
|
+
this.spaceCache.set(sessionId, {
|
|
195
|
+
space: spaceName,
|
|
196
|
+
thread: event.message.thread?.name,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const incomingMessage: HiveIncomingMessage = {
|
|
200
|
+
sessionId,
|
|
201
|
+
channel: "google-chat",
|
|
202
|
+
accountId: this.accountId,
|
|
203
|
+
peerId,
|
|
204
|
+
peerKind: kind,
|
|
205
|
+
content: event.message.text,
|
|
206
|
+
metadata: {
|
|
207
|
+
googleChat: {
|
|
208
|
+
spaceName,
|
|
209
|
+
userId,
|
|
210
|
+
displayName: event.message.sender.displayName,
|
|
211
|
+
threadName: event.message.thread?.name,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
res.writeHead(200).end();
|
|
217
|
+
|
|
218
|
+
await this.handleMessage(incomingMessage);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async stop(): Promise<void> {
|
|
222
|
+
if (this.server) {
|
|
223
|
+
return new Promise((resolve) => {
|
|
224
|
+
this.server!.close(() => {
|
|
225
|
+
this.running = false;
|
|
226
|
+
this.log.info("Google Chat channel stopped");
|
|
227
|
+
resolve();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async send(sessionId: string, message: OutboundMessage): Promise<void> {
|
|
234
|
+
const content = message.content ?? "";
|
|
235
|
+
|
|
236
|
+
if (!content || content.trim().length === 0) {
|
|
237
|
+
this.log.warn("Empty response, skipping send");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const cached = this.spaceCache.get(sessionId);
|
|
242
|
+
|
|
243
|
+
if (!cached) {
|
|
244
|
+
this.log.warn(`No cached space for session: ${sessionId}`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (message.type === "stream" && message.chunk) {
|
|
249
|
+
this.log.info(`[Google Chat] Stream chunk to ${cached.space}: ${message.chunk.slice(0, 50)}...`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
this.log.info(`[Google Chat] Would send to ${cached.space}: ${content.slice(0, 100)}...`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async sendMessage(space: string, content: string, thread?: string): Promise<void> {
|
|
257
|
+
this.log.info(`[Google Chat] Sending to ${space}: ${content.slice(0, 100)}...`);
|
|
258
|
+
if (thread) {
|
|
259
|
+
this.log.info(` Thread: ${thread}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function createGoogleChatChannel(
|
|
265
|
+
accountId: string,
|
|
266
|
+
config: GoogleChatConfig
|
|
267
|
+
): GoogleChatChannel {
|
|
268
|
+
return new GoogleChatChannel(accountId, config);
|
|
269
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { Config } from "../config/loader.ts";
|
|
2
|
+
import { logger } from "../utils/logger.ts";
|
|
3
|
+
|
|
4
|
+
export * from "./pairing.ts";
|
|
5
|
+
export * from "./rate-limit.ts";
|
|
6
|
+
export * from "./signal.ts";
|
|
7
|
+
export * from "./google-chat.ts";
|
|
8
|
+
|
|
9
|
+
export interface RateLimitConfig {
|
|
10
|
+
windowMs: number;
|
|
11
|
+
maxRequests: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RateLimitEntry {
|
|
15
|
+
count: number;
|
|
16
|
+
resetAt: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class RateLimiter {
|
|
20
|
+
private limits: Map<string, RateLimitEntry> = new Map();
|
|
21
|
+
private config: RateLimitConfig;
|
|
22
|
+
private log = logger.child("rate-limiter");
|
|
23
|
+
|
|
24
|
+
constructor(config: RateLimitConfig) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.startCleanup();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
check(key: string): { allowed: boolean; remaining: number; resetAt: number } {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
const entry = this.limits.get(key);
|
|
32
|
+
|
|
33
|
+
if (!entry || now > entry.resetAt) {
|
|
34
|
+
const resetAt = now + this.config.windowMs;
|
|
35
|
+
this.limits.set(key, { count: 1, resetAt });
|
|
36
|
+
return { allowed: true, remaining: this.config.maxRequests - 1, resetAt };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (entry.count >= this.config.maxRequests) {
|
|
40
|
+
this.log.warn(`Rate limit exceeded for ${key}`);
|
|
41
|
+
return { allowed: false, remaining: 0, resetAt: entry.resetAt };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
entry.count++;
|
|
45
|
+
return {
|
|
46
|
+
allowed: true,
|
|
47
|
+
remaining: this.config.maxRequests - entry.count,
|
|
48
|
+
resetAt: entry.resetAt
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
reset(key: string): void {
|
|
53
|
+
this.limits.delete(key);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private startCleanup(): void {
|
|
57
|
+
setInterval(() => {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
for (const [key, entry] of this.limits) {
|
|
60
|
+
if (now > entry.resetAt) {
|
|
61
|
+
this.limits.delete(key);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}, this.config.windowMs);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class InputValidator {
|
|
69
|
+
private maxMessageLength: number;
|
|
70
|
+
private maxCommandArgs: number;
|
|
71
|
+
private log = logger.child("validator");
|
|
72
|
+
|
|
73
|
+
constructor(options: { maxMessageLength?: number; maxCommandArgs?: number } = {}) {
|
|
74
|
+
this.maxMessageLength = options.maxMessageLength ?? 100000;
|
|
75
|
+
this.maxCommandArgs = options.maxCommandArgs ?? 50;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
validateMessage(content: string): { valid: boolean; error?: string } {
|
|
79
|
+
if (typeof content !== "string") {
|
|
80
|
+
return { valid: false, error: "Message must be a string" };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (content.length === 0) {
|
|
84
|
+
return { valid: false, error: "Message cannot be empty" };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (content.length > this.maxMessageLength) {
|
|
88
|
+
this.log.warn(`Message too long: ${content.length} > ${this.maxMessageLength}`);
|
|
89
|
+
return {
|
|
90
|
+
valid: false,
|
|
91
|
+
error: `Message too long (max ${this.maxMessageLength} characters)`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { valid: true };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
validateCommand(name: string, args: string[]): { valid: boolean; error?: string } {
|
|
99
|
+
if (!name || typeof name !== "string") {
|
|
100
|
+
return { valid: false, error: "Command name is required" };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!/^[a-z0-9_-]+$/.test(name)) {
|
|
104
|
+
return { valid: false, error: "Invalid command name format" };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (args.length > this.maxCommandArgs) {
|
|
108
|
+
return { valid: false, error: `Too many arguments (max ${this.maxCommandArgs})` };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { valid: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
validateSessionId(sessionId: string): { valid: boolean; error?: string } {
|
|
115
|
+
if (!sessionId || typeof sessionId !== "string") {
|
|
116
|
+
return { valid: false, error: "Session ID is required" };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const pattern = /^agent:[a-z0-9_-]+:[a-z0-9_-]+:(main|dm|group)(?::[a-z0-9_-]+)?$/;
|
|
120
|
+
if (!pattern.test(sessionId)) {
|
|
121
|
+
return { valid: false, error: "Invalid session ID format" };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { valid: true };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
sanitizeInput(input: string): string {
|
|
128
|
+
return input
|
|
129
|
+
.replace(/\x00/g, "")
|
|
130
|
+
.replace(/[\x1F\x7F]/g, "")
|
|
131
|
+
.trim();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export class AuthManager {
|
|
136
|
+
private config: Config;
|
|
137
|
+
private allowedUsers: Set<string>;
|
|
138
|
+
private log = logger.child("auth");
|
|
139
|
+
|
|
140
|
+
constructor(config: Config) {
|
|
141
|
+
this.config = config;
|
|
142
|
+
this.allowedUsers = new Set(config.security?.allowedUsers ?? []);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
isAllowed(peerId: string, channel: string): boolean {
|
|
146
|
+
if (this.allowedUsers.size === 0) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const channelConfig = this.config.channels?.[channel as keyof typeof this.config.channels];
|
|
151
|
+
if (channelConfig && typeof channelConfig === "object" && "allowFrom" in channelConfig) {
|
|
152
|
+
const allowFrom = (channelConfig as { allowFrom?: string[] }).allowFrom;
|
|
153
|
+
if (allowFrom && allowFrom.length > 0) {
|
|
154
|
+
return allowFrom.includes(peerId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return this.allowedUsers.has(peerId);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
addAllowedUser(peerId: string): void {
|
|
162
|
+
this.allowedUsers.add(peerId);
|
|
163
|
+
this.log.info(`Added allowed user: ${peerId}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
removeAllowedUser(peerId: string): boolean {
|
|
167
|
+
const removed = this.allowedUsers.delete(peerId);
|
|
168
|
+
if (removed) {
|
|
169
|
+
this.log.info(`Removed allowed user: ${peerId}`);
|
|
170
|
+
}
|
|
171
|
+
return removed;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
listAllowedUsers(): string[] {
|
|
175
|
+
return Array.from(this.allowedUsers);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function createRateLimiter(config: RateLimitConfig): RateLimiter {
|
|
180
|
+
return new RateLimiter(config);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function createInputValidator(options?: {
|
|
184
|
+
maxMessageLength?: number;
|
|
185
|
+
maxCommandArgs?: number;
|
|
186
|
+
}): InputValidator {
|
|
187
|
+
return new InputValidator(options);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function createAuthManager(config: Config): AuthManager {
|
|
191
|
+
return new AuthManager(config);
|
|
192
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { eventBus } from "../events/event-bus.ts";
|
|
3
|
+
import { logger } from "../utils/logger.ts";
|
|
4
|
+
|
|
5
|
+
export interface PairingCode {
|
|
6
|
+
code: string;
|
|
7
|
+
channel: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
expiresAt: number;
|
|
11
|
+
attempts: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PairingConfig {
|
|
15
|
+
codeLength?: number;
|
|
16
|
+
expirationMs?: number;
|
|
17
|
+
maxAttempts?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PairingStats {
|
|
21
|
+
pendingCodes: number;
|
|
22
|
+
totalAllowlist: number;
|
|
23
|
+
byChannel: Record<string, { pending: number; allowed: number }>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class PairingService {
|
|
27
|
+
private codes: Map<string, PairingCode> = new Map();
|
|
28
|
+
private allowlist: Map<string, Set<string>> = new Map();
|
|
29
|
+
private config: Required<PairingConfig>;
|
|
30
|
+
private log = logger.child("pairing");
|
|
31
|
+
|
|
32
|
+
constructor(config: PairingConfig = {}) {
|
|
33
|
+
this.config = {
|
|
34
|
+
codeLength: config.codeLength ?? 8,
|
|
35
|
+
expirationMs: config.expirationMs ?? 10 * 60 * 1000,
|
|
36
|
+
maxAttempts: config.maxAttempts ?? 3,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
this.startCleanup();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
generateCode(channel: string, userId: string): string {
|
|
43
|
+
this.cleanup();
|
|
44
|
+
|
|
45
|
+
const code = this.generateSecureCode();
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
|
|
48
|
+
const record: PairingCode = {
|
|
49
|
+
code,
|
|
50
|
+
channel,
|
|
51
|
+
userId,
|
|
52
|
+
createdAt: now,
|
|
53
|
+
expiresAt: now + this.config.expirationMs,
|
|
54
|
+
attempts: 0,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
this.codes.set(code, record);
|
|
58
|
+
|
|
59
|
+
this.log.info(`Generated pairing code for ${channel}:${userId}`);
|
|
60
|
+
|
|
61
|
+
eventBus.emit("pairing:requested", {
|
|
62
|
+
channel,
|
|
63
|
+
userId,
|
|
64
|
+
code,
|
|
65
|
+
expiresAt: record.expiresAt,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return code;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
validateCode(code: string): PairingCode | null {
|
|
72
|
+
const record = this.codes.get(code);
|
|
73
|
+
if (!record) return null;
|
|
74
|
+
|
|
75
|
+
if (Date.now() > record.expiresAt) {
|
|
76
|
+
this.codes.delete(code);
|
|
77
|
+
eventBus.emit("pairing:expired", {
|
|
78
|
+
code,
|
|
79
|
+
channel: record.channel,
|
|
80
|
+
userId: record.userId,
|
|
81
|
+
});
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return record;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
approve(code: string): { success: boolean; error?: string } {
|
|
89
|
+
const record = this.validateCode(code);
|
|
90
|
+
if (!record) {
|
|
91
|
+
return { success: false, error: "Invalid or expired code" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!this.allowlist.has(record.channel)) {
|
|
95
|
+
this.allowlist.set(record.channel, new Set());
|
|
96
|
+
}
|
|
97
|
+
this.allowlist.get(record.channel)!.add(record.userId);
|
|
98
|
+
|
|
99
|
+
this.codes.delete(code);
|
|
100
|
+
|
|
101
|
+
this.log.info(`Approved pairing for ${record.channel}:${record.userId}`);
|
|
102
|
+
|
|
103
|
+
eventBus.emit("pairing:approved", {
|
|
104
|
+
channel: record.channel,
|
|
105
|
+
userId: record.userId,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return { success: true };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
reject(code: string, reason: string): boolean {
|
|
112
|
+
const record = this.codes.get(code);
|
|
113
|
+
if (!record) return false;
|
|
114
|
+
|
|
115
|
+
this.codes.delete(code);
|
|
116
|
+
|
|
117
|
+
this.log.info(`Rejected pairing for ${record.channel}:${record.userId}: ${reason}`);
|
|
118
|
+
|
|
119
|
+
eventBus.emit("pairing:rejected", {
|
|
120
|
+
channel: record.channel,
|
|
121
|
+
userId: record.userId,
|
|
122
|
+
reason,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
attempt(code: string): boolean {
|
|
129
|
+
const record = this.codes.get(code);
|
|
130
|
+
if (!record) return false;
|
|
131
|
+
|
|
132
|
+
record.attempts++;
|
|
133
|
+
|
|
134
|
+
if (record.attempts >= this.config.maxAttempts) {
|
|
135
|
+
this.codes.delete(code);
|
|
136
|
+
this.log.warn(`Code ${code} exhausted attempts`);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
isAllowed(channel: string, userId: string): boolean {
|
|
144
|
+
const channelAllowlist = this.allowlist.get(channel);
|
|
145
|
+
return channelAllowlist?.has(userId) ?? false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
removeFromAllowlist(channel: string, userId: string): boolean {
|
|
149
|
+
const channelAllowlist = this.allowlist.get(channel);
|
|
150
|
+
if (!channelAllowlist) return false;
|
|
151
|
+
|
|
152
|
+
const removed = channelAllowlist.delete(userId);
|
|
153
|
+
|
|
154
|
+
if (channelAllowlist.size === 0) {
|
|
155
|
+
this.allowlist.delete(channel);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (removed) {
|
|
159
|
+
this.log.info(`Removed ${userId} from allowlist for ${channel}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return removed;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
listAllowed(channel?: string): { channel: string; userId: string }[] {
|
|
166
|
+
const result: { channel: string; userId: string }[] = [];
|
|
167
|
+
|
|
168
|
+
if (channel) {
|
|
169
|
+
const channelAllowlist = this.allowlist.get(channel);
|
|
170
|
+
if (channelAllowlist) {
|
|
171
|
+
for (const userId of channelAllowlist) {
|
|
172
|
+
result.push({ channel, userId });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
for (const [ch, users] of this.allowlist) {
|
|
177
|
+
for (const userId of users) {
|
|
178
|
+
result.push({ channel: ch, userId });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
listPending(): PairingCode[] {
|
|
187
|
+
this.cleanup();
|
|
188
|
+
return Array.from(this.codes.values());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
getStats(): PairingStats {
|
|
192
|
+
const byChannel: Record<string, { pending: number; allowed: number }> = {};
|
|
193
|
+
|
|
194
|
+
for (const [channel, users] of this.allowlist) {
|
|
195
|
+
byChannel[channel] = {
|
|
196
|
+
pending: 0,
|
|
197
|
+
allowed: users.size,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const record of this.codes.values()) {
|
|
202
|
+
if (!byChannel[record.channel]) {
|
|
203
|
+
byChannel[record.channel] = { pending: 0, allowed: 0 };
|
|
204
|
+
}
|
|
205
|
+
byChannel[record.channel]!.pending++;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
pendingCodes: this.codes.size,
|
|
210
|
+
totalAllowlist: Array.from(this.allowlist.values()).reduce(
|
|
211
|
+
(sum, set) => sum + set.size,
|
|
212
|
+
0
|
|
213
|
+
),
|
|
214
|
+
byChannel,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
clear(): void {
|
|
219
|
+
this.codes.clear();
|
|
220
|
+
this.allowlist.clear();
|
|
221
|
+
this.log.info("All pairing data cleared");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private generateSecureCode(): string {
|
|
225
|
+
const bytes = crypto.randomBytes(Math.ceil(this.config.codeLength / 2));
|
|
226
|
+
return bytes.toString("hex").toUpperCase().slice(0, this.config.codeLength);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private cleanup(): void {
|
|
230
|
+
const now = Date.now();
|
|
231
|
+
for (const [code, record] of this.codes) {
|
|
232
|
+
if (now > record.expiresAt) {
|
|
233
|
+
this.codes.delete(code);
|
|
234
|
+
eventBus.emit("pairing:expired", {
|
|
235
|
+
code,
|
|
236
|
+
channel: record.channel,
|
|
237
|
+
userId: record.userId,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private startCleanup(): void {
|
|
244
|
+
setInterval(() => {
|
|
245
|
+
this.cleanup();
|
|
246
|
+
}, 60 * 1000);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export const pairingService = new PairingService();
|