@johpaz/hive-sdk 0.0.14 → 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 (257) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +280 -0
  3. package/docs/API-AGENTS.md +316 -0
  4. package/docs/API-CONTEXT-COMPILER.md +252 -0
  5. package/docs/API-DAG-SCHEDULER.md +273 -0
  6. package/docs/API-TOOLS-SKILLS-CHANNELS.md +350 -0
  7. package/docs/API-WORKERS-EVENTS.md +299 -0
  8. package/docs/INDEX.md +190 -0
  9. package/docs/README.md +161 -0
  10. package/docs/TEMPLATE-HIVE-APP.md +360 -0
  11. package/package.json +60 -104
  12. package/packages/cli/bin/hive +2 -0
  13. package/packages/cli/package.json +17 -0
  14. package/packages/cli/src/commands/add-skill.ts +42 -0
  15. package/packages/cli/src/commands/add-tool.ts +45 -0
  16. package/packages/cli/src/commands/add-worker.ts +49 -0
  17. package/packages/cli/src/commands/create-app-utils.ts +32 -0
  18. package/packages/cli/src/commands/create-app.test.ts +151 -0
  19. package/packages/cli/src/commands/create-app.ts +35 -0
  20. package/packages/cli/src/commands/init.ts +56 -0
  21. package/packages/cli/src/commands/run.ts +45 -0
  22. package/packages/cli/src/commands/test.ts +42 -0
  23. package/packages/cli/src/commands/trace.ts +55 -0
  24. package/packages/cli/src/index.ts +59 -0
  25. package/packages/cli/templates/hive-app/.env.example +17 -0
  26. package/packages/cli/templates/hive-app/docker-compose.yml +20 -0
  27. package/packages/cli/templates/hive-app/hive.config.ts +19 -0
  28. package/packages/cli/templates/hive-app/package.json +16 -0
  29. package/packages/cli/templates/hive-app/src/agents/coordinator.ts +9 -0
  30. package/packages/cli/templates/hive-app/src/main.ts +56 -0
  31. package/packages/core/package.json +58 -0
  32. package/packages/core/src/ace/Curator.ts +158 -0
  33. package/packages/core/src/ace/Reflector.ts +200 -0
  34. package/packages/core/src/ace/Tracer.ts +100 -0
  35. package/packages/core/src/ace/index.ts +4 -0
  36. package/packages/core/src/agent/AgentRunner.ts +699 -0
  37. package/packages/core/src/agent/Compaction.ts +221 -0
  38. package/packages/core/src/agent/ContextCompiler.ts +567 -0
  39. package/packages/core/src/agent/ContextGuard.ts +91 -0
  40. package/packages/core/src/agent/ConversationStore.ts +244 -0
  41. package/packages/core/src/agent/Hooks.ts +166 -0
  42. package/packages/core/src/agent/NativeTools.ts +31 -0
  43. package/packages/core/src/agent/PromptBuilder.ts +169 -0
  44. package/packages/core/src/agent/Service.ts +267 -0
  45. package/packages/core/src/agent/StuckLoop.ts +133 -0
  46. package/packages/core/src/agent/index.ts +12 -0
  47. package/packages/core/src/agent/providers/LLMClient.ts +149 -0
  48. package/packages/core/src/agent/providers/anthropic.ts +212 -0
  49. package/packages/core/src/agent/providers/gemini.ts +215 -0
  50. package/packages/core/src/agent/providers/index.ts +199 -0
  51. package/packages/core/src/agent/providers/interface.ts +195 -0
  52. package/packages/core/src/agent/providers/ollama.ts +175 -0
  53. package/packages/core/src/agent/providers/openai-compat.ts +231 -0
  54. package/packages/core/src/agent/selectors/PlaybookSelector.ts +147 -0
  55. package/packages/core/src/agent/selectors/SkillSelector.ts +478 -0
  56. package/packages/core/src/agent/selectors/ToolSelector.ts +577 -0
  57. package/packages/core/src/agent/selectors/index.ts +6 -0
  58. package/packages/core/src/api/createAgent.test.ts +48 -0
  59. package/packages/core/src/api/createAgent.ts +122 -0
  60. package/packages/core/src/api/index.ts +2 -0
  61. package/packages/core/src/auth/auth.ts +108 -0
  62. package/packages/core/src/auth/index.ts +1 -0
  63. package/packages/core/src/canvas/CanvasManager.ts +390 -0
  64. package/packages/core/src/canvas/a2ui-tools.ts +255 -0
  65. package/packages/core/src/canvas/canvas-tools.ts +448 -0
  66. package/packages/core/src/canvas/canvas.test.ts +32 -0
  67. package/packages/core/src/canvas/emitter.ts +149 -0
  68. package/packages/core/src/canvas/index.ts +3 -0
  69. package/packages/core/src/channels/base.ts +154 -0
  70. package/packages/core/src/channels/channels.test.ts +18 -0
  71. package/packages/core/src/channels/discord.ts +273 -0
  72. package/packages/core/src/channels/index.ts +7 -0
  73. package/packages/core/src/channels/manager.ts +450 -0
  74. package/packages/core/src/channels/slack.ts +323 -0
  75. package/packages/core/src/channels/telegram.ts +612 -0
  76. package/packages/core/src/channels/webchat.ts +139 -0
  77. package/packages/core/src/channels/whatsapp.ts +548 -0
  78. package/packages/core/src/config/index.ts +2 -0
  79. package/packages/core/src/config/loader.ts +554 -0
  80. package/packages/core/src/ethics/EthicsGuard.test.ts +54 -0
  81. package/packages/core/src/ethics/EthicsGuard.ts +66 -0
  82. package/packages/core/src/ethics/index.ts +2 -0
  83. package/packages/core/src/events/agent-bus.ts +460 -0
  84. package/packages/core/src/events/event-bus.ts +169 -0
  85. package/packages/core/src/gateway/channel-notify.ts +37 -0
  86. package/packages/core/src/gateway/gateway.test.ts +38 -0
  87. package/packages/core/src/gateway/index.ts +2 -0
  88. package/packages/core/src/gateway/server.ts +139 -0
  89. package/packages/core/src/heartbeat/index.ts +157 -0
  90. package/packages/core/src/index.ts +81 -0
  91. package/packages/core/src/mcp/MCPClient.ts +439 -0
  92. package/packages/core/src/mcp/MCPToolAdapter.ts +176 -0
  93. package/packages/core/src/mcp/config.ts +13 -0
  94. package/packages/core/src/mcp/hot-reload.ts +147 -0
  95. package/packages/core/src/mcp/index.ts +11 -0
  96. package/packages/core/src/mcp/logger.ts +42 -0
  97. package/packages/core/src/mcp/singleton.ts +21 -0
  98. package/packages/core/src/mcp/transports/index.ts +67 -0
  99. package/packages/core/src/mcp/transports/sse.ts +241 -0
  100. package/packages/core/src/mcp/transports/websocket.ts +159 -0
  101. package/packages/core/src/memory/Scratchpad.test.ts +47 -0
  102. package/packages/core/src/memory/Scratchpad.ts +37 -0
  103. package/packages/core/src/memory/Storage.ts +6 -0
  104. package/packages/core/src/memory/index.ts +2 -0
  105. package/packages/core/src/multimodal/VisionService.ts +293 -0
  106. package/packages/core/src/multimodal/index.ts +2 -0
  107. package/packages/core/src/multimodal/types.ts +28 -0
  108. package/packages/core/src/multimodal/vision-service.ts +283 -0
  109. package/packages/core/src/plugins/api.ts +128 -0
  110. package/packages/core/src/plugins/index.ts +2 -0
  111. package/packages/core/src/plugins/loader.ts +365 -0
  112. package/packages/core/src/resilience/circuit-breaker.ts +225 -0
  113. package/packages/core/src/scheduler/CronScheduler.ts +699 -0
  114. package/packages/core/src/scheduler/dag/AgentExecutor.ts +53 -0
  115. package/packages/core/src/scheduler/dag/DAGScheduler.ts +250 -0
  116. package/packages/core/src/scheduler/dag/EventBridge.ts +122 -0
  117. package/packages/core/src/scheduler/dag/TaskGraph.ts +192 -0
  118. package/packages/core/src/scheduler/dag/TaskNode.ts +97 -0
  119. package/packages/core/src/scheduler/dag/TaskResult.ts +22 -0
  120. package/packages/core/src/scheduler/dag/errors.ts +37 -0
  121. package/packages/core/src/scheduler/dag/index.ts +26 -0
  122. package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +97 -0
  123. package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +21 -0
  124. package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +46 -0
  125. package/packages/core/src/scheduler/index.ts +22 -0
  126. package/packages/core/src/scheduler/integration.ts +237 -0
  127. package/packages/core/src/scheduler/scheduler.test.ts +19 -0
  128. package/packages/core/src/scheduler/types.ts +164 -0
  129. package/packages/core/src/security/Pairing.ts +250 -0
  130. package/packages/core/src/security/RateLimit.ts +270 -0
  131. package/packages/core/src/security/google-chat.ts +269 -0
  132. package/packages/core/src/security/index.ts +192 -0
  133. package/packages/core/src/security/rate-limit.ts +270 -0
  134. package/packages/core/src/security/signal.ts +321 -0
  135. package/packages/core/src/skills/SkillLoader.ts +388 -0
  136. package/packages/core/src/skills/bundled-data.generated.ts +3332 -0
  137. package/packages/core/src/skills/defineSkill.ts +18 -0
  138. package/packages/core/src/skills/index.ts +4 -0
  139. package/packages/core/src/state/index.ts +2 -0
  140. package/packages/core/src/state/store.ts +312 -0
  141. package/packages/core/src/storage/SQLiteStorage.ts +407 -0
  142. package/packages/core/src/storage/crypto.ts +233 -0
  143. package/packages/core/src/storage/index.ts +10 -0
  144. package/packages/core/src/storage/onboarding.ts +1603 -0
  145. package/packages/core/src/storage/schema.ts +689 -0
  146. package/packages/core/src/storage/seed.ts +740 -0
  147. package/packages/core/src/storage/storage.test.ts +37 -0
  148. package/packages/core/src/storage/usage.ts +374 -0
  149. package/packages/core/src/swarm/AgentBus.ts +460 -0
  150. package/packages/core/src/swarm/AgentExecutor.ts +53 -0
  151. package/packages/core/src/swarm/Coordinator.ts +251 -0
  152. package/packages/core/src/swarm/EventBridge.ts +122 -0
  153. package/packages/core/src/swarm/EventBus.ts +169 -0
  154. package/packages/core/src/swarm/TaskGraph.ts +192 -0
  155. package/packages/core/src/swarm/TaskNode.ts +97 -0
  156. package/packages/core/src/swarm/TaskResult.ts +22 -0
  157. package/packages/core/src/swarm/WorkerPool.ts +236 -0
  158. package/packages/core/src/swarm/errors.ts +37 -0
  159. package/packages/core/src/swarm/index.ts +30 -0
  160. package/packages/core/src/swarm/presets/HiveLearnPreset.ts +99 -0
  161. package/packages/core/src/swarm/presets/ResearchPreset.ts +97 -0
  162. package/packages/core/src/swarm/presets/index.ts +4 -0
  163. package/packages/core/src/swarm/strategies/ParallelStrategy.ts +21 -0
  164. package/packages/core/src/swarm/strategies/PriorityStrategy.ts +46 -0
  165. package/packages/core/src/swarm/strategies/index.ts +3 -0
  166. package/packages/core/src/swarm/swarm.test.ts +24 -0
  167. package/packages/core/src/swarm/types.ts +164 -0
  168. package/packages/core/src/tool-runtime/index.ts +522 -0
  169. package/packages/core/src/tool-runtime/tool-runtime.test.ts +91 -0
  170. package/packages/core/src/tool-runtime/tool-worker.ts +125 -0
  171. package/packages/core/src/tools/ToolExecutor.ts +58 -0
  172. package/packages/core/src/tools/ToolRegistry.test.ts +98 -0
  173. package/packages/core/src/tools/ToolRegistry.ts +61 -0
  174. package/packages/core/src/tools/agents/get-available-models.ts +118 -0
  175. package/packages/core/src/tools/agents/index.ts +715 -0
  176. package/packages/core/src/tools/bridge-events.ts +26 -0
  177. package/packages/core/src/tools/canvas/index.ts +375 -0
  178. package/packages/core/src/tools/cli/index.ts +142 -0
  179. package/packages/core/src/tools/codebridge/index.ts +342 -0
  180. package/packages/core/src/tools/core/index.ts +476 -0
  181. package/packages/core/src/tools/cron/index.ts +626 -0
  182. package/packages/core/src/tools/filesystem/fs-delete.ts +78 -0
  183. package/packages/core/src/tools/filesystem/fs-edit.ts +106 -0
  184. package/packages/core/src/tools/filesystem/fs-exists.ts +63 -0
  185. package/packages/core/src/tools/filesystem/fs-glob.ts +108 -0
  186. package/packages/core/src/tools/filesystem/fs-list.ts +129 -0
  187. package/packages/core/src/tools/filesystem/fs-read.ts +72 -0
  188. package/packages/core/src/tools/filesystem/fs-write.ts +67 -0
  189. package/packages/core/src/tools/filesystem/index.ts +34 -0
  190. package/packages/core/src/tools/filesystem/workspace-guard.ts +62 -0
  191. package/packages/core/src/tools/index.ts +231 -0
  192. package/packages/core/src/tools/meeting/index.ts +363 -0
  193. package/packages/core/src/tools/office/index.ts +47 -0
  194. package/packages/core/src/tools/office/office-escribir-docx.ts +192 -0
  195. package/packages/core/src/tools/office/office-escribir-pdf.ts +172 -0
  196. package/packages/core/src/tools/office/office-escribir-pptx.ts +174 -0
  197. package/packages/core/src/tools/office/office-escribir-xlsx.ts +116 -0
  198. package/packages/core/src/tools/office/office-leer-docx.ts +93 -0
  199. package/packages/core/src/tools/office/office-leer-pdf.ts +114 -0
  200. package/packages/core/src/tools/office/office-leer-pptx.ts +136 -0
  201. package/packages/core/src/tools/office/office-leer-xlsx.ts +124 -0
  202. package/packages/core/src/tools/projects/index.ts +37 -0
  203. package/packages/core/src/tools/projects/project-create.ts +94 -0
  204. package/packages/core/src/tools/projects/project-done.ts +66 -0
  205. package/packages/core/src/tools/projects/project-fail.ts +66 -0
  206. package/packages/core/src/tools/projects/project-list.ts +96 -0
  207. package/packages/core/src/tools/projects/project-update.ts +72 -0
  208. package/packages/core/src/tools/projects/task-create.ts +68 -0
  209. package/packages/core/src/tools/projects/task-evaluate.ts +93 -0
  210. package/packages/core/src/tools/projects/task-update.ts +93 -0
  211. package/packages/core/src/tools/types.ts +39 -0
  212. package/packages/core/src/tools/voice/index.ts +104 -0
  213. package/packages/core/src/tools/web/browser-click.ts +78 -0
  214. package/packages/core/src/tools/web/browser-extract.ts +139 -0
  215. package/packages/core/src/tools/web/browser-navigate.ts +106 -0
  216. package/packages/core/src/tools/web/browser-screenshot.ts +87 -0
  217. package/packages/core/src/tools/web/browser-script.ts +88 -0
  218. package/packages/core/src/tools/web/browser-service.ts +554 -0
  219. package/packages/core/src/tools/web/browser-type.ts +101 -0
  220. package/packages/core/src/tools/web/browser-wait.ts +136 -0
  221. package/packages/core/src/tools/web/index.ts +41 -0
  222. package/packages/core/src/tools/web/web-fetch.ts +78 -0
  223. package/packages/core/src/tools/web/web-search.ts +123 -0
  224. package/packages/core/src/utils/benchmark.ts +80 -0
  225. package/packages/core/src/utils/crypto.ts +73 -0
  226. package/packages/core/src/utils/date.ts +42 -0
  227. package/packages/core/src/utils/index.ts +10 -0
  228. package/packages/core/src/utils/logger.ts +389 -0
  229. package/packages/core/src/utils/retry.ts +70 -0
  230. package/packages/core/src/utils/toon.ts +253 -0
  231. package/packages/core/src/voice/index.ts +643 -0
  232. package/packages/core/src/workers/WorkerPool.ts +167 -0
  233. package/packages/core/src/workers/agent.worker.ts +68 -0
  234. package/packages/core/src/workers/createWorker.ts +144 -0
  235. package/packages/core/src/workers/index.ts +5 -0
  236. package/packages/core/src/workers/workers.test.ts +48 -0
  237. package/test/setup-db.ts +216 -0
  238. package/tsconfig.json +40 -0
  239. package/src/agents.ts +0 -1
  240. package/src/canvas.ts +0 -1
  241. package/src/channels.ts +0 -1
  242. package/src/config.ts +0 -1
  243. package/src/events.ts +0 -1
  244. package/src/gateway.ts +0 -1
  245. package/src/index.ts +0 -304
  246. package/src/mcp.ts +0 -1
  247. package/src/multimodal.ts +0 -1
  248. package/src/scheduler.ts +0 -1
  249. package/src/security.ts +0 -1
  250. package/src/skills.ts +0 -1
  251. package/src/state.ts +0 -1
  252. package/src/storage.ts +0 -1
  253. package/src/tools.ts +0 -1
  254. package/src/tts.ts +0 -1
  255. package/src/types.ts +0 -82
  256. package/src/utils.ts +0 -1
  257. package/src/voice.ts +0 -1
@@ -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,250 @@
1
+ import crypto from "crypto";
2
+ import { eventBus } from "../swarm/EventBus.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();
@@ -0,0 +1,270 @@
1
+ import { logger } from "../utils/logger.ts";
2
+
3
+ export interface TokenBucketConfig {
4
+ maxTokens: number;
5
+ refillRate: number;
6
+ refillIntervalMs?: number;
7
+ }
8
+
9
+ export interface TokenBucket {
10
+ tokens: number;
11
+ lastUpdate: number;
12
+ }
13
+
14
+ export interface RateLimitResult {
15
+ allowed: boolean;
16
+ remaining: number;
17
+ retryAfterMs: number;
18
+ }
19
+
20
+ export interface TokenBucketStats {
21
+ totalBuckets: number;
22
+ activeBuckets: number;
23
+ totalTokensAvailable: number;
24
+ }
25
+
26
+ export class TokenBucketRateLimiter {
27
+ private buckets: Map<string, TokenBucket> = new Map();
28
+ private config: Required<TokenBucketConfig>;
29
+ private log = logger.child("token-bucket-limiter");
30
+
31
+ constructor(config: TokenBucketConfig) {
32
+ this.config = {
33
+ maxTokens: config.maxTokens,
34
+ refillRate: config.refillRate,
35
+ refillIntervalMs: config.refillIntervalMs ?? 1000,
36
+ };
37
+
38
+ this.startCleanup();
39
+ }
40
+
41
+ canProceed(key: string): RateLimitResult {
42
+ const now = Date.now();
43
+ let bucket = this.buckets.get(key);
44
+
45
+ if (!bucket) {
46
+ bucket = {
47
+ tokens: this.config.maxTokens,
48
+ lastUpdate: now,
49
+ };
50
+ this.buckets.set(key, bucket);
51
+ }
52
+
53
+ const elapsed = now - bucket.lastUpdate;
54
+ const tokensToAdd = (elapsed / this.config.refillIntervalMs) * this.config.refillRate;
55
+ bucket.tokens = Math.min(this.config.maxTokens, bucket.tokens + tokensToAdd);
56
+ bucket.lastUpdate = now;
57
+
58
+ if (bucket.tokens >= 1) {
59
+ bucket.tokens--;
60
+ return {
61
+ allowed: true,
62
+ remaining: Math.floor(bucket.tokens),
63
+ retryAfterMs: 0,
64
+ };
65
+ }
66
+
67
+ const retryAfterMs = Math.ceil(
68
+ ((1 - bucket.tokens) / this.config.refillRate) * this.config.refillIntervalMs
69
+ );
70
+
71
+ this.log.debug(`Rate limit hit for ${key}, retry after ${retryAfterMs}ms`);
72
+
73
+ return {
74
+ allowed: false,
75
+ remaining: 0,
76
+ retryAfterMs,
77
+ };
78
+ }
79
+
80
+ consume(key: string, tokens: number = 1): RateLimitResult {
81
+ const now = Date.now();
82
+ let bucket = this.buckets.get(key);
83
+
84
+ if (!bucket) {
85
+ bucket = {
86
+ tokens: this.config.maxTokens,
87
+ lastUpdate: now,
88
+ };
89
+ this.buckets.set(key, bucket);
90
+ }
91
+
92
+ const elapsed = now - bucket.lastUpdate;
93
+ const tokensToAdd = (elapsed / this.config.refillIntervalMs) * this.config.refillRate;
94
+ bucket.tokens = Math.min(this.config.maxTokens, bucket.tokens + tokensToAdd);
95
+ bucket.lastUpdate = now;
96
+
97
+ if (bucket.tokens >= tokens) {
98
+ bucket.tokens -= tokens;
99
+ return {
100
+ allowed: true,
101
+ remaining: Math.floor(bucket.tokens),
102
+ retryAfterMs: 0,
103
+ };
104
+ }
105
+
106
+ const retryAfterMs = Math.ceil(
107
+ ((tokens - bucket.tokens) / this.config.refillRate) * this.config.refillIntervalMs
108
+ );
109
+
110
+ return {
111
+ allowed: false,
112
+ remaining: 0,
113
+ retryAfterMs,
114
+ };
115
+ }
116
+
117
+ peek(key: string): number {
118
+ const bucket = this.buckets.get(key);
119
+ if (!bucket) return this.config.maxTokens;
120
+
121
+ const now = Date.now();
122
+ const elapsed = now - bucket.lastUpdate;
123
+ const tokensToAdd = (elapsed / this.config.refillIntervalMs) * this.config.refillRate;
124
+
125
+ return Math.min(this.config.maxTokens, bucket.tokens + tokensToAdd);
126
+ }
127
+
128
+ refill(key: string, tokens?: number): void {
129
+ const bucket = this.buckets.get(key);
130
+ if (!bucket) return;
131
+
132
+ bucket.tokens = Math.min(
133
+ this.config.maxTokens,
134
+ bucket.tokens + (tokens ?? this.config.maxTokens)
135
+ );
136
+ }
137
+
138
+ reset(key: string): void {
139
+ this.buckets.delete(key);
140
+ }
141
+
142
+ resetAll(): void {
143
+ this.buckets.clear();
144
+ this.log.info("All rate limit buckets cleared");
145
+ }
146
+
147
+ getStats(): TokenBucketStats {
148
+ let totalTokens = 0;
149
+ let activeBuckets = 0;
150
+ const now = Date.now();
151
+
152
+ for (const bucket of this.buckets.values()) {
153
+ const elapsed = now - bucket.lastUpdate;
154
+ const tokens = Math.min(
155
+ this.config.maxTokens,
156
+ bucket.tokens + (elapsed / this.config.refillIntervalMs) * this.config.refillRate
157
+ );
158
+
159
+ if (tokens < this.config.maxTokens) {
160
+ activeBuckets++;
161
+ }
162
+ totalTokens += tokens;
163
+ }
164
+
165
+ return {
166
+ totalBuckets: this.buckets.size,
167
+ activeBuckets,
168
+ totalTokensAvailable: Math.floor(totalTokens),
169
+ };
170
+ }
171
+
172
+ private startCleanup(): void {
173
+ setInterval(() => {
174
+ const now = Date.now();
175
+ for (const [key, bucket] of this.buckets) {
176
+ const elapsed = now - bucket.lastUpdate;
177
+ const fullTokens =
178
+ bucket.tokens + (elapsed / this.config.refillIntervalMs) * this.config.refillRate;
179
+
180
+ if (fullTokens >= this.config.maxTokens) {
181
+ this.buckets.delete(key);
182
+ }
183
+ }
184
+ }, 5 * 60 * 1000);
185
+ }
186
+ }
187
+
188
+ export interface SlidingWindowConfig {
189
+ windowMs: number;
190
+ maxRequests: number;
191
+ }
192
+
193
+ export interface SlidingWindowEntry {
194
+ timestamps: number[];
195
+ }
196
+
197
+ export class SlidingWindowRateLimiter {
198
+ private windows: Map<string, SlidingWindowEntry> = new Map();
199
+ private config: SlidingWindowConfig;
200
+ private log = logger.child("sliding-window-limiter");
201
+
202
+ constructor(config: SlidingWindowConfig) {
203
+ this.config = config;
204
+ this.startCleanup();
205
+ }
206
+
207
+ check(key: string): RateLimitResult {
208
+ const now = Date.now();
209
+ const windowStart = now - this.config.windowMs;
210
+
211
+ let entry = this.windows.get(key);
212
+ if (!entry) {
213
+ entry = { timestamps: [] };
214
+ this.windows.set(key, entry);
215
+ }
216
+
217
+ entry.timestamps = entry.timestamps.filter((t) => t > windowStart);
218
+
219
+ if (entry.timestamps.length >= this.config.maxRequests) {
220
+ const oldestInWindow = entry.timestamps[0];
221
+ const retryAfterMs = oldestInWindow + this.config.windowMs - now;
222
+
223
+ this.log.debug(`Sliding window limit hit for ${key}`);
224
+
225
+ return {
226
+ allowed: false,
227
+ remaining: 0,
228
+ retryAfterMs: Math.max(0, retryAfterMs),
229
+ };
230
+ }
231
+
232
+ entry.timestamps.push(now);
233
+
234
+ return {
235
+ allowed: true,
236
+ remaining: this.config.maxRequests - entry.timestamps.length,
237
+ retryAfterMs: 0,
238
+ };
239
+ }
240
+
241
+ reset(key: string): void {
242
+ this.windows.delete(key);
243
+ }
244
+
245
+ resetAll(): void {
246
+ this.windows.clear();
247
+ }
248
+
249
+ private startCleanup(): void {
250
+ setInterval(() => {
251
+ const now = Date.now();
252
+ const windowStart = now - this.config.windowMs;
253
+
254
+ for (const [key, entry] of this.windows) {
255
+ entry.timestamps = entry.timestamps.filter((t) => t > windowStart);
256
+ if (entry.timestamps.length === 0) {
257
+ this.windows.delete(key);
258
+ }
259
+ }
260
+ }, 60 * 1000);
261
+ }
262
+ }
263
+
264
+ export function createTokenBucketLimiter(config: TokenBucketConfig): TokenBucketRateLimiter {
265
+ return new TokenBucketRateLimiter(config);
266
+ }
267
+
268
+ export function createSlidingWindowLimiter(config: SlidingWindowConfig): SlidingWindowRateLimiter {
269
+ return new SlidingWindowRateLimiter(config);
270
+ }