@hailer/mcp 0.1.15 → 0.1.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/.claude/agents/agent-giuseppe-app-builder.md +7 -6
- package/.claude/agents/agent-lars-code-inspector.md +26 -14
- package/dist/agents/bot-manager.d.ts +48 -0
- package/dist/agents/bot-manager.js +254 -0
- package/dist/agents/factory.d.ts +150 -0
- package/dist/agents/factory.js +650 -0
- package/dist/agents/giuseppe/ai.d.ts +83 -0
- package/dist/agents/giuseppe/ai.js +466 -0
- package/dist/agents/giuseppe/bot.d.ts +110 -0
- package/dist/agents/giuseppe/bot.js +780 -0
- package/dist/agents/giuseppe/config.d.ts +25 -0
- package/dist/agents/giuseppe/config.js +227 -0
- package/dist/agents/giuseppe/files.d.ts +52 -0
- package/dist/agents/giuseppe/files.js +338 -0
- package/dist/agents/giuseppe/git.d.ts +48 -0
- package/dist/agents/giuseppe/git.js +298 -0
- package/dist/agents/giuseppe/index.d.ts +97 -0
- package/dist/agents/giuseppe/index.js +258 -0
- package/dist/agents/giuseppe/lsp.d.ts +113 -0
- package/dist/agents/giuseppe/lsp.js +485 -0
- package/dist/agents/giuseppe/monitor.d.ts +118 -0
- package/dist/agents/giuseppe/monitor.js +621 -0
- package/dist/agents/giuseppe/prompt.d.ts +5 -0
- package/dist/agents/giuseppe/prompt.js +94 -0
- package/dist/agents/giuseppe/registries/pending-classification.d.ts +28 -0
- package/dist/agents/giuseppe/registries/pending-classification.js +50 -0
- package/dist/agents/giuseppe/registries/pending-fix.d.ts +30 -0
- package/dist/agents/giuseppe/registries/pending-fix.js +42 -0
- package/dist/agents/giuseppe/registries/pending.d.ts +27 -0
- package/dist/agents/giuseppe/registries/pending.js +49 -0
- package/dist/agents/giuseppe/specialist.d.ts +47 -0
- package/dist/agents/giuseppe/specialist.js +237 -0
- package/dist/agents/giuseppe/types.d.ts +123 -0
- package/dist/agents/giuseppe/types.js +9 -0
- package/dist/agents/hailer-expert/index.d.ts +8 -0
- package/dist/agents/hailer-expert/index.js +14 -0
- package/dist/agents/hal/daemon.d.ts +142 -0
- package/dist/agents/hal/daemon.js +1103 -0
- package/dist/agents/hal/definitions.d.ts +55 -0
- package/dist/agents/hal/definitions.js +263 -0
- package/dist/agents/hal/index.d.ts +3 -0
- package/dist/agents/hal/index.js +8 -0
- package/dist/agents/index.d.ts +18 -0
- package/dist/agents/index.js +48 -0
- package/dist/agents/shared/base.d.ts +216 -0
- package/dist/agents/shared/base.js +846 -0
- package/dist/agents/shared/services/agent-registry.d.ts +107 -0
- package/dist/agents/shared/services/agent-registry.js +629 -0
- package/dist/agents/shared/services/conversation-manager.d.ts +50 -0
- package/dist/agents/shared/services/conversation-manager.js +136 -0
- package/dist/agents/shared/services/mcp-client.d.ts +56 -0
- package/dist/agents/shared/services/mcp-client.js +124 -0
- package/dist/agents/shared/services/message-classifier.d.ts +37 -0
- package/dist/agents/shared/services/message-classifier.js +187 -0
- package/dist/agents/shared/services/message-formatter.d.ts +89 -0
- package/dist/agents/shared/services/message-formatter.js +371 -0
- package/dist/agents/shared/services/session-logger.d.ts +106 -0
- package/dist/agents/shared/services/session-logger.js +446 -0
- package/dist/agents/shared/services/tool-executor.d.ts +41 -0
- package/dist/agents/shared/services/tool-executor.js +169 -0
- package/dist/agents/shared/services/workspace-schema-cache.d.ts +125 -0
- package/dist/agents/shared/services/workspace-schema-cache.js +578 -0
- package/dist/agents/shared/specialist.d.ts +91 -0
- package/dist/agents/shared/specialist.js +399 -0
- package/dist/agents/shared/tool-schema-loader.d.ts +62 -0
- package/dist/agents/shared/tool-schema-loader.js +232 -0
- package/dist/agents/shared/types.d.ts +327 -0
- package/dist/agents/shared/types.js +121 -0
- package/dist/app.js +21 -4
- package/dist/cli.js +0 -0
- package/dist/client/agents/orchestrator.d.ts +1 -0
- package/dist/client/agents/orchestrator.js +12 -1
- package/dist/commands/seed-config.d.ts +9 -0
- package/dist/commands/seed-config.js +372 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +61 -1
- package/dist/core.d.ts +8 -0
- package/dist/core.js +137 -6
- package/dist/lib/discussion-lock.d.ts +42 -0
- package/dist/lib/discussion-lock.js +110 -0
- package/dist/mcp/UserContextCache.js +2 -2
- package/dist/mcp/hailer-clients.d.ts +15 -0
- package/dist/mcp/hailer-clients.js +100 -6
- package/dist/mcp/signal-handler.d.ts +16 -5
- package/dist/mcp/signal-handler.js +173 -122
- package/dist/mcp/tools/activity.js +9 -1
- package/dist/mcp/tools/bot-config.d.ts +184 -9
- package/dist/mcp/tools/bot-config.js +2177 -163
- package/dist/mcp/tools/giuseppe-tools.d.ts +21 -0
- package/dist/mcp/tools/giuseppe-tools.js +525 -0
- package/dist/mcp/utils/hailer-api-client.d.ts +42 -1
- package/dist/mcp/utils/hailer-api-client.js +128 -2
- package/dist/mcp/webhook-handler.d.ts +87 -0
- package/dist/mcp/webhook-handler.js +343 -0
- package/dist/mcp/workspace-cache.d.ts +5 -0
- package/dist/mcp/workspace-cache.js +11 -0
- package/dist/mcp-server.js +55 -5
- package/dist/modules/bug-reports/giuseppe-agent.d.ts +58 -0
- package/dist/modules/bug-reports/giuseppe-agent.js +467 -0
- package/dist/modules/bug-reports/giuseppe-ai.d.ts +25 -1
- package/dist/modules/bug-reports/giuseppe-ai.js +133 -2
- package/dist/modules/bug-reports/giuseppe-bot.d.ts +2 -2
- package/dist/modules/bug-reports/giuseppe-bot.js +66 -42
- package/dist/modules/bug-reports/giuseppe-daemon.d.ts +80 -0
- package/dist/modules/bug-reports/giuseppe-daemon.js +617 -0
- package/dist/modules/bug-reports/giuseppe-files.d.ts +12 -0
- package/dist/modules/bug-reports/giuseppe-files.js +37 -0
- package/dist/modules/bug-reports/giuseppe-lsp.d.ts +84 -13
- package/dist/modules/bug-reports/giuseppe-lsp.js +403 -61
- package/dist/modules/bug-reports/index.d.ts +1 -0
- package/dist/modules/bug-reports/index.js +31 -29
- package/package.json +3 -2
|
@@ -14,6 +14,12 @@ class HailerApiClient {
|
|
|
14
14
|
constructor(clients) {
|
|
15
15
|
this.clients = clients;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Get the underlying HailerClient for direct access (e.g., SignalHandler)
|
|
19
|
+
*/
|
|
20
|
+
getClient() {
|
|
21
|
+
return this.clients;
|
|
22
|
+
}
|
|
17
23
|
/**
|
|
18
24
|
* Makes a socket API call - thin wrapper around clients.socket.request
|
|
19
25
|
*/
|
|
@@ -77,6 +83,7 @@ class HailerApiClient {
|
|
|
77
83
|
sortOrder: options?.sortOrder || "desc",
|
|
78
84
|
includeStats: options?.includeStats !== undefined ? options?.includeStats : true,
|
|
79
85
|
returnFlat: options?.returnFlat !== undefined ? options?.returnFlat : true,
|
|
86
|
+
fields: options?.fields || undefined,
|
|
80
87
|
};
|
|
81
88
|
this.logger.debug('📋 V3 Activity List API Call', {
|
|
82
89
|
endpoint: 'v3.activity.list',
|
|
@@ -201,10 +208,35 @@ class HailerApiClient {
|
|
|
201
208
|
}
|
|
202
209
|
/**
|
|
203
210
|
* Fetch activity by ID - uses Socket API
|
|
211
|
+
* WARNING: This uses activities.load which evaluates function fields and may fail
|
|
212
|
+
* For bot config, prefer fetchActivityByIdSafe()
|
|
204
213
|
*/
|
|
205
214
|
async fetchActivityById(activityId) {
|
|
206
215
|
return await this.request('activities.load', [activityId]);
|
|
207
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Fetch activity by ID safely using v3.activity.list API
|
|
219
|
+
* This doesn't evaluate function fields so it works even when function fields are broken
|
|
220
|
+
*/
|
|
221
|
+
async fetchActivityByIdSafe(activityId, workflowId, phaseId) {
|
|
222
|
+
// v3.activity.list doesn't support filtering by _id, so we list and filter client-side
|
|
223
|
+
if (!workflowId || !phaseId) {
|
|
224
|
+
throw new Error('fetchActivityByIdSafe requires workflowId and phaseId');
|
|
225
|
+
}
|
|
226
|
+
const query = { processId: workflowId, phaseId };
|
|
227
|
+
const options = { limit: 100, returnFlat: true };
|
|
228
|
+
const result = await this.request('v3.activity.list', [query, options]);
|
|
229
|
+
const activities = result?.activities || result?.list || result?.data || [];
|
|
230
|
+
this.logger.debug('fetchActivityByIdSafe response', {
|
|
231
|
+
activityId,
|
|
232
|
+
workflowId,
|
|
233
|
+
phaseId,
|
|
234
|
+
resultKeys: Object.keys(result || {}),
|
|
235
|
+
activityCount: activities.length,
|
|
236
|
+
activityIds: activities.slice(0, 5).map((a) => a._id)
|
|
237
|
+
});
|
|
238
|
+
return activities.find((a) => a._id === activityId) || null;
|
|
239
|
+
}
|
|
208
240
|
/**
|
|
209
241
|
* Send discussion message - uses Socket API
|
|
210
242
|
*/
|
|
@@ -291,9 +323,103 @@ class HailerApiClient {
|
|
|
291
323
|
}
|
|
292
324
|
/**
|
|
293
325
|
* Fetch workspace initialization data - uses Socket API
|
|
326
|
+
* @param include - What to include: 'users', 'network', 'networks', 'teams', etc.
|
|
327
|
+
*/
|
|
328
|
+
async fetchInit(include = ['users', 'network']) {
|
|
329
|
+
return await this.request('v2.core.init', [include]);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Update current user's profile info (firstname, lastname, etc.)
|
|
333
|
+
* Only works for the currently authenticated user
|
|
334
|
+
* @param key - Field to update: firstname, lastname, email, status, etc.
|
|
335
|
+
* @param value - New value as string
|
|
336
|
+
*/
|
|
337
|
+
async setUserInfo(key, value) {
|
|
338
|
+
await this.request('user.set_user_info', [key, [value]]);
|
|
339
|
+
this.logger.debug('User info updated', { key, value });
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Update current user's display name (firstname + lastname)
|
|
343
|
+
* Parses a full name into first/last parts
|
|
344
|
+
* @param fullName - Full display name like "HAL 9000"
|
|
345
|
+
*/
|
|
346
|
+
async setUserDisplayName(fullName) {
|
|
347
|
+
const parts = fullName.trim().split(/\s+/);
|
|
348
|
+
const firstname = parts[0] || fullName;
|
|
349
|
+
const lastname = parts.slice(1).join(' ') || '';
|
|
350
|
+
await this.setUserInfo('firstname', firstname);
|
|
351
|
+
if (lastname) {
|
|
352
|
+
await this.setUserInfo('lastname', lastname);
|
|
353
|
+
}
|
|
354
|
+
this.logger.info('User display name updated', { fullName, firstname, lastname });
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Find user by name - searches init data for firstname/lastname match
|
|
358
|
+
* @param name - Full name to search for (firstname lastname)
|
|
359
|
+
* @returns User ID if found, null otherwise
|
|
360
|
+
*/
|
|
361
|
+
async findUserByName(name) {
|
|
362
|
+
try {
|
|
363
|
+
const init = await this.fetchInit(['users']);
|
|
364
|
+
const users = Object.values(init?.users || {});
|
|
365
|
+
const searchName = name.toLowerCase().trim();
|
|
366
|
+
// Try to find user by full name match
|
|
367
|
+
const matchingUser = users.find(u => {
|
|
368
|
+
const fullName = `${u.firstname || ''} ${u.lastname || ''}`.toLowerCase().trim();
|
|
369
|
+
return fullName === searchName;
|
|
370
|
+
});
|
|
371
|
+
if (matchingUser) {
|
|
372
|
+
this.logger.debug('findUserByName: found user', { name, userId: matchingUser._id });
|
|
373
|
+
return matchingUser._id;
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
this.logger.debug('findUserByName failed', { name, error });
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* List all workflows in the workspace - extracts from init data
|
|
384
|
+
* @param workspaceId - Optional workspace ID to filter workflows (uses CID field)
|
|
294
385
|
*/
|
|
295
|
-
async
|
|
296
|
-
|
|
386
|
+
async listWorkflows(workspaceId) {
|
|
387
|
+
// Must include 'processes' to get workflows
|
|
388
|
+
const init = await this.fetchInit(['users', 'network', 'processes']);
|
|
389
|
+
// Init returns processes (workflows) as an object keyed by process ID
|
|
390
|
+
if (init?.processes) {
|
|
391
|
+
let workflows = Object.entries(init.processes).map(([id, process]) => ({
|
|
392
|
+
_id: id,
|
|
393
|
+
...process
|
|
394
|
+
}));
|
|
395
|
+
// Filter by workspace ID if specified (fixes API caching issue after workspace switch)
|
|
396
|
+
if (workspaceId) {
|
|
397
|
+
workflows = workflows.filter(w => w.cid === workspaceId);
|
|
398
|
+
this.logger.debug('Filtered workflows by workspace', {
|
|
399
|
+
workspaceId,
|
|
400
|
+
totalCount: Object.keys(init.processes).length,
|
|
401
|
+
filteredCount: workflows.length
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return workflows;
|
|
405
|
+
}
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Get workflow details including phases and fields
|
|
410
|
+
*/
|
|
411
|
+
async getWorkflow(workflowId) {
|
|
412
|
+
// Must include 'processes' to get workflow data
|
|
413
|
+
const init = await this.fetchInit(['processes']);
|
|
414
|
+
if (init?.processes?.[workflowId]) {
|
|
415
|
+
return {
|
|
416
|
+
_id: workflowId,
|
|
417
|
+
...init.processes[workflowId],
|
|
418
|
+
phases: init.processes[workflowId].phases || {},
|
|
419
|
+
fields: init.processes[workflowId].fields || {}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
throw new Error(`Workflow ${workflowId} not found`);
|
|
297
423
|
}
|
|
298
424
|
/**
|
|
299
425
|
* Global search across workspace - uses Socket API
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook Handler for Bot Config Updates
|
|
3
|
+
*
|
|
4
|
+
* Receives activity updates from Hailer workflow webhooks and updates
|
|
5
|
+
* local .bot-config/{workspaceId}.json files.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Generate HMAC-SHA256 signature for webhook payload
|
|
9
|
+
*/
|
|
10
|
+
export declare function generateWebhookSignature(payload: string, secret: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Verify HMAC-SHA256 signature of webhook payload
|
|
13
|
+
* @returns true if signature is valid, false otherwise
|
|
14
|
+
*/
|
|
15
|
+
export declare function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Get webhook token for secure endpoints.
|
|
18
|
+
*
|
|
19
|
+
* Production: WEBHOOK_TOKEN env var (injected by AWS Secrets Manager)
|
|
20
|
+
* Development: Falls back to file-based token for local testing
|
|
21
|
+
*/
|
|
22
|
+
export declare function getWebhookToken(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Get the full webhook path with token (no prefix for security through obscurity)
|
|
25
|
+
*/
|
|
26
|
+
export declare function getWebhookPath(): string;
|
|
27
|
+
interface WebhookField {
|
|
28
|
+
id: string;
|
|
29
|
+
type: string;
|
|
30
|
+
value: any;
|
|
31
|
+
key: string;
|
|
32
|
+
}
|
|
33
|
+
interface WebhookPayload {
|
|
34
|
+
_id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
fields: WebhookField[];
|
|
37
|
+
currentPhase: string;
|
|
38
|
+
process: string;
|
|
39
|
+
cid: string;
|
|
40
|
+
uid: string;
|
|
41
|
+
created: number;
|
|
42
|
+
updated: number;
|
|
43
|
+
}
|
|
44
|
+
interface BotEntry {
|
|
45
|
+
activityId: string;
|
|
46
|
+
userId: string | null;
|
|
47
|
+
email: string;
|
|
48
|
+
password: string;
|
|
49
|
+
botType: string;
|
|
50
|
+
enabled: boolean;
|
|
51
|
+
displayName?: string;
|
|
52
|
+
}
|
|
53
|
+
interface WorkspaceConfig {
|
|
54
|
+
workspaceId: string;
|
|
55
|
+
workspaceName: string;
|
|
56
|
+
orchestrator?: {
|
|
57
|
+
activityId: string;
|
|
58
|
+
userId: string;
|
|
59
|
+
email: string;
|
|
60
|
+
password: string;
|
|
61
|
+
displayName?: string;
|
|
62
|
+
};
|
|
63
|
+
specialists: BotEntry[];
|
|
64
|
+
lastSynced: string;
|
|
65
|
+
}
|
|
66
|
+
type BotUpdateCallback = (workspaceId: string, bot: BotEntry, action: 'add' | 'update' | 'remove') => void;
|
|
67
|
+
export declare function onBotUpdate(callback: BotUpdateCallback): void;
|
|
68
|
+
/**
|
|
69
|
+
* Process webhook payload and update workspace config
|
|
70
|
+
*/
|
|
71
|
+
export declare function handleBotConfigWebhook(payload: WebhookPayload): {
|
|
72
|
+
success: boolean;
|
|
73
|
+
action: string;
|
|
74
|
+
workspaceId: string;
|
|
75
|
+
botType: string | null;
|
|
76
|
+
error?: string;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Get workspace config (for debugging/status)
|
|
80
|
+
*/
|
|
81
|
+
export declare function getWorkspaceConfig(workspaceId: string): WorkspaceConfig | null;
|
|
82
|
+
/**
|
|
83
|
+
* List all workspace configs
|
|
84
|
+
*/
|
|
85
|
+
export declare function listWorkspaceConfigs(): WorkspaceConfig[];
|
|
86
|
+
export {};
|
|
87
|
+
//# sourceMappingURL=webhook-handler.d.ts.map
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Webhook Handler for Bot Config Updates
|
|
4
|
+
*
|
|
5
|
+
* Receives activity updates from Hailer workflow webhooks and updates
|
|
6
|
+
* local .bot-config/{workspaceId}.json files.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.generateWebhookSignature = generateWebhookSignature;
|
|
43
|
+
exports.verifyWebhookSignature = verifyWebhookSignature;
|
|
44
|
+
exports.getWebhookToken = getWebhookToken;
|
|
45
|
+
exports.getWebhookPath = getWebhookPath;
|
|
46
|
+
exports.onBotUpdate = onBotUpdate;
|
|
47
|
+
exports.handleBotConfigWebhook = handleBotConfigWebhook;
|
|
48
|
+
exports.getWorkspaceConfig = getWorkspaceConfig;
|
|
49
|
+
exports.listWorkspaceConfigs = listWorkspaceConfigs;
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const crypto = __importStar(require("crypto"));
|
|
53
|
+
const logger_1 = require("../lib/logger");
|
|
54
|
+
const config_1 = require("../config");
|
|
55
|
+
const logger = (0, logger_1.createLogger)({ component: 'webhook-handler' });
|
|
56
|
+
const BOT_CONFIG_DIR = '.bot-config';
|
|
57
|
+
const WEBHOOK_SECRET_FILE = 'webhook-secret.txt';
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// WEBHOOK TOKEN SECURITY
|
|
60
|
+
// ============================================================================
|
|
61
|
+
/**
|
|
62
|
+
* Constant-time string comparison to prevent timing attacks
|
|
63
|
+
*/
|
|
64
|
+
function timingSafeEqual(a, b) {
|
|
65
|
+
if (a.length !== b.length) {
|
|
66
|
+
// Still perform comparison to maintain constant time
|
|
67
|
+
crypto.timingSafeEqual(Buffer.from(a), Buffer.from(a));
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Generate HMAC-SHA256 signature for webhook payload
|
|
74
|
+
*/
|
|
75
|
+
function generateWebhookSignature(payload, secret) {
|
|
76
|
+
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Verify HMAC-SHA256 signature of webhook payload
|
|
80
|
+
* @returns true if signature is valid, false otherwise
|
|
81
|
+
*/
|
|
82
|
+
function verifyWebhookSignature(payload, signature, secret) {
|
|
83
|
+
if (!signature || !secret) {
|
|
84
|
+
logger.warn('Missing signature or secret for webhook verification');
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const expectedSignature = generateWebhookSignature(payload, secret);
|
|
88
|
+
// Use constant-time comparison to prevent timing attacks
|
|
89
|
+
const isValid = timingSafeEqual(signature, expectedSignature);
|
|
90
|
+
if (!isValid) {
|
|
91
|
+
logger.warn('Invalid webhook signature', {
|
|
92
|
+
provided: signature.slice(0, 8) + '...',
|
|
93
|
+
expected: expectedSignature.slice(0, 8) + '...',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return isValid;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get webhook token for secure endpoints.
|
|
100
|
+
*
|
|
101
|
+
* Production: WEBHOOK_TOKEN env var (injected by AWS Secrets Manager)
|
|
102
|
+
* Development: Falls back to file-based token for local testing
|
|
103
|
+
*/
|
|
104
|
+
function getWebhookToken() {
|
|
105
|
+
// Production: Always use environment variable (AWS Secrets Manager injects this)
|
|
106
|
+
if (process.env.WEBHOOK_TOKEN) {
|
|
107
|
+
logger.debug('Using webhook token from environment');
|
|
108
|
+
return process.env.WEBHOOK_TOKEN;
|
|
109
|
+
}
|
|
110
|
+
// Development only: File-based fallback
|
|
111
|
+
if (process.env.NODE_ENV === 'production') {
|
|
112
|
+
throw new Error('WEBHOOK_TOKEN environment variable is required in production');
|
|
113
|
+
}
|
|
114
|
+
const configDir = path.join(process.cwd(), BOT_CONFIG_DIR);
|
|
115
|
+
const secretPath = path.join(configDir, WEBHOOK_SECRET_FILE);
|
|
116
|
+
// Try to read existing dev token
|
|
117
|
+
if (fs.existsSync(secretPath)) {
|
|
118
|
+
try {
|
|
119
|
+
const token = fs.readFileSync(secretPath, 'utf-8').trim();
|
|
120
|
+
if (token.length >= 16) {
|
|
121
|
+
logger.debug('Using webhook token from file (dev mode)');
|
|
122
|
+
return token;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
logger.warn('Failed to read webhook secret file', { error });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Generate new dev token
|
|
130
|
+
const token = crypto.randomBytes(16).toString('hex');
|
|
131
|
+
if (!fs.existsSync(configDir)) {
|
|
132
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
fs.writeFileSync(secretPath, token, { mode: 0o600 });
|
|
135
|
+
const maskedToken = `${token.slice(0, 4)}...${token.slice(-4)}`;
|
|
136
|
+
logger.info('Generated dev webhook token', { maskedToken, path: secretPath });
|
|
137
|
+
return token;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get the full webhook path with token (no prefix for security through obscurity)
|
|
141
|
+
*/
|
|
142
|
+
function getWebhookPath() {
|
|
143
|
+
return `/${getWebhookToken()}`;
|
|
144
|
+
}
|
|
145
|
+
let botUpdateCallback = null;
|
|
146
|
+
function onBotUpdate(callback) {
|
|
147
|
+
botUpdateCallback = callback;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get field value from webhook payload by key
|
|
151
|
+
*/
|
|
152
|
+
function getFieldValue(fields, key) {
|
|
153
|
+
const field = fields.find((f) => f.key === key);
|
|
154
|
+
return field?.value ?? null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Load existing workspace config or create empty one
|
|
158
|
+
*/
|
|
159
|
+
function loadWorkspaceConfig(workspaceId) {
|
|
160
|
+
const configDir = path.join(process.cwd(), BOT_CONFIG_DIR);
|
|
161
|
+
const configPath = path.join(configDir, `${workspaceId}.json`);
|
|
162
|
+
if (fs.existsSync(configPath)) {
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
logger.warn('Failed to load workspace config', { workspaceId, error: String(error) });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
workspaceId,
|
|
172
|
+
workspaceName: workspaceId,
|
|
173
|
+
specialists: [],
|
|
174
|
+
lastSynced: new Date().toISOString(),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Save workspace config to file
|
|
179
|
+
*/
|
|
180
|
+
function saveWorkspaceConfig(config) {
|
|
181
|
+
const configDir = path.join(process.cwd(), BOT_CONFIG_DIR);
|
|
182
|
+
// Ensure directory exists
|
|
183
|
+
if (!fs.existsSync(configDir)) {
|
|
184
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
185
|
+
}
|
|
186
|
+
const configPath = path.join(configDir, `${config.workspaceId}.json`);
|
|
187
|
+
config.lastSynced = new Date().toISOString();
|
|
188
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
189
|
+
logger.info('Saved workspace config', {
|
|
190
|
+
workspaceId: config.workspaceId,
|
|
191
|
+
path: configPath,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Process webhook payload and update workspace config
|
|
196
|
+
*/
|
|
197
|
+
function handleBotConfigWebhook(payload) {
|
|
198
|
+
const workspaceId = payload.cid;
|
|
199
|
+
logger.info('Processing bot config webhook', {
|
|
200
|
+
activityId: payload._id,
|
|
201
|
+
activityName: payload.name,
|
|
202
|
+
workspaceId,
|
|
203
|
+
phase: payload.currentPhase,
|
|
204
|
+
});
|
|
205
|
+
// Extract fields
|
|
206
|
+
const email = getFieldValue(payload.fields, 'agentEmailInHailer');
|
|
207
|
+
const password = getFieldValue(payload.fields, 'password');
|
|
208
|
+
const botType = getFieldValue(payload.fields, 'botType');
|
|
209
|
+
const userId = getFieldValue(payload.fields, 'hailerProfile');
|
|
210
|
+
const schemaConfigStr = getFieldValue(payload.fields, 'schemaConfig');
|
|
211
|
+
// Validate required fields
|
|
212
|
+
if (!email || !password) {
|
|
213
|
+
logger.warn('Webhook missing credentials', {
|
|
214
|
+
activityId: payload._id,
|
|
215
|
+
hasEmail: !!email,
|
|
216
|
+
hasPassword: !!password,
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
success: false,
|
|
220
|
+
action: 'skip',
|
|
221
|
+
workspaceId,
|
|
222
|
+
botType,
|
|
223
|
+
error: 'Missing email or password',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
// Parse schema config to determine if deployed or retired
|
|
227
|
+
let deployedPhaseId = null;
|
|
228
|
+
let retiredPhaseId = null;
|
|
229
|
+
if (schemaConfigStr) {
|
|
230
|
+
try {
|
|
231
|
+
const schemaConfig = JSON.parse(schemaConfigStr);
|
|
232
|
+
deployedPhaseId = schemaConfig.deployedPhaseId;
|
|
233
|
+
retiredPhaseId = schemaConfig.retiredPhaseId;
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
logger.warn('Failed to parse schemaConfig', { schemaConfigStr });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const isDeployed = deployedPhaseId ? payload.currentPhase === deployedPhaseId : true;
|
|
240
|
+
const isRetired = retiredPhaseId ? payload.currentPhase === retiredPhaseId : false;
|
|
241
|
+
const enabled = isDeployed && !isRetired;
|
|
242
|
+
// Load existing config
|
|
243
|
+
const config = loadWorkspaceConfig(workspaceId);
|
|
244
|
+
const botEntry = {
|
|
245
|
+
activityId: payload._id,
|
|
246
|
+
userId: userId || null,
|
|
247
|
+
email,
|
|
248
|
+
password,
|
|
249
|
+
botType: botType || 'unknown',
|
|
250
|
+
enabled,
|
|
251
|
+
displayName: payload.name, // Activity name from Agent Directory
|
|
252
|
+
};
|
|
253
|
+
let action;
|
|
254
|
+
// Handle orchestrator
|
|
255
|
+
if (botType === 'orchestrator') {
|
|
256
|
+
if (enabled) {
|
|
257
|
+
config.orchestrator = {
|
|
258
|
+
activityId: payload._id,
|
|
259
|
+
userId: userId || '',
|
|
260
|
+
email,
|
|
261
|
+
password,
|
|
262
|
+
displayName: payload.name,
|
|
263
|
+
};
|
|
264
|
+
action = 'update';
|
|
265
|
+
logger.info('Updated orchestrator', { workspaceId, email: (0, config_1.maskEmail)(email), displayName: payload.name });
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
// Orchestrator disabled - remove it
|
|
269
|
+
if (config.orchestrator?.activityId === payload._id) {
|
|
270
|
+
delete config.orchestrator;
|
|
271
|
+
action = 'remove';
|
|
272
|
+
logger.info('Removed orchestrator', { workspaceId, email: (0, config_1.maskEmail)(email) });
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
action = 'update';
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
// Handle specialist
|
|
281
|
+
const existingIndex = config.specialists.findIndex((s) => s.activityId === payload._id);
|
|
282
|
+
if (existingIndex >= 0) {
|
|
283
|
+
// Update existing
|
|
284
|
+
config.specialists[existingIndex] = botEntry;
|
|
285
|
+
action = enabled ? 'update' : 'remove';
|
|
286
|
+
}
|
|
287
|
+
else if (enabled) {
|
|
288
|
+
// Add new
|
|
289
|
+
config.specialists.push(botEntry);
|
|
290
|
+
action = 'add';
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
action = 'update';
|
|
294
|
+
}
|
|
295
|
+
logger.info('Updated specialist', {
|
|
296
|
+
workspaceId,
|
|
297
|
+
email,
|
|
298
|
+
botType,
|
|
299
|
+
enabled,
|
|
300
|
+
action,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// Save config
|
|
304
|
+
saveWorkspaceConfig(config);
|
|
305
|
+
// Trigger callback for hot reload
|
|
306
|
+
if (botUpdateCallback) {
|
|
307
|
+
botUpdateCallback(workspaceId, botEntry, action);
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
success: true,
|
|
311
|
+
action,
|
|
312
|
+
workspaceId,
|
|
313
|
+
botType,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Get workspace config (for debugging/status)
|
|
318
|
+
*/
|
|
319
|
+
function getWorkspaceConfig(workspaceId) {
|
|
320
|
+
const config = loadWorkspaceConfig(workspaceId);
|
|
321
|
+
return config.orchestrator || config.specialists.length > 0 ? config : null;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* List all workspace configs
|
|
325
|
+
*/
|
|
326
|
+
function listWorkspaceConfigs() {
|
|
327
|
+
const configDir = path.join(process.cwd(), BOT_CONFIG_DIR);
|
|
328
|
+
if (!fs.existsSync(configDir))
|
|
329
|
+
return [];
|
|
330
|
+
const files = fs.readdirSync(configDir).filter((f) => f.endsWith('.json'));
|
|
331
|
+
const configs = [];
|
|
332
|
+
for (const file of files) {
|
|
333
|
+
try {
|
|
334
|
+
const content = fs.readFileSync(path.join(configDir, file), 'utf-8');
|
|
335
|
+
configs.push(JSON.parse(content));
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
logger.warn('Failed to load workspace config', { file, error: String(error) });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return configs;
|
|
342
|
+
}
|
|
343
|
+
//# sourceMappingURL=webhook-handler.js.map
|
|
@@ -5,6 +5,7 @@ export interface UserInfo {
|
|
|
5
5
|
firstname: string;
|
|
6
6
|
lastname: string;
|
|
7
7
|
fullName: string;
|
|
8
|
+
email?: string;
|
|
8
9
|
companies: string[];
|
|
9
10
|
default_profilepic?: string;
|
|
10
11
|
lastSeen: number;
|
|
@@ -31,6 +32,10 @@ export declare function createWorkspaceCache(init: HailerV2CoreInitResponse, con
|
|
|
31
32
|
* Gets user information by ID
|
|
32
33
|
*/
|
|
33
34
|
export declare function getUserById(cache: WorkspaceCache, userId: string): UserInfo | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Gets user information by email (case-insensitive)
|
|
37
|
+
*/
|
|
38
|
+
export declare function getUserByEmail(cache: WorkspaceCache, email: string): UserInfo | undefined;
|
|
34
39
|
/**
|
|
35
40
|
* Gets workspace by name (case-insensitive)
|
|
36
41
|
*/
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createWorkspaceCache = createWorkspaceCache;
|
|
4
4
|
exports.getUserById = getUserById;
|
|
5
|
+
exports.getUserByEmail = getUserByEmail;
|
|
5
6
|
exports.getWorkspaceByName = getWorkspaceByName;
|
|
6
7
|
exports.resolveWorkspaceId = resolveWorkspaceId;
|
|
7
8
|
/**
|
|
@@ -33,11 +34,14 @@ function createWorkspaceCache(init, config) {
|
|
|
33
34
|
const users = [];
|
|
34
35
|
const usersById = {};
|
|
35
36
|
Object.values(init.users || {}).forEach((user) => {
|
|
37
|
+
// Cast to any to access email field (API returns it but type doesn't include it)
|
|
38
|
+
const userAny = user;
|
|
36
39
|
let userInfo = {
|
|
37
40
|
id: user._id,
|
|
38
41
|
firstname: user.firstname,
|
|
39
42
|
lastname: user.lastname,
|
|
40
43
|
fullName: `${user.firstname} ${user.lastname}`,
|
|
44
|
+
email: userAny.email, // Include email if available
|
|
41
45
|
companies: user.companies || [],
|
|
42
46
|
default_profilepic: config.compactUserData ? undefined : user.default_profilepic,
|
|
43
47
|
lastSeen: config.compactUserData ? 0 : (user.lastSeen || 0),
|
|
@@ -76,6 +80,13 @@ function createWorkspaceCache(init, config) {
|
|
|
76
80
|
function getUserById(cache, userId) {
|
|
77
81
|
return cache.usersById[userId];
|
|
78
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Gets user information by email (case-insensitive)
|
|
85
|
+
*/
|
|
86
|
+
function getUserByEmail(cache, email) {
|
|
87
|
+
const lowerEmail = email.toLowerCase();
|
|
88
|
+
return cache.users.find(user => user.email?.toLowerCase() === lowerEmail);
|
|
89
|
+
}
|
|
79
90
|
/**
|
|
80
91
|
* Gets workspace by name (case-insensitive)
|
|
81
92
|
*/
|