@johpaz/hive-sdk 0.0.15 → 0.0.16

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.
Files changed (94) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +179 -57
  3. package/docs/API-AGENTS.md +9 -9
  4. package/docs/API-CONTEXT-COMPILER.md +13 -13
  5. package/docs/API-DAG-SCHEDULER.md +8 -8
  6. package/docs/API-TOOLS-SKILLS-CHANNELS.md +150 -93
  7. package/docs/API-WORKERS-EVENTS.md +206 -59
  8. package/docs/INDEX.md +99 -50
  9. package/docs/README.md +117 -24
  10. package/docs/TEMPLATE-HIVE-APP.md +360 -0
  11. package/package.json +13 -6
  12. package/packages/cli/bin/hive +2 -0
  13. package/packages/cli/src/commands/add-skill.ts +42 -0
  14. package/packages/cli/src/commands/add-tool.ts +45 -0
  15. package/packages/cli/src/commands/add-worker.ts +49 -0
  16. package/packages/cli/src/commands/create-app-utils.ts +32 -0
  17. package/packages/cli/src/commands/create-app.test.ts +151 -0
  18. package/packages/cli/src/commands/create-app.ts +35 -0
  19. package/packages/cli/src/index.ts +21 -5
  20. package/packages/cli/templates/hive-app/.env.example +17 -0
  21. package/packages/cli/templates/hive-app/docker-compose.yml +20 -0
  22. package/packages/cli/templates/hive-app/hive.config.ts +19 -0
  23. package/packages/cli/templates/hive-app/package.json +16 -0
  24. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +9 -0
  25. package/packages/cli/templates/hive-app/src/main.ts +56 -0
  26. package/packages/core/src/auth/auth.ts +108 -0
  27. package/packages/core/src/auth/index.ts +1 -0
  28. package/packages/core/src/canvas/canvas.test.ts +32 -0
  29. package/packages/core/src/canvas/emitter.ts +1 -1
  30. package/packages/core/src/canvas/index.ts +3 -6
  31. package/packages/core/src/channels/base.ts +154 -0
  32. package/packages/core/src/channels/channels.test.ts +18 -0
  33. package/packages/core/src/channels/discord.ts +273 -0
  34. package/packages/core/src/channels/index.ts +7 -0
  35. package/packages/core/src/channels/manager.ts +450 -0
  36. package/packages/core/src/channels/slack.ts +323 -0
  37. package/packages/core/src/channels/telegram.ts +612 -0
  38. package/packages/core/src/channels/webchat.ts +139 -0
  39. package/packages/core/src/channels/whatsapp.ts +548 -0
  40. package/packages/core/src/events/agent-bus.ts +460 -0
  41. package/packages/core/src/events/event-bus.ts +169 -0
  42. package/packages/core/src/gateway/channel-notify.ts +32 -7
  43. package/packages/core/src/gateway/gateway.test.ts +38 -0
  44. package/packages/core/src/gateway/index.ts +2 -1
  45. package/packages/core/src/gateway/server.ts +139 -0
  46. package/packages/core/src/heartbeat/index.ts +157 -0
  47. package/packages/core/src/index.ts +44 -0
  48. package/packages/core/src/multimodal/index.ts +2 -2
  49. package/packages/core/src/multimodal/vision-service.ts +283 -0
  50. package/packages/core/src/plugins/api.ts +128 -0
  51. package/packages/core/src/plugins/index.ts +2 -0
  52. package/packages/core/src/plugins/loader.ts +365 -0
  53. package/packages/core/src/resilience/circuit-breaker.ts +225 -0
  54. package/packages/core/src/scheduler/CronScheduler.ts +699 -0
  55. package/packages/core/src/scheduler/dag/AgentExecutor.ts +53 -0
  56. package/packages/core/src/scheduler/dag/DAGScheduler.ts +250 -0
  57. package/packages/core/src/scheduler/dag/EventBridge.ts +122 -0
  58. package/packages/core/src/scheduler/dag/TaskGraph.ts +192 -0
  59. package/packages/core/src/scheduler/dag/TaskNode.ts +97 -0
  60. package/packages/core/src/scheduler/dag/TaskResult.ts +22 -0
  61. package/packages/core/src/scheduler/dag/errors.ts +37 -0
  62. package/packages/core/src/scheduler/dag/index.ts +26 -0
  63. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +97 -0
  64. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +21 -0
  65. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +46 -0
  66. package/packages/core/src/scheduler/index.ts +22 -0
  67. package/packages/core/src/scheduler/integration.ts +237 -0
  68. package/packages/core/src/scheduler/scheduler.test.ts +19 -0
  69. package/packages/core/src/scheduler/types.ts +164 -0
  70. package/packages/core/src/security/google-chat.ts +269 -0
  71. package/packages/core/src/security/index.ts +192 -4
  72. package/packages/core/src/security/rate-limit.ts +270 -0
  73. package/packages/core/src/security/signal.ts +321 -0
  74. package/packages/core/src/storage/crypto.ts +198 -66
  75. package/packages/core/src/storage/storage.test.ts +37 -0
  76. package/packages/core/src/swarm/swarm.test.ts +24 -0
  77. package/packages/core/src/tool-runtime/index.ts +522 -0
  78. package/packages/core/src/tool-runtime/tool-runtime.test.ts +91 -0
  79. package/packages/core/src/tool-runtime/tool-worker.ts +125 -0
  80. package/packages/core/src/voice/index.ts +5 -18
  81. package/packages/core/src/workers/WorkerPool.ts +167 -0
  82. package/packages/core/src/workers/agent.worker.ts +68 -0
  83. package/packages/core/src/workers/createWorker.ts +144 -0
  84. package/packages/core/src/workers/index.ts +5 -0
  85. package/packages/core/src/workers/workers.test.ts +48 -0
  86. package/test/setup-db.ts +2 -2
  87. package/tsconfig.json +2 -1
  88. package/.github/CODEOWNERS +0 -9
  89. package/.github/workflows/publish.yml +0 -89
  90. package/.github/workflows/version-bump.yml +0 -102
  91. package/bun.lock +0 -543
  92. package/bunfig.toml +0 -7
  93. package/packages/core/src/agent/providers.ts +0 -1
  94. package/packages/core/src/gateway/channel-notify.test.ts +0 -14
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Hive Scheduler - Type Definitions
3
+ *
4
+ * Type interfaces for the Croner-based scheduling system.
5
+ * All names use "CronJob" terminology (formerly ScheduledTask).
6
+ */
7
+
8
+ import type { Database } from "bun:sqlite";
9
+ import type { Cron } from "croner";
10
+
11
+ /**
12
+ * Task type: recurring uses cron expression, one_shot uses fire_at
13
+ */
14
+ export type TaskType = "recurring" | "one_shot";
15
+
16
+ /**
17
+ * Task status
18
+ */
19
+ export type TaskStatus = "active" | "paused" | "completed" | "failed" | "cancelled";
20
+
21
+ /**
22
+ * Task run status
23
+ */
24
+ export type TaskRunStatus = "running" | "success" | "failed" | "timeout";
25
+
26
+ /**
27
+ * CronJob as stored in SQLite (cron_jobs table)
28
+ */
29
+ export interface CronJob {
30
+ id: string;
31
+ name: string;
32
+ task: string;
33
+ task_type: TaskType;
34
+ cron_expression: string | null;
35
+ fire_at: string | null;
36
+ timezone: string;
37
+ start_at: string | null;
38
+ stop_at: string | null;
39
+ dom_and_dow: number;
40
+ max_runs: number | null;
41
+ protect: number;
42
+ interval_sec: number | null;
43
+ agent_id: string | null;
44
+ channel: string;
45
+ payload: string;
46
+ tool_name: string | null;
47
+ status: TaskStatus;
48
+ run_count: number;
49
+ error_count: number;
50
+ last_error: string | null;
51
+ created_at: string;
52
+ updated_at: string;
53
+ last_run_at: string | null;
54
+ next_run_at: string | null;
55
+ completed_at: string | null;
56
+ }
57
+
58
+ /**
59
+ * Task run history record
60
+ */
61
+ export interface TaskRun {
62
+ id: string;
63
+ task_id: string;
64
+ status: TaskRunStatus;
65
+ started_at: string;
66
+ finished_at: string | null;
67
+ duration_ms: number | null;
68
+ error_message: string | null;
69
+ payload_snapshot: string | null;
70
+ agent_response: string | null;
71
+ }
72
+
73
+ /**
74
+ * Input for creating a new cron job
75
+ */
76
+ export interface CreateCronJobInput {
77
+ name: string;
78
+ task: string;
79
+ task_type: TaskType;
80
+ cron_expression?: string;
81
+ fire_at?: string;
82
+ timezone: string;
83
+ start_at?: string;
84
+ stop_at?: string;
85
+ dom_and_dow?: boolean;
86
+ agent_id?: string | null;
87
+ channel?: string;
88
+ payload?: Record<string, unknown>;
89
+ tool_name?: string | null;
90
+ max_runs?: number | null;
91
+ protect?: boolean;
92
+ interval_sec?: number | null;
93
+ }
94
+
95
+ /**
96
+ * Input for updating an existing cron job
97
+ */
98
+ export interface UpdateCronJobInput {
99
+ name?: string;
100
+ task?: string;
101
+ task_type?: TaskType;
102
+ cron_expression?: string | null;
103
+ fire_at?: string | null;
104
+ timezone?: string;
105
+ start_at?: string | null;
106
+ stop_at?: string | null;
107
+ dom_and_dow?: boolean;
108
+ agent_id?: string | null;
109
+ channel?: string;
110
+ payload?: Record<string, unknown>;
111
+ tool_name?: string | null;
112
+ max_runs?: number | null;
113
+ protect?: boolean;
114
+ interval_sec?: number | null;
115
+ status?: TaskStatus;
116
+ }
117
+
118
+ /**
119
+ * Scheduler status for a cron job
120
+ */
121
+ export interface CronJobStatus {
122
+ id: string;
123
+ name: string;
124
+ nextRun: Date | null;
125
+ isBusy: boolean;
126
+ status: TaskStatus;
127
+ }
128
+
129
+ /**
130
+ * Handler function type for executing cron jobs
131
+ */
132
+ export type CronJobExecutionHandler = (job: CronJob) => Promise<CronJobExecutionResult>;
133
+
134
+ /**
135
+ * Result of cron job execution
136
+ */
137
+ export interface CronJobExecutionResult {
138
+ success: boolean;
139
+ response?: string;
140
+ error?: string;
141
+ }
142
+
143
+ /**
144
+ * Internal job wrapper holding Croner instance and metadata
145
+ */
146
+ export interface CronJobEntry {
147
+ job: CronJob;
148
+ cron: Cron;
149
+ }
150
+
151
+ /**
152
+ * Options for Croner job creation
153
+ */
154
+ export interface CronerOptions {
155
+ timezone: string;
156
+ protect: boolean;
157
+ catch: boolean | ((error: Error) => void);
158
+ name: string;
159
+ maxRuns?: number;
160
+ interval?: number;
161
+ startAt?: string;
162
+ stopAt?: string;
163
+ domAndDow?: boolean;
164
+ }
@@ -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
+ }
@@ -1,4 +1,192 @@
1
- export type { PairingCode, PairingConfig, PairingStats } from "./Pairing.ts";
2
- export { PairingService } from "./Pairing.ts";
3
- export type { TokenBucketConfig, TokenBucket, RateLimitResult, TokenBucketStats } from "./RateLimit.ts";
4
- export { TokenBucketRateLimiter } from "./RateLimit.ts";
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
+ }