@johpaz/hive-core 1.0.7 → 1.0.10
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/package.json +10 -9
- package/src/agent/ethics.ts +70 -68
- package/src/agent/index.ts +48 -17
- package/src/agent/providers/index.ts +11 -5
- package/src/agent/soul.ts +19 -15
- package/src/agent/user.ts +19 -15
- package/src/agent/workspace.ts +6 -6
- package/src/agents/index.ts +4 -0
- package/src/agents/inter-agent-bus.test.ts +264 -0
- package/src/agents/inter-agent-bus.ts +279 -0
- package/src/agents/registry.test.ts +275 -0
- package/src/agents/registry.ts +273 -0
- package/src/agents/router.test.ts +229 -0
- package/src/agents/router.ts +251 -0
- package/src/agents/team-coordinator.test.ts +401 -0
- package/src/agents/team-coordinator.ts +480 -0
- package/src/canvas/canvas-manager.test.ts +159 -0
- package/src/canvas/canvas-manager.ts +219 -0
- package/src/canvas/canvas-tools.ts +189 -0
- package/src/canvas/index.ts +2 -0
- package/src/channels/whatsapp.ts +12 -12
- package/src/config/loader.ts +12 -9
- package/src/events/event-bus.test.ts +98 -0
- package/src/events/event-bus.ts +171 -0
- package/src/gateway/server.ts +131 -35
- package/src/index.ts +9 -1
- package/src/multi-agent/manager.ts +12 -12
- package/src/plugins/api.ts +129 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/loader.test.ts +285 -0
- package/src/plugins/loader.ts +363 -0
- package/src/resilience/circuit-breaker.test.ts +129 -0
- package/src/resilience/circuit-breaker.ts +223 -0
- package/src/security/google-chat.test.ts +219 -0
- package/src/security/google-chat.ts +269 -0
- package/src/security/index.ts +5 -0
- package/src/security/pairing.test.ts +302 -0
- package/src/security/pairing.ts +250 -0
- package/src/security/rate-limit.test.ts +239 -0
- package/src/security/rate-limit.ts +270 -0
- package/src/security/signal.test.ts +92 -0
- package/src/security/signal.ts +321 -0
- package/src/state/store.test.ts +190 -0
- package/src/state/store.ts +310 -0
- package/src/storage/sqlite.ts +3 -3
- package/src/tools/cron.ts +42 -2
- package/src/tools/dynamic-registry.test.ts +226 -0
- package/src/tools/dynamic-registry.ts +258 -0
- package/src/tools/fs.test.ts +127 -0
- package/src/tools/fs.ts +364 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/read.ts +23 -19
- package/src/utils/logger.ts +112 -33
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "bun:test";
|
|
2
|
+
import { PairingService, type PairingCode } from "./pairing.ts";
|
|
3
|
+
import { eventBus } from "../events/event-bus.ts";
|
|
4
|
+
|
|
5
|
+
describe("PairingService", () => {
|
|
6
|
+
let service: PairingService;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
service = new PairingService({ expirationMs: 1000 });
|
|
10
|
+
eventBus.removeAllListeners();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("generateCode", () => {
|
|
14
|
+
it("should generate unique codes", () => {
|
|
15
|
+
const code1 = service.generateCode("telegram", "user1");
|
|
16
|
+
const code2 = service.generateCode("telegram", "user2");
|
|
17
|
+
|
|
18
|
+
expect(code1).not.toBe(code2);
|
|
19
|
+
expect(code1.length).toBe(8);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should generate uppercase hex codes", () => {
|
|
23
|
+
const code = service.generateCode("telegram", "user1");
|
|
24
|
+
expect(/^[A-F0-9]+$/.test(code)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should emit pairing:requested event", () => {
|
|
28
|
+
const handler = vi.fn();
|
|
29
|
+
eventBus.on("pairing:requested", handler);
|
|
30
|
+
|
|
31
|
+
service.generateCode("telegram", "user1");
|
|
32
|
+
|
|
33
|
+
expect(handler).toHaveBeenCalledWith(
|
|
34
|
+
expect.objectContaining({
|
|
35
|
+
channel: "telegram",
|
|
36
|
+
userId: "user1",
|
|
37
|
+
code: expect.any(String),
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("validateCode", () => {
|
|
44
|
+
it("should return code record for valid code", () => {
|
|
45
|
+
const code = service.generateCode("telegram", "user1");
|
|
46
|
+
const record = service.validateCode(code);
|
|
47
|
+
|
|
48
|
+
expect(record).toBeDefined();
|
|
49
|
+
expect(record?.channel).toBe("telegram");
|
|
50
|
+
expect(record?.userId).toBe("user1");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should return null for invalid code", () => {
|
|
54
|
+
expect(service.validateCode("invalid")).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should return null for expired code", async () => {
|
|
58
|
+
const code = service.generateCode("telegram", "user1");
|
|
59
|
+
|
|
60
|
+
await new Promise((r) => setTimeout(r, 1100));
|
|
61
|
+
|
|
62
|
+
expect(service.validateCode(code)).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should emit pairing:expired event for expired code", async () => {
|
|
66
|
+
const handler = vi.fn();
|
|
67
|
+
eventBus.on("pairing:expired", handler);
|
|
68
|
+
|
|
69
|
+
const code = service.generateCode("telegram", "user1");
|
|
70
|
+
|
|
71
|
+
await new Promise((r) => setTimeout(r, 1100));
|
|
72
|
+
|
|
73
|
+
service.validateCode(code);
|
|
74
|
+
|
|
75
|
+
expect(handler).toHaveBeenCalledWith(
|
|
76
|
+
expect.objectContaining({
|
|
77
|
+
code,
|
|
78
|
+
channel: "telegram",
|
|
79
|
+
userId: "user1",
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("approve", () => {
|
|
86
|
+
it("should approve valid code", () => {
|
|
87
|
+
const code = service.generateCode("telegram", "user1");
|
|
88
|
+
const result = service.approve(code);
|
|
89
|
+
|
|
90
|
+
expect(result.success).toBe(true);
|
|
91
|
+
expect(service.isAllowed("telegram", "user1")).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should fail for invalid code", () => {
|
|
95
|
+
const result = service.approve("invalid");
|
|
96
|
+
|
|
97
|
+
expect(result.success).toBe(false);
|
|
98
|
+
expect(result.error).toContain("Invalid or expired");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should fail for expired code", async () => {
|
|
102
|
+
const code = service.generateCode("telegram", "user1");
|
|
103
|
+
|
|
104
|
+
await new Promise((r) => setTimeout(r, 1100));
|
|
105
|
+
|
|
106
|
+
const result = service.approve(code);
|
|
107
|
+
expect(result.success).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should emit pairing:approved event", () => {
|
|
111
|
+
const handler = vi.fn();
|
|
112
|
+
eventBus.on("pairing:approved", handler);
|
|
113
|
+
|
|
114
|
+
const code = service.generateCode("telegram", "user1");
|
|
115
|
+
service.approve(code);
|
|
116
|
+
|
|
117
|
+
expect(handler).toHaveBeenCalledWith(
|
|
118
|
+
expect.objectContaining({
|
|
119
|
+
channel: "telegram",
|
|
120
|
+
userId: "user1",
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should delete code after approval", () => {
|
|
126
|
+
const code = service.generateCode("telegram", "user1");
|
|
127
|
+
service.approve(code);
|
|
128
|
+
|
|
129
|
+
const result = service.approve(code);
|
|
130
|
+
expect(result.success).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("reject", () => {
|
|
135
|
+
it("should reject pending code", () => {
|
|
136
|
+
const code = service.generateCode("telegram", "user1");
|
|
137
|
+
const result = service.reject(code, "Blocked");
|
|
138
|
+
|
|
139
|
+
expect(result).toBe(true);
|
|
140
|
+
expect(service.validateCode(code)).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should emit pairing:rejected event", () => {
|
|
144
|
+
const handler = vi.fn();
|
|
145
|
+
eventBus.on("pairing:rejected", handler);
|
|
146
|
+
|
|
147
|
+
const code = service.generateCode("telegram", "user1");
|
|
148
|
+
service.reject(code, "Blocked");
|
|
149
|
+
|
|
150
|
+
expect(handler).toHaveBeenCalledWith(
|
|
151
|
+
expect.objectContaining({
|
|
152
|
+
channel: "telegram",
|
|
153
|
+
userId: "user1",
|
|
154
|
+
reason: "Blocked",
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should return false for invalid code", () => {
|
|
160
|
+
expect(service.reject("invalid", "reason")).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("attempt", () => {
|
|
165
|
+
it("should increment attempt count", () => {
|
|
166
|
+
const code = service.generateCode("telegram", "user1");
|
|
167
|
+
|
|
168
|
+
service.attempt(code);
|
|
169
|
+
service.attempt(code);
|
|
170
|
+
|
|
171
|
+
const pending = service.listPending();
|
|
172
|
+
const record = pending.find((p) => p.code === code);
|
|
173
|
+
expect(record?.attempts).toBe(2);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should delete code after max attempts", () => {
|
|
177
|
+
const serviceWithLowAttempts = new PairingService({
|
|
178
|
+
expirationMs: 60000,
|
|
179
|
+
maxAttempts: 2,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const code = serviceWithLowAttempts.generateCode("telegram", "user1");
|
|
183
|
+
|
|
184
|
+
serviceWithLowAttempts.attempt(code);
|
|
185
|
+
const result = serviceWithLowAttempts.attempt(code);
|
|
186
|
+
|
|
187
|
+
expect(result).toBe(false);
|
|
188
|
+
expect(serviceWithLowAttempts.validateCode(code)).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("isAllowed", () => {
|
|
193
|
+
it("should return false for non-approved user", () => {
|
|
194
|
+
expect(service.isAllowed("telegram", "user1")).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should return true for approved user", () => {
|
|
198
|
+
const code = service.generateCode("telegram", "user1");
|
|
199
|
+
service.approve(code);
|
|
200
|
+
|
|
201
|
+
expect(service.isAllowed("telegram", "user1")).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should work per channel", () => {
|
|
205
|
+
const code = service.generateCode("telegram", "user1");
|
|
206
|
+
service.approve(code);
|
|
207
|
+
|
|
208
|
+
expect(service.isAllowed("telegram", "user1")).toBe(true);
|
|
209
|
+
expect(service.isAllowed("discord", "user1")).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("removeFromAllowlist", () => {
|
|
214
|
+
it("should remove user from allowlist", () => {
|
|
215
|
+
const code = service.generateCode("telegram", "user1");
|
|
216
|
+
service.approve(code);
|
|
217
|
+
|
|
218
|
+
const result = service.removeFromAllowlist("telegram", "user1");
|
|
219
|
+
|
|
220
|
+
expect(result).toBe(true);
|
|
221
|
+
expect(service.isAllowed("telegram", "user1")).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should return false if user not in allowlist", () => {
|
|
225
|
+
expect(service.removeFromAllowlist("telegram", "unknown")).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("listAllowed", () => {
|
|
230
|
+
it("should list all allowed users", () => {
|
|
231
|
+
const code1 = service.generateCode("telegram", "user1");
|
|
232
|
+
const code2 = service.generateCode("discord", "user2");
|
|
233
|
+
service.approve(code1);
|
|
234
|
+
service.approve(code2);
|
|
235
|
+
|
|
236
|
+
const list = service.listAllowed();
|
|
237
|
+
|
|
238
|
+
expect(list.length).toBe(2);
|
|
239
|
+
expect(list).toContainEqual({ channel: "telegram", userId: "user1" });
|
|
240
|
+
expect(list).toContainEqual({ channel: "discord", userId: "user2" });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should filter by channel", () => {
|
|
244
|
+
const code1 = service.generateCode("telegram", "user1");
|
|
245
|
+
const code2 = service.generateCode("discord", "user2");
|
|
246
|
+
service.approve(code1);
|
|
247
|
+
service.approve(code2);
|
|
248
|
+
|
|
249
|
+
const list = service.listAllowed("telegram");
|
|
250
|
+
|
|
251
|
+
expect(list.length).toBe(1);
|
|
252
|
+
expect(list[0]).toEqual({ channel: "telegram", userId: "user1" });
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("listPending", () => {
|
|
257
|
+
it("should list pending codes", () => {
|
|
258
|
+
service.generateCode("telegram", "user1");
|
|
259
|
+
service.generateCode("discord", "user2");
|
|
260
|
+
|
|
261
|
+
const pending = service.listPending();
|
|
262
|
+
|
|
263
|
+
expect(pending.length).toBe(2);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should not include expired codes", async () => {
|
|
267
|
+
service.generateCode("telegram", "user1");
|
|
268
|
+
|
|
269
|
+
await new Promise((r) => setTimeout(r, 1100));
|
|
270
|
+
|
|
271
|
+
const pending = service.listPending();
|
|
272
|
+
expect(pending.length).toBe(0);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("getStats", () => {
|
|
277
|
+
it("should return correct stats", () => {
|
|
278
|
+
const code1 = service.generateCode("telegram", "user1");
|
|
279
|
+
service.approve(code1);
|
|
280
|
+
service.generateCode("discord", "user2");
|
|
281
|
+
|
|
282
|
+
const stats = service.getStats();
|
|
283
|
+
|
|
284
|
+
expect(stats.pendingCodes).toBe(1);
|
|
285
|
+
expect(stats.totalAllowlist).toBe(1);
|
|
286
|
+
expect(stats.byChannel.telegram.allowed).toBe(1);
|
|
287
|
+
expect(stats.byChannel.discord.pending).toBe(1);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("clear", () => {
|
|
292
|
+
it("should clear all data", () => {
|
|
293
|
+
const code = service.generateCode("telegram", "user1");
|
|
294
|
+
service.approve(code);
|
|
295
|
+
|
|
296
|
+
service.clear();
|
|
297
|
+
|
|
298
|
+
expect(service.listPending().length).toBe(0);
|
|
299
|
+
expect(service.listAllowed().length).toBe(0);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -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();
|