@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.
- package/CHANGELOG.md +27 -1
- package/README.md +179 -57
- package/docs/API-AGENTS.md +9 -9
- package/docs/API-CONTEXT-COMPILER.md +13 -13
- package/docs/API-DAG-SCHEDULER.md +8 -8
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +150 -93
- package/docs/API-WORKERS-EVENTS.md +206 -59
- package/docs/INDEX.md +99 -50
- package/docs/README.md +117 -24
- package/docs/TEMPLATE-HIVE-APP.md +360 -0
- package/package.json +13 -6
- package/packages/cli/bin/hive +2 -0
- package/packages/cli/src/commands/add-skill.ts +42 -0
- package/packages/cli/src/commands/add-tool.ts +45 -0
- package/packages/cli/src/commands/add-worker.ts +49 -0
- package/packages/cli/src/commands/create-app-utils.ts +32 -0
- package/packages/cli/src/commands/create-app.test.ts +151 -0
- package/packages/cli/src/commands/create-app.ts +35 -0
- package/packages/cli/src/index.ts +21 -5
- package/packages/cli/templates/hive-app/.env.example +17 -0
- package/packages/cli/templates/hive-app/docker-compose.yml +20 -0
- package/packages/cli/templates/hive-app/hive.config.ts +19 -0
- package/packages/cli/templates/hive-app/package.json +16 -0
- package/packages/cli/templates/hive-app/src/agents/coordinator.ts +9 -0
- package/packages/cli/templates/hive-app/src/main.ts +56 -0
- package/packages/core/src/auth/auth.ts +108 -0
- package/packages/core/src/auth/index.ts +1 -0
- package/packages/core/src/canvas/canvas.test.ts +32 -0
- package/packages/core/src/canvas/emitter.ts +1 -1
- package/packages/core/src/canvas/index.ts +3 -6
- package/packages/core/src/channels/base.ts +154 -0
- package/packages/core/src/channels/channels.test.ts +18 -0
- package/packages/core/src/channels/discord.ts +273 -0
- package/packages/core/src/channels/index.ts +7 -0
- package/packages/core/src/channels/manager.ts +450 -0
- package/packages/core/src/channels/slack.ts +323 -0
- package/packages/core/src/channels/telegram.ts +612 -0
- package/packages/core/src/channels/webchat.ts +139 -0
- package/packages/core/src/channels/whatsapp.ts +548 -0
- package/packages/core/src/events/agent-bus.ts +460 -0
- package/packages/core/src/events/event-bus.ts +169 -0
- package/packages/core/src/gateway/channel-notify.ts +32 -7
- package/packages/core/src/gateway/gateway.test.ts +38 -0
- package/packages/core/src/gateway/index.ts +2 -1
- package/packages/core/src/gateway/server.ts +139 -0
- package/packages/core/src/heartbeat/index.ts +157 -0
- package/packages/core/src/index.ts +44 -0
- package/packages/core/src/multimodal/index.ts +2 -2
- package/packages/core/src/multimodal/vision-service.ts +283 -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/scheduler/CronScheduler.ts +699 -0
- package/packages/core/src/scheduler/dag/AgentExecutor.ts +53 -0
- package/packages/core/src/scheduler/dag/DAGScheduler.ts +250 -0
- package/packages/core/src/scheduler/dag/EventBridge.ts +122 -0
- package/packages/core/src/scheduler/dag/TaskGraph.ts +192 -0
- package/packages/core/src/scheduler/dag/TaskNode.ts +97 -0
- package/packages/core/src/scheduler/dag/TaskResult.ts +22 -0
- package/packages/core/src/scheduler/dag/errors.ts +37 -0
- package/packages/core/src/scheduler/dag/index.ts +26 -0
- package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +97 -0
- package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +21 -0
- package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +46 -0
- package/packages/core/src/scheduler/index.ts +22 -0
- package/packages/core/src/scheduler/integration.ts +237 -0
- package/packages/core/src/scheduler/scheduler.test.ts +19 -0
- package/packages/core/src/scheduler/types.ts +164 -0
- package/packages/core/src/security/google-chat.ts +269 -0
- package/packages/core/src/security/index.ts +192 -4
- package/packages/core/src/security/rate-limit.ts +270 -0
- package/packages/core/src/security/signal.ts +321 -0
- package/packages/core/src/storage/crypto.ts +198 -66
- package/packages/core/src/storage/storage.test.ts +37 -0
- package/packages/core/src/swarm/swarm.test.ts +24 -0
- package/packages/core/src/tool-runtime/index.ts +522 -0
- package/packages/core/src/tool-runtime/tool-runtime.test.ts +91 -0
- package/packages/core/src/tool-runtime/tool-worker.ts +125 -0
- package/packages/core/src/voice/index.ts +5 -18
- package/packages/core/src/workers/WorkerPool.ts +167 -0
- package/packages/core/src/workers/agent.worker.ts +68 -0
- package/packages/core/src/workers/createWorker.ts +144 -0
- package/packages/core/src/workers/index.ts +5 -0
- package/packages/core/src/workers/workers.test.ts +48 -0
- package/test/setup-db.ts +2 -2
- package/tsconfig.json +2 -1
- package/.github/CODEOWNERS +0 -9
- package/.github/workflows/publish.yml +0 -89
- package/.github/workflows/version-bump.yml +0 -102
- package/bun.lock +0 -543
- package/bunfig.toml +0 -7
- package/packages/core/src/agent/providers.ts +0 -1
- 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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export
|
|
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
|
+
}
|