@hailer/mcp 1.1.12 → 1.1.13
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 +0 -7
- package/{.claude → dist}/CLAUDE.md +2 -2
- package/dist/app.js +18 -5
- package/dist/bot/bot-config.d.ts +10 -1
- package/dist/bot/bot-config.js +64 -3
- package/dist/bot/bot-manager.d.ts +2 -0
- package/dist/bot/bot-manager.js +9 -2
- package/dist/bot/bot.d.ts +33 -0
- package/dist/bot/bot.js +461 -160
- package/dist/bot/services/message-classifier.js +17 -0
- package/dist/bot/services/permission-guard.d.ts +52 -0
- package/dist/bot/services/permission-guard.js +149 -0
- package/dist/bot/services/types.d.ts +5 -0
- package/dist/bot/services/typing-indicator.d.ts +6 -1
- package/dist/bot/services/typing-indicator.js +19 -3
- package/dist/cli.js +0 -0
- package/dist/config.d.ts +6 -1
- package/dist/config.js +43 -0
- package/dist/core.js +3 -6
- package/dist/lib/discussion-lock.d.ts +42 -0
- package/dist/lib/discussion-lock.js +110 -0
- package/dist/mcp/UserContextCache.d.ts +5 -0
- package/dist/mcp/UserContextCache.js +51 -19
- package/dist/mcp/hailer-clients.d.ts +19 -1
- package/dist/mcp/hailer-clients.js +158 -24
- package/dist/mcp/session-store.d.ts +68 -0
- package/dist/mcp/session-store.js +169 -0
- package/dist/mcp/signal-handler.js +2 -0
- package/dist/mcp/tool-registry.d.ts +17 -4
- package/dist/mcp/tool-registry.js +37 -7
- package/dist/mcp/tools/activity.js +99 -7
- package/dist/mcp/tools/app-scaffold.js +304 -336
- package/dist/mcp/tools/bot-config/constants.d.ts +23 -0
- package/dist/mcp/tools/bot-config/constants.js +94 -0
- package/dist/mcp/tools/bot-config/core.d.ts +253 -0
- package/dist/mcp/tools/bot-config/core.js +2456 -0
- package/dist/mcp/tools/bot-config/index.d.ts +10 -0
- package/dist/mcp/tools/bot-config/index.js +59 -0
- package/dist/mcp/tools/bot-config/tools.d.ts +7 -0
- package/dist/mcp/tools/bot-config/tools.js +15 -0
- package/dist/mcp/tools/bot-config/types.d.ts +50 -0
- package/dist/mcp/tools/bot-config/types.js +6 -0
- package/dist/mcp/tools/bug-fixer-tools.d.ts +45 -0
- package/dist/mcp/tools/bug-fixer-tools.js +1096 -0
- package/dist/mcp/tools/company.d.ts +9 -0
- package/dist/mcp/tools/company.js +88 -0
- package/dist/mcp/tools/discussion.js +68 -0
- package/dist/mcp/tools/document.d.ts +11 -0
- package/dist/mcp/tools/document.js +741 -0
- package/dist/mcp/tools/investigate.d.ts +9 -0
- package/dist/mcp/tools/investigate.js +254 -0
- package/dist/mcp/tools/workflow-permissions.d.ts +15 -0
- package/dist/mcp/tools/workflow-permissions.js +204 -0
- package/dist/mcp/tools/workflow.js +57 -18
- package/dist/mcp/utils/index.d.ts +2 -0
- package/dist/mcp/utils/index.js +12 -1
- package/dist/mcp/utils/role-utils.d.ts +74 -0
- package/dist/mcp/utils/role-utils.js +151 -0
- package/dist/mcp/utils/types.d.ts +43 -1
- package/dist/mcp/utils/types.js +14 -0
- package/dist/mcp/webhook-handler.d.ts +4 -0
- package/dist/mcp/webhook-handler.js +8 -0
- package/dist/mcp-server.d.ts +23 -2
- package/dist/mcp-server.js +639 -127
- package/dist/plugins/vipunen/client.d.ts +150 -0
- package/dist/plugins/vipunen/client.js +535 -0
- package/dist/plugins/vipunen/config/schema-config.json +19 -0
- package/dist/plugins/vipunen/config/schema-doc.json +22 -0
- package/dist/plugins/vipunen/index.d.ts +41 -0
- package/dist/plugins/vipunen/index.js +88 -0
- package/dist/plugins/vipunen/tools.d.ts +26 -0
- package/dist/plugins/vipunen/tools.js +501 -0
- package/dist/stdio-server.d.ts +14 -0
- package/dist/stdio-server.js +101 -0
- package/package.json +2 -1
- package/.claude/agents/agent-ada-skill-builder.md +0 -94
- package/.claude/agents/agent-alejandro-function-fields.md +0 -342
- package/.claude/agents/agent-bjorn-config-audit.md +0 -103
- package/.claude/agents/agent-builder-agent-creator.md +0 -130
- package/.claude/agents/agent-code-simplifier.md +0 -53
- package/.claude/agents/agent-dmitri-activity-crud.md +0 -159
- package/.claude/agents/agent-giuseppe-app-builder.md +0 -247
- package/.claude/agents/agent-gunther-mcp-tools.md +0 -39
- package/.claude/agents/agent-helga-workflow-config.md +0 -204
- package/.claude/agents/agent-igor-activity-mover-automation.md +0 -125
- package/.claude/agents/agent-ingrid-doc-templates.md +0 -261
- package/.claude/agents/agent-ivan-monolith.md +0 -154
- package/.claude/agents/agent-kenji-data-reader.md +0 -86
- package/.claude/agents/agent-lars-code-inspector.md +0 -102
- package/.claude/agents/agent-marco-mockup-builder.md +0 -110
- package/.claude/agents/agent-marcus-api-documenter.md +0 -323
- package/.claude/agents/agent-marketplace-publisher.md +0 -280
- package/.claude/agents/agent-marketplace-reviewer.md +0 -309
- package/.claude/agents/agent-permissions-handler.md +0 -208
- package/.claude/agents/agent-simple-writer.md +0 -48
- package/.claude/agents/agent-svetlana-code-review.md +0 -171
- package/.claude/agents/agent-tanya-test-runner.md +0 -333
- package/.claude/agents/agent-ui-designer.md +0 -100
- package/.claude/agents/agent-viktor-sql-insights.md +0 -212
- package/.claude/agents/agent-web-search.md +0 -55
- package/.claude/agents/agent-yevgeni-discussions.md +0 -45
- package/.claude/agents/agent-zara-zapier.md +0 -159
- package/.claude/commands/app-squad.md +0 -135
- package/.claude/commands/audit-squad.md +0 -158
- package/.claude/commands/autoplan.md +0 -563
- package/.claude/commands/cleanup-squad.md +0 -98
- package/.claude/commands/config-squad.md +0 -106
- package/.claude/commands/crud-squad.md +0 -87
- package/.claude/commands/data-squad.md +0 -97
- package/.claude/commands/debug-squad.md +0 -303
- package/.claude/commands/doc-squad.md +0 -65
- package/.claude/commands/handoff.md +0 -137
- package/.claude/commands/health.md +0 -49
- package/.claude/commands/help.md +0 -29
- package/.claude/commands/help:agents.md +0 -151
- package/.claude/commands/help:commands.md +0 -78
- package/.claude/commands/help:faq.md +0 -79
- package/.claude/commands/help:plugins.md +0 -50
- package/.claude/commands/help:skills.md +0 -93
- package/.claude/commands/help:tools.md +0 -75
- package/.claude/commands/hotfix-squad.md +0 -112
- package/.claude/commands/integration-squad.md +0 -82
- package/.claude/commands/janitor-squad.md +0 -167
- package/.claude/commands/learn-auto.md +0 -120
- package/.claude/commands/learn.md +0 -120
- package/.claude/commands/mcp-list.md +0 -27
- package/.claude/commands/onboard-squad.md +0 -140
- package/.claude/commands/plan-workspace.md +0 -732
- package/.claude/commands/prd.md +0 -130
- package/.claude/commands/project-status.md +0 -82
- package/.claude/commands/publish.md +0 -138
- package/.claude/commands/recap.md +0 -69
- package/.claude/commands/restore.md +0 -64
- package/.claude/commands/review-squad.md +0 -152
- package/.claude/commands/save.md +0 -24
- package/.claude/commands/stats.md +0 -19
- package/.claude/commands/swarm.md +0 -210
- package/.claude/commands/tool-builder.md +0 -39
- package/.claude/commands/ws-pull.md +0 -44
- package/.claude/hooks/_shared-memory.cjs +0 -305
- package/.claude/hooks/_utils.cjs +0 -108
- package/.claude/hooks/agent-failure-detector.cjs +0 -383
- package/.claude/hooks/agent-usage-logger.cjs +0 -204
- package/.claude/hooks/app-edit-guard.cjs +0 -494
- package/.claude/hooks/auto-learn.cjs +0 -304
- package/.claude/hooks/bash-guard.cjs +0 -272
- package/.claude/hooks/builder-mode-manager.cjs +0 -354
- package/.claude/hooks/bulk-activity-guard.cjs +0 -271
- package/.claude/hooks/context-watchdog.cjs +0 -230
- package/.claude/hooks/delegation-reminder.cjs +0 -465
- package/.claude/hooks/design-system-lint.cjs +0 -271
- package/.claude/hooks/post-scaffold-hook.cjs +0 -181
- package/.claude/hooks/prompt-guard.cjs +0 -354
- package/.claude/hooks/publish-template-guard.cjs +0 -147
- package/.claude/hooks/session-start.cjs +0 -35
- package/.claude/hooks/shared-memory-writer.cjs +0 -147
- package/.claude/hooks/skill-injector.cjs +0 -140
- package/.claude/hooks/skill-usage-logger.cjs +0 -258
- package/.claude/hooks/src-edit-guard.cjs +0 -240
- package/.claude/hooks/sync-marketplace-agents.cjs +0 -346
- package/.claude/settings.json +0 -257
- package/.claude/skills/SDK-activity-patterns/SKILL.md +0 -428
- package/.claude/skills/SDK-document-templates/SKILL.md +0 -1033
- package/.claude/skills/SDK-function-fields/SKILL.md +0 -542
- package/.claude/skills/SDK-generate-skill/SKILL.md +0 -92
- package/.claude/skills/SDK-init-skill/SKILL.md +0 -127
- package/.claude/skills/SDK-insight-queries/SKILL.md +0 -787
- package/.claude/skills/SDK-ws-config-skill/SKILL.md +0 -1139
- package/.claude/skills/agent-structure/SKILL.md +0 -98
- package/.claude/skills/api-documentation-patterns/SKILL.md +0 -474
- package/.claude/skills/chrome-mcp-reference/SKILL.md +0 -370
- package/.claude/skills/delegation-routing/SKILL.md +0 -202
- package/.claude/skills/frontend-design/SKILL.md +0 -254
- package/.claude/skills/hailer-activity-mover/SKILL.md +0 -213
- package/.claude/skills/hailer-api-client/SKILL.md +0 -518
- package/.claude/skills/hailer-app-builder/SKILL.md +0 -1434
- package/.claude/skills/hailer-apps-pictures/SKILL.md +0 -269
- package/.claude/skills/hailer-design-system/SKILL.md +0 -235
- package/.claude/skills/hailer-monolith-automations/SKILL.md +0 -686
- package/.claude/skills/hailer-permissions-system/SKILL.md +0 -121
- package/.claude/skills/hailer-project-protocol/SKILL.md +0 -488
- package/.claude/skills/hailer-rest-api/SKILL.md +0 -61
- package/.claude/skills/hailer-rest-api/hailer-activities.md +0 -184
- package/.claude/skills/hailer-rest-api/hailer-admin.md +0 -473
- package/.claude/skills/hailer-rest-api/hailer-calendar.md +0 -256
- package/.claude/skills/hailer-rest-api/hailer-feed.md +0 -249
- package/.claude/skills/hailer-rest-api/hailer-insights.md +0 -195
- package/.claude/skills/hailer-rest-api/hailer-messaging.md +0 -276
- package/.claude/skills/hailer-rest-api/hailer-workflows.md +0 -283
- package/.claude/skills/insight-join-patterns/SKILL.md +0 -174
- package/.claude/skills/integration-patterns/SKILL.md +0 -421
- package/.claude/skills/json-only-output/SKILL.md +0 -72
- package/.claude/skills/lsp-setup/SKILL.md +0 -160
- package/.claude/skills/mcp-direct-tools/SKILL.md +0 -153
- package/.claude/skills/optional-parameters/SKILL.md +0 -72
- package/.claude/skills/publish-hailer-app/SKILL.md +0 -244
- package/.claude/skills/testing-patterns/SKILL.md +0 -630
- package/.claude/skills/tool-builder/SKILL.md +0 -250
- package/.claude/skills/tool-parameter-usage/SKILL.md +0 -126
- package/.claude/skills/tool-response-verification/SKILL.md +0 -92
- package/.claude/skills/zapier-hailer-patterns/SKILL.md +0 -581
- package/.mcp.json +0 -13
- package/.opencode/agent/agent-ada-skill-builder.md +0 -35
- package/.opencode/agent/agent-alejandro-function-fields.md +0 -39
- package/.opencode/agent/agent-bjorn-config-audit.md +0 -36
- package/.opencode/agent/agent-builder-agent-creator.md +0 -39
- package/.opencode/agent/agent-code-simplifier.md +0 -31
- package/.opencode/agent/agent-dmitri-activity-crud.md +0 -40
- package/.opencode/agent/agent-giuseppe-app-builder.md +0 -37
- package/.opencode/agent/agent-gunther-mcp-tools.md +0 -39
- package/.opencode/agent/agent-helga-workflow-config.md +0 -203
- package/.opencode/agent/agent-igor-activity-mover-automation.md +0 -46
- package/.opencode/agent/agent-ingrid-doc-templates.md +0 -39
- package/.opencode/agent/agent-ivan-monolith.md +0 -46
- package/.opencode/agent/agent-kenji-data-reader.md +0 -53
- package/.opencode/agent/agent-lars-code-inspector.md +0 -28
- package/.opencode/agent/agent-marco-mockup-builder.md +0 -42
- package/.opencode/agent/agent-marcus-api-documenter.md +0 -53
- package/.opencode/agent/agent-marketplace-publisher.md +0 -44
- package/.opencode/agent/agent-marketplace-reviewer.md +0 -42
- package/.opencode/agent/agent-permissions-handler.md +0 -50
- package/.opencode/agent/agent-simple-writer.md +0 -45
- package/.opencode/agent/agent-svetlana-code-review.md +0 -39
- package/.opencode/agent/agent-tanya-test-runner.md +0 -57
- package/.opencode/agent/agent-ui-designer.md +0 -56
- package/.opencode/agent/agent-viktor-sql-insights.md +0 -34
- package/.opencode/agent/agent-web-search.md +0 -42
- package/.opencode/agent/agent-yevgeni-discussions.md +0 -37
- package/.opencode/agent/agent-zara-zapier.md +0 -53
- package/.opencode/commands/app-squad.md +0 -135
- package/.opencode/commands/audit-squad.md +0 -158
- package/.opencode/commands/autoplan.md +0 -563
- package/.opencode/commands/cleanup-squad.md +0 -98
- package/.opencode/commands/config-squad.md +0 -106
- package/.opencode/commands/crud-squad.md +0 -87
- package/.opencode/commands/data-squad.md +0 -97
- package/.opencode/commands/debug-squad.md +0 -303
- package/.opencode/commands/doc-squad.md +0 -65
- package/.opencode/commands/handoff.md +0 -137
- package/.opencode/commands/health.md +0 -49
- package/.opencode/commands/help-agents.md +0 -151
- package/.opencode/commands/help-commands.md +0 -32
- package/.opencode/commands/help-faq.md +0 -29
- package/.opencode/commands/help-plugins.md +0 -28
- package/.opencode/commands/help-skills.md +0 -7
- package/.opencode/commands/help-tools.md +0 -40
- package/.opencode/commands/help.md +0 -28
- package/.opencode/commands/hotfix-squad.md +0 -112
- package/.opencode/commands/integration-squad.md +0 -82
- package/.opencode/commands/janitor-squad.md +0 -167
- package/.opencode/commands/learn-auto.md +0 -120
- package/.opencode/commands/learn.md +0 -120
- package/.opencode/commands/mcp-list.md +0 -27
- package/.opencode/commands/onboard-squad.md +0 -140
- package/.opencode/commands/plan-workspace.md +0 -732
- package/.opencode/commands/prd.md +0 -131
- package/.opencode/commands/project-status.md +0 -82
- package/.opencode/commands/publish.md +0 -138
- package/.opencode/commands/recap.md +0 -69
- package/.opencode/commands/restore.md +0 -64
- package/.opencode/commands/review-squad.md +0 -152
- package/.opencode/commands/save.md +0 -24
- package/.opencode/commands/stats.md +0 -19
- package/.opencode/commands/swarm.md +0 -210
- package/.opencode/commands/tool-builder.md +0 -39
- package/.opencode/commands/ws-pull.md +0 -44
- package/.opencode/opencode.json +0 -28
- package/SESSION-HANDOFF.md +0 -68
- package/inbox/2026-03-04-bot-config-patterns.md +0 -24
- package/scripts/postinstall.cjs +0 -64
- package/scripts/test-hal-tools.ts +0 -154
|
@@ -6,6 +6,7 @@ const workspace_cache_1 = require("./workspace-cache");
|
|
|
6
6
|
const config_1 = require("../config");
|
|
7
7
|
const logger_1 = require("../lib/logger");
|
|
8
8
|
const index_1 = require("./utils/index");
|
|
9
|
+
const index_2 = require("./utils/index");
|
|
9
10
|
const logger = (0, logger_1.createLogger)({ component: 'user-context-cache' });
|
|
10
11
|
/**
|
|
11
12
|
* Cache for user-specific data (client connections, init data, workspace cache)
|
|
@@ -57,7 +58,7 @@ class UserContextCache {
|
|
|
57
58
|
else {
|
|
58
59
|
// Cache expired, remove stale entry
|
|
59
60
|
this.cache.delete(apiKey);
|
|
60
|
-
logger.
|
|
61
|
+
logger.info('Cache expired, refreshing user context', {
|
|
61
62
|
apiKey: apiKey.substring(0, 8) + '...',
|
|
62
63
|
ageMinutes: Math.round(age / (1000 * 60))
|
|
63
64
|
});
|
|
@@ -66,7 +67,7 @@ class UserContextCache {
|
|
|
66
67
|
else if (forceRefresh && this.cache.has(apiKey)) {
|
|
67
68
|
// Force refresh requested, clear existing cache
|
|
68
69
|
this.cache.delete(apiKey);
|
|
69
|
-
logger.
|
|
70
|
+
logger.info('Force refresh requested, clearing cached user context', {
|
|
70
71
|
apiKey: apiKey.substring(0, 8) + '...'
|
|
71
72
|
});
|
|
72
73
|
}
|
|
@@ -105,29 +106,57 @@ class UserContextCache {
|
|
|
105
106
|
const init = await client.socket.request('v2.core.init', [
|
|
106
107
|
['processes', 'users', 'network', 'networks', 'teams']
|
|
107
108
|
]);
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
(0, index_1.normalizeInitProcesses)(init);
|
|
110
|
+
// Multi-workspace support: log available workspaces, use first one as default
|
|
111
|
+
// User can switch workspaces using list_my_workspaces tool and specifying workspaceId
|
|
112
|
+
const networks = (init.networks || {});
|
|
113
|
+
const workspaceCount = Object.keys(networks).length;
|
|
110
114
|
if (workspaceCount > 1) {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
.map((ws) => ws.name)
|
|
115
|
+
const workspaceList = Object.entries(networks)
|
|
116
|
+
.map(([id, ws]) => `${ws.name} (${id})`)
|
|
114
117
|
.join(', ');
|
|
115
|
-
logger.
|
|
118
|
+
logger.info('Multi-workspace user detected', {
|
|
116
119
|
workspaceCount,
|
|
117
|
-
workspaces:
|
|
118
|
-
apiKey: apiKey.substring(0, 8) + '...'
|
|
120
|
+
workspaces: workspaceList,
|
|
121
|
+
apiKey: apiKey.substring(0, 8) + '...',
|
|
122
|
+
hint: 'Use list_my_workspaces tool to see all workspaces and switch between them'
|
|
119
123
|
});
|
|
120
|
-
// Clean up the connection before throwing - prevents dangling socket
|
|
121
|
-
(0, hailer_clients_1.disconnectHailerClientByApiKey)(apiKey);
|
|
122
|
-
throw new Error(`Multi-workspace credentials detected (${workspaceCount} workspaces: ${workspaceNames}). ` +
|
|
123
|
-
`MCP requires bot credentials with access to a single workspace. ` +
|
|
124
|
-
`Please use the bot account created during 'hailer-sdk init'.`);
|
|
125
124
|
}
|
|
126
125
|
// Create workspace cache from init data
|
|
127
126
|
const appConfig = (0, config_1.createApplicationConfig)();
|
|
128
127
|
const workspaceCache = (0, workspace_cache_1.createWorkspaceCache)(init, appConfig.mcpConfig);
|
|
128
|
+
// Extract user roles from ALL workspaces
|
|
129
|
+
const currentUserId = await (0, hailer_clients_1.getCurrentUserId)(client);
|
|
130
|
+
const workspaceRoles = (0, index_2.extractWorkspaceRoles)(networks, currentUserId);
|
|
131
|
+
// Get current workspace ID
|
|
132
|
+
const currentWorkspaceId = init.network?._id || Object.keys(networks)[0] || '';
|
|
133
|
+
// Get role for current workspace (for backward compatibility and initial tool filtering)
|
|
134
|
+
const userRole = workspaceRoles[currentWorkspaceId] || 'guest';
|
|
135
|
+
const allowedGroups = (0, index_2.getAllowedGroups)(userRole, config_1.environment.ENABLE_NUCLEAR_TOOLS);
|
|
136
|
+
logger.info('User roles extracted from all workspaces', {
|
|
137
|
+
apiKey: apiKey.substring(0, 8) + '...',
|
|
138
|
+
workspaceCount: Object.keys(workspaceRoles).length,
|
|
139
|
+
currentWorkspaceId,
|
|
140
|
+
currentRole: userRole,
|
|
141
|
+
allRoles: Object.entries(workspaceRoles).map(([id, role]) => `${id.slice(-6)}:${role}`).join(', ')
|
|
142
|
+
});
|
|
129
143
|
// Get credentials from config (for tools like publish_hailer_app that need external auth)
|
|
130
|
-
|
|
144
|
+
// User API Keys (OAuth users) don't have bot credentials - use empty strings
|
|
145
|
+
const isUserApiKey = apiKey.startsWith('userapikey_');
|
|
146
|
+
let email = '';
|
|
147
|
+
let password = '';
|
|
148
|
+
if (!isUserApiKey) {
|
|
149
|
+
try {
|
|
150
|
+
const accountConfig = appConfig.getClientConfig(apiKey);
|
|
151
|
+
email = accountConfig.email;
|
|
152
|
+
password = accountConfig.password;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
logger.warn('No client config found for API key, using empty credentials', {
|
|
156
|
+
apiKey: apiKey.substring(0, 8) + '...'
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
131
160
|
const context = {
|
|
132
161
|
client,
|
|
133
162
|
hailer,
|
|
@@ -135,8 +164,11 @@ class UserContextCache {
|
|
|
135
164
|
workspaceCache,
|
|
136
165
|
apiKey,
|
|
137
166
|
createdAt: Date.now(),
|
|
138
|
-
email
|
|
139
|
-
password
|
|
167
|
+
email,
|
|
168
|
+
password,
|
|
169
|
+
workspaceRoles, // NEW: Map of workspaceId → role
|
|
170
|
+
currentWorkspaceId, // NEW: Current workspace ID
|
|
171
|
+
allowedGroups, // Keep for stdio-server compatibility
|
|
140
172
|
};
|
|
141
173
|
// Calculate and log cache sizes
|
|
142
174
|
const rawInitSize = Buffer.byteLength(JSON.stringify(init), 'utf8');
|
|
@@ -176,7 +208,7 @@ class UserContextCache {
|
|
|
176
208
|
static clearAll() {
|
|
177
209
|
const size = this.cache.size;
|
|
178
210
|
this.cache.clear();
|
|
179
|
-
logger.
|
|
211
|
+
logger.info('Cleared all user contexts from cache', { clearedCount: size });
|
|
180
212
|
}
|
|
181
213
|
/**
|
|
182
214
|
* Get cache statistics (for monitoring and debugging)
|
|
@@ -5,10 +5,28 @@ export interface HailerRestClient {
|
|
|
5
5
|
sessionKey: string;
|
|
6
6
|
}
|
|
7
7
|
export interface HailerClient {
|
|
8
|
-
socket: Client;
|
|
8
|
+
socket: Client | RestOnlySocket;
|
|
9
9
|
rest: HailerRestClient;
|
|
10
10
|
sessionKey: string;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* REST-only socket mock for User API Key sessions
|
|
14
|
+
* User API Keys require IP for validation, which socket.resume() doesn't provide.
|
|
15
|
+
* This mock implements the socket.request() interface using REST API calls.
|
|
16
|
+
*/
|
|
17
|
+
export declare class RestOnlySocket {
|
|
18
|
+
host: string;
|
|
19
|
+
private apiKey;
|
|
20
|
+
constructor(baseUrl: string, apiKey: string);
|
|
21
|
+
/**
|
|
22
|
+
* Make RPC request via REST API instead of socket
|
|
23
|
+
* Hailer's /api endpoint accepts the same RPC format as socket
|
|
24
|
+
*/
|
|
25
|
+
request(method: string, args?: unknown[]): Promise<unknown>;
|
|
26
|
+
on(_event: string, _handler: (...args: any[]) => void): void;
|
|
27
|
+
off(_event: string, _handler: (...args: any[]) => void): void;
|
|
28
|
+
disconnect(): void;
|
|
29
|
+
}
|
|
12
30
|
/**
|
|
13
31
|
* Get the current user ID from the authenticated session
|
|
14
32
|
* This is called after authentication to retrieve the user ID automatically
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.subscribeToSignal = exports.clearAllConnections = exports.disconnectHailerClientByApiKey = exports.createHailerClientByApiKey = exports.HailerClientManager = void 0;
|
|
3
|
+
exports.subscribeToSignal = exports.clearAllConnections = exports.disconnectHailerClientByApiKey = exports.createHailerClientByApiKey = exports.HailerClientManager = exports.RestOnlySocket = void 0;
|
|
4
4
|
exports.getCurrentUserId = getCurrentUserId;
|
|
5
5
|
exports.registerBotCredentials = registerBotCredentials;
|
|
6
6
|
exports.unregisterBotCredentials = unregisterBotCredentials;
|
|
@@ -9,6 +9,70 @@ const auth_1 = require("./auth");
|
|
|
9
9
|
const logger_1 = require("../lib/logger");
|
|
10
10
|
const config_1 = require("../config");
|
|
11
11
|
const logger = (0, logger_1.createLogger)({ component: 'hailer-clients' });
|
|
12
|
+
// Default API base URL for OAuth sessions (User API Keys)
|
|
13
|
+
const DEFAULT_API_BASE_URL = process.env.BOT_API_BASE_URL || 'https://api.hailer.com';
|
|
14
|
+
/**
|
|
15
|
+
* REST-only socket mock for User API Key sessions
|
|
16
|
+
* User API Keys require IP for validation, which socket.resume() doesn't provide.
|
|
17
|
+
* This mock implements the socket.request() interface using REST API calls.
|
|
18
|
+
*/
|
|
19
|
+
class RestOnlySocket {
|
|
20
|
+
host;
|
|
21
|
+
apiKey;
|
|
22
|
+
constructor(baseUrl, apiKey) {
|
|
23
|
+
this.host = baseUrl;
|
|
24
|
+
this.apiKey = apiKey;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Make RPC request via REST API instead of socket
|
|
28
|
+
* Hailer's /api endpoint accepts the same RPC format as socket
|
|
29
|
+
*/
|
|
30
|
+
async request(method, args = []) {
|
|
31
|
+
const url = `${this.host}/api/${method.replace(/\./g, '/')}`;
|
|
32
|
+
logger.debug('RestOnlySocket making request', {
|
|
33
|
+
url,
|
|
34
|
+
method,
|
|
35
|
+
apiKeyPrefix: this.apiKey.substring(0, 20) + '...',
|
|
36
|
+
});
|
|
37
|
+
const response = await (0, auth_1.safeFetch)(url, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
'hlrkey': this.apiKey,
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({ args }),
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const errorText = await response.text();
|
|
47
|
+
logger.error('RestOnlySocket request failed', {
|
|
48
|
+
url,
|
|
49
|
+
method,
|
|
50
|
+
status: response.status,
|
|
51
|
+
errorText,
|
|
52
|
+
});
|
|
53
|
+
throw new Error(`REST API request failed: ${response.status} ${errorText}`);
|
|
54
|
+
}
|
|
55
|
+
const result = await response.json();
|
|
56
|
+
logger.debug('RestOnlySocket request succeeded', { method });
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
// Stub methods for socket interface compatibility
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
on(_event, _handler) {
|
|
62
|
+
// REST-only client doesn't support real-time events
|
|
63
|
+
logger.debug('RestOnlySocket.on() called - events not supported for User API Key sessions');
|
|
64
|
+
}
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
|
+
off(_event, _handler) {
|
|
67
|
+
// REST-only client doesn't support real-time events
|
|
68
|
+
logger.debug('RestOnlySocket.off() called - events not supported for User API Key sessions');
|
|
69
|
+
}
|
|
70
|
+
disconnect() {
|
|
71
|
+
// Nothing to disconnect for REST-only client
|
|
72
|
+
logger.debug('RestOnlySocket.disconnect() called - no-op for REST client');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.RestOnlySocket = RestOnlySocket;
|
|
12
76
|
/**
|
|
13
77
|
* Get the current user ID from the authenticated session
|
|
14
78
|
* This is called after authentication to retrieve the user ID automatically
|
|
@@ -91,24 +155,23 @@ class HailerClientManager {
|
|
|
91
155
|
password: this.password,
|
|
92
156
|
...(isLocalDev && { rejectUnauthorized: false }), // Add for local dev with self-signed certs
|
|
93
157
|
};
|
|
94
|
-
// Track timeout
|
|
158
|
+
// Track timeout for proper cleanup
|
|
95
159
|
let timeoutId = null;
|
|
96
|
-
let
|
|
160
|
+
let timeoutCleared = false;
|
|
97
161
|
try {
|
|
98
162
|
// Create socket client using @hailer/cli with cancellable timeout
|
|
99
163
|
this.socketClient = (await Promise.race([
|
|
100
164
|
cli_1.Client.create(clientOptions).then(client => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
165
|
+
// Clear timeout on success
|
|
166
|
+
if (timeoutId && !timeoutCleared) {
|
|
167
|
+
clearTimeout(timeoutId);
|
|
168
|
+
timeoutCleared = true;
|
|
105
169
|
}
|
|
106
170
|
return client;
|
|
107
171
|
}),
|
|
108
172
|
new Promise((_, reject) => {
|
|
109
173
|
timeoutId = setTimeout(() => {
|
|
110
|
-
if (!
|
|
111
|
-
settled = true;
|
|
174
|
+
if (!timeoutCleared) {
|
|
112
175
|
reject(new Error(`Timeout connecting to: ${this.host}`));
|
|
113
176
|
}
|
|
114
177
|
}, 30000);
|
|
@@ -116,10 +179,10 @@ class HailerClientManager {
|
|
|
116
179
|
]));
|
|
117
180
|
}
|
|
118
181
|
catch (error) {
|
|
119
|
-
// Ensure timeout is cleared on error
|
|
120
|
-
if (timeoutId && !
|
|
121
|
-
settled = true;
|
|
182
|
+
// Ensure timeout is cleared on error too
|
|
183
|
+
if (timeoutId && !timeoutCleared) {
|
|
122
184
|
clearTimeout(timeoutId);
|
|
185
|
+
timeoutCleared = true;
|
|
123
186
|
}
|
|
124
187
|
logger.error('Failed to create socket client', error, { username: this.username });
|
|
125
188
|
throw error;
|
|
@@ -163,13 +226,13 @@ class HailerClientManager {
|
|
|
163
226
|
logger.info('Socket reconnected and session resumed', { username: this.username });
|
|
164
227
|
});
|
|
165
228
|
this.socketClient.on("connect", () => {
|
|
166
|
-
logger.
|
|
229
|
+
logger.info('Socket connected', { username: this.username });
|
|
167
230
|
});
|
|
168
231
|
this.socketClient.on("connect_error", (error) => {
|
|
169
232
|
logger.error('Socket connection error', { error: error.message, username: this.username });
|
|
170
233
|
});
|
|
171
234
|
this.socketClient.on("reconnect_attempt", (attempt) => {
|
|
172
|
-
logger.
|
|
235
|
+
logger.info('Socket reconnection attempt', { attempt, username: this.username });
|
|
173
236
|
});
|
|
174
237
|
this.socketClient.on("reconnect_failed", () => {
|
|
175
238
|
logger.error('Socket reconnection failed permanently', { username: this.username });
|
|
@@ -204,10 +267,7 @@ class HailerClientManager {
|
|
|
204
267
|
if (!this.signalHandlers.has(eventType)) {
|
|
205
268
|
this.signalHandlers.set(eventType, []);
|
|
206
269
|
}
|
|
207
|
-
|
|
208
|
-
if (!handlers.includes(handler)) {
|
|
209
|
-
handlers.push(handler);
|
|
210
|
-
}
|
|
270
|
+
this.signalHandlers.get(eventType).push(handler);
|
|
211
271
|
}
|
|
212
272
|
// Remove a signal handler
|
|
213
273
|
offSignal(eventType, handler) {
|
|
@@ -247,12 +307,86 @@ class HailerClientManager {
|
|
|
247
307
|
exports.HailerClientManager = HailerClientManager;
|
|
248
308
|
// Connection pool for the MCP server
|
|
249
309
|
const connectionPool = new Map();
|
|
310
|
+
// Pool for User API Key socket clients
|
|
311
|
+
const userApiKeyClients = new Map();
|
|
312
|
+
/**
|
|
313
|
+
* Create a REST-only client for User API Keys
|
|
314
|
+
*
|
|
315
|
+
* User API Keys require IP address for validation (see validateApiKey in backend).
|
|
316
|
+
* Socket.IO resume() doesn't pass IP, so we use REST API with hlrkey header instead.
|
|
317
|
+
* The REST API passes req.ip to core.getSession() which then validates the key.
|
|
318
|
+
*/
|
|
319
|
+
async function createUserApiKeyClient(userApiKey) {
|
|
320
|
+
// Check if we already have this client
|
|
321
|
+
const existing = userApiKeyClients.get(userApiKey);
|
|
322
|
+
if (existing) {
|
|
323
|
+
logger.debug('Reusing existing User API Key client', {
|
|
324
|
+
apiKey: userApiKey.substring(0, 20) + '...'
|
|
325
|
+
});
|
|
326
|
+
return existing;
|
|
327
|
+
}
|
|
328
|
+
logger.info('Creating REST-only client for User API Key', {
|
|
329
|
+
apiKey: userApiKey.substring(0, 20) + '...',
|
|
330
|
+
apiBaseUrl: DEFAULT_API_BASE_URL
|
|
331
|
+
});
|
|
332
|
+
try {
|
|
333
|
+
// Create REST-only socket mock that uses HTTP API instead of WebSocket
|
|
334
|
+
const restSocket = new RestOnlySocket(DEFAULT_API_BASE_URL, userApiKey);
|
|
335
|
+
// Verify the API key works by making a test request
|
|
336
|
+
logger.debug('Verifying User API Key via REST...');
|
|
337
|
+
const testResult = await restSocket.request('v2.core.init', [['user']]);
|
|
338
|
+
if (!testResult?.user?._id) {
|
|
339
|
+
throw new Error('User API Key validation failed - could not get user info');
|
|
340
|
+
}
|
|
341
|
+
logger.info('User API Key validated successfully via REST', {
|
|
342
|
+
apiKey: userApiKey.substring(0, 20) + '...',
|
|
343
|
+
userId: testResult.user._id
|
|
344
|
+
});
|
|
345
|
+
// Create REST client
|
|
346
|
+
const restClient = {
|
|
347
|
+
fetch: (url, options = {}) => {
|
|
348
|
+
const fullUrl = url.startsWith('http') ? url : `${DEFAULT_API_BASE_URL}${url}`;
|
|
349
|
+
const headers = {
|
|
350
|
+
...options.headers,
|
|
351
|
+
hlrkey: userApiKey,
|
|
352
|
+
'Content-Type': 'application/json',
|
|
353
|
+
};
|
|
354
|
+
return (0, auth_1.safeFetch)(fullUrl, { ...options, headers });
|
|
355
|
+
},
|
|
356
|
+
sessionKey: userApiKey,
|
|
357
|
+
};
|
|
358
|
+
const client = {
|
|
359
|
+
socket: restSocket,
|
|
360
|
+
rest: restClient,
|
|
361
|
+
sessionKey: userApiKey,
|
|
362
|
+
};
|
|
363
|
+
userApiKeyClients.set(userApiKey, client);
|
|
364
|
+
return client;
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
const errorMsg = error instanceof Error
|
|
368
|
+
? error.message
|
|
369
|
+
: (typeof error === 'object' && error !== null)
|
|
370
|
+
? JSON.stringify(error)
|
|
371
|
+
: String(error);
|
|
372
|
+
logger.error('Failed to create User API Key client', {
|
|
373
|
+
error: errorMsg,
|
|
374
|
+
errorType: typeof error,
|
|
375
|
+
});
|
|
376
|
+
throw new Error(`Failed to create User API Key session: ${errorMsg}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
250
379
|
/**
|
|
251
380
|
* Create Hailer client by API key (O(1) lookup)
|
|
252
381
|
* This is the new efficient method for MCP Server to get connections
|
|
253
382
|
*/
|
|
254
383
|
const createHailerClientByApiKey = async (apiKey) => {
|
|
255
|
-
//
|
|
384
|
+
// Check if this is a User API Key (from OAuth flow)
|
|
385
|
+
// User API Keys start with "userapikey_" and can authenticate via socket resume()
|
|
386
|
+
if (apiKey.startsWith('userapikey_')) {
|
|
387
|
+
return await createUserApiKeyClient(apiKey);
|
|
388
|
+
}
|
|
389
|
+
// O(1) lookup in Map-based CLIENT_CONFIGS (for bot accounts)
|
|
256
390
|
const account = config_1.environment.CLIENT_CONFIGS[apiKey];
|
|
257
391
|
if (!account) {
|
|
258
392
|
throw new Error(`No Hailer account found for API key: ${apiKey}`);
|
|
@@ -283,7 +417,7 @@ const createHailerClientByApiKey = async (apiKey) => {
|
|
|
283
417
|
connectionPool.delete(connectionKey);
|
|
284
418
|
}
|
|
285
419
|
// Create new connection
|
|
286
|
-
logger.
|
|
420
|
+
logger.info('Creating new connection', {
|
|
287
421
|
apiKey: apiKey.substring(0, 8) + '...',
|
|
288
422
|
email: (0, config_1.maskEmail)(account.email),
|
|
289
423
|
host: account.apiBaseUrl
|
|
@@ -315,7 +449,7 @@ const clearAllConnections = () => {
|
|
|
315
449
|
clientManager.disconnect();
|
|
316
450
|
}
|
|
317
451
|
connectionPool.clear();
|
|
318
|
-
logger.
|
|
452
|
+
logger.info('Cleared Hailer connections', { count });
|
|
319
453
|
};
|
|
320
454
|
exports.clearAllConnections = clearAllConnections;
|
|
321
455
|
/**
|
|
@@ -349,10 +483,10 @@ function registerBotCredentials(botId, email, password, options) {
|
|
|
349
483
|
config_1.environment.CLIENT_CONFIGS[apiKey] = {
|
|
350
484
|
email,
|
|
351
485
|
password,
|
|
352
|
-
apiBaseUrl:
|
|
486
|
+
apiBaseUrl: 'https://api.hailer.com',
|
|
353
487
|
...(options?.allowedGroups && { allowedGroups: options.allowedGroups }),
|
|
354
488
|
};
|
|
355
|
-
logger.
|
|
489
|
+
logger.info('Bot credentials registered', {
|
|
356
490
|
botId,
|
|
357
491
|
hasAllowedGroups: !!options?.allowedGroups
|
|
358
492
|
});
|
|
@@ -364,6 +498,6 @@ function registerBotCredentials(botId, email, password, options) {
|
|
|
364
498
|
function unregisterBotCredentials(apiKey) {
|
|
365
499
|
delete config_1.environment.CLIENT_CONFIGS[apiKey];
|
|
366
500
|
(0, exports.disconnectHailerClientByApiKey)(apiKey);
|
|
367
|
-
logger.
|
|
501
|
+
logger.info('Bot credentials unregistered');
|
|
368
502
|
}
|
|
369
503
|
//# sourceMappingURL=hailer-clients.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionStore - Maps key_id to real api_key for Claude App authentication
|
|
3
|
+
*
|
|
4
|
+
* Purpose: Keep real Hailer API keys on server side, Claude only gets a reference (key_id).
|
|
5
|
+
* Multiple concurrent sessions supported - each user gets their own key_id.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. User logs in via Hailer auth page
|
|
9
|
+
* 2. Frontend generates key_id, sends {key_id, api_key} to MCP /auth/register
|
|
10
|
+
* 3. Frontend redirects to Claude with key_id as "access_token"
|
|
11
|
+
* 4. Claude sends key_id in Authorization header
|
|
12
|
+
* 5. MCP looks up real api_key from this store
|
|
13
|
+
*/
|
|
14
|
+
export interface Session {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
workspaceId: string;
|
|
17
|
+
createdAt: number;
|
|
18
|
+
lastAccessedAt: number;
|
|
19
|
+
}
|
|
20
|
+
export declare class SessionStore {
|
|
21
|
+
private store;
|
|
22
|
+
private maxTtlMs;
|
|
23
|
+
private inactivityTtlMs;
|
|
24
|
+
private cleanupInterval;
|
|
25
|
+
/**
|
|
26
|
+
* @param maxTtlHours - Maximum session lifetime in hours (default 24)
|
|
27
|
+
* @param inactivityMinutes - Session expires after this many minutes of inactivity (default 30)
|
|
28
|
+
*/
|
|
29
|
+
constructor(maxTtlHours?: number, inactivityMinutes?: number);
|
|
30
|
+
/**
|
|
31
|
+
* Register a new session (called by /auth/register endpoint)
|
|
32
|
+
*/
|
|
33
|
+
set(keyId: string, apiKey: string, workspaceId: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Get session by key_id (returns null if expired or not found)
|
|
36
|
+
* Updates lastAccessedAt to extend inactivity timeout
|
|
37
|
+
*/
|
|
38
|
+
get(keyId: string): Session | null;
|
|
39
|
+
/**
|
|
40
|
+
* Delete session (logout)
|
|
41
|
+
*/
|
|
42
|
+
delete(keyId: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Remove all expired sessions (both max TTL and inactivity)
|
|
45
|
+
*/
|
|
46
|
+
cleanup(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Get store statistics
|
|
49
|
+
*/
|
|
50
|
+
getStats(): {
|
|
51
|
+
size: number;
|
|
52
|
+
config: {
|
|
53
|
+
maxTtlHours: number;
|
|
54
|
+
inactivityMinutes: number;
|
|
55
|
+
};
|
|
56
|
+
sessions: Array<{
|
|
57
|
+
keyId: string;
|
|
58
|
+
ageMinutes: number;
|
|
59
|
+
inactivityMinutes: number;
|
|
60
|
+
}>;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Stop cleanup interval (for graceful shutdown)
|
|
64
|
+
*/
|
|
65
|
+
destroy(): void;
|
|
66
|
+
}
|
|
67
|
+
export declare const sessionStore: SessionStore;
|
|
68
|
+
//# sourceMappingURL=session-store.d.ts.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SessionStore - Maps key_id to real api_key for Claude App authentication
|
|
4
|
+
*
|
|
5
|
+
* Purpose: Keep real Hailer API keys on server side, Claude only gets a reference (key_id).
|
|
6
|
+
* Multiple concurrent sessions supported - each user gets their own key_id.
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* 1. User logs in via Hailer auth page
|
|
10
|
+
* 2. Frontend generates key_id, sends {key_id, api_key} to MCP /auth/register
|
|
11
|
+
* 3. Frontend redirects to Claude with key_id as "access_token"
|
|
12
|
+
* 4. Claude sends key_id in Authorization header
|
|
13
|
+
* 5. MCP looks up real api_key from this store
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.sessionStore = exports.SessionStore = void 0;
|
|
17
|
+
const logger_1 = require("../lib/logger");
|
|
18
|
+
const logger = (0, logger_1.createLogger)({ component: 'session-store' });
|
|
19
|
+
class SessionStore {
|
|
20
|
+
store = new Map();
|
|
21
|
+
maxTtlMs; // Maximum session lifetime (absolute)
|
|
22
|
+
inactivityTtlMs; // Session expires after inactivity
|
|
23
|
+
cleanupInterval = null;
|
|
24
|
+
/**
|
|
25
|
+
* @param maxTtlHours - Maximum session lifetime in hours (default 24)
|
|
26
|
+
* @param inactivityMinutes - Session expires after this many minutes of inactivity (default 30)
|
|
27
|
+
*/
|
|
28
|
+
constructor(maxTtlHours = 24, inactivityMinutes = 30) {
|
|
29
|
+
this.maxTtlMs = maxTtlHours * 60 * 60 * 1000;
|
|
30
|
+
this.inactivityTtlMs = inactivityMinutes * 60 * 1000;
|
|
31
|
+
// Cleanup expired sessions every 5 minutes (more frequent for inactivity-based expiry)
|
|
32
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 5 * 60 * 1000);
|
|
33
|
+
logger.info('SessionStore initialized', {
|
|
34
|
+
maxTtlHours,
|
|
35
|
+
inactivityMinutes,
|
|
36
|
+
maxTtlMs: this.maxTtlMs,
|
|
37
|
+
inactivityTtlMs: this.inactivityTtlMs
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Register a new session (called by /auth/register endpoint)
|
|
42
|
+
*/
|
|
43
|
+
set(keyId, apiKey, workspaceId) {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const session = {
|
|
46
|
+
apiKey,
|
|
47
|
+
workspaceId,
|
|
48
|
+
createdAt: now,
|
|
49
|
+
lastAccessedAt: now,
|
|
50
|
+
};
|
|
51
|
+
this.store.set(keyId, session);
|
|
52
|
+
logger.info('Session registered', {
|
|
53
|
+
keyId: keyId.substring(0, 8) + '...',
|
|
54
|
+
workspaceId: workspaceId.substring(0, 8) + '...',
|
|
55
|
+
totalSessions: this.store.size,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get session by key_id (returns null if expired or not found)
|
|
60
|
+
* Updates lastAccessedAt to extend inactivity timeout
|
|
61
|
+
*/
|
|
62
|
+
get(keyId) {
|
|
63
|
+
const session = this.store.get(keyId);
|
|
64
|
+
if (!session) {
|
|
65
|
+
logger.debug('Session not found', { keyId: keyId.substring(0, 8) + '...' });
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const age = now - session.createdAt;
|
|
70
|
+
const inactivity = now - session.lastAccessedAt;
|
|
71
|
+
// Check if max lifetime exceeded (absolute expiry)
|
|
72
|
+
if (age > this.maxTtlMs) {
|
|
73
|
+
logger.info('Session expired (max TTL)', {
|
|
74
|
+
keyId: keyId.substring(0, 8) + '...',
|
|
75
|
+
ageHours: Math.round(age / (1000 * 60 * 60)),
|
|
76
|
+
});
|
|
77
|
+
this.store.delete(keyId);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
// Check if inactivity timeout exceeded
|
|
81
|
+
if (inactivity > this.inactivityTtlMs) {
|
|
82
|
+
logger.info('Session expired (inactivity)', {
|
|
83
|
+
keyId: keyId.substring(0, 8) + '...',
|
|
84
|
+
inactivityMinutes: Math.round(inactivity / (1000 * 60)),
|
|
85
|
+
});
|
|
86
|
+
this.store.delete(keyId);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
// Update lastAccessedAt to extend session
|
|
90
|
+
session.lastAccessedAt = now;
|
|
91
|
+
logger.debug('Session found', {
|
|
92
|
+
keyId: keyId.substring(0, 8) + '...',
|
|
93
|
+
ageMinutes: Math.round(age / (1000 * 60)),
|
|
94
|
+
inactivityMinutes: Math.round(inactivity / (1000 * 60)),
|
|
95
|
+
});
|
|
96
|
+
return session;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Delete session (logout)
|
|
100
|
+
*/
|
|
101
|
+
delete(keyId) {
|
|
102
|
+
const existed = this.store.delete(keyId);
|
|
103
|
+
if (existed) {
|
|
104
|
+
logger.info('Session deleted', { keyId: keyId.substring(0, 8) + '...' });
|
|
105
|
+
}
|
|
106
|
+
return existed;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Remove all expired sessions (both max TTL and inactivity)
|
|
110
|
+
*/
|
|
111
|
+
cleanup() {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
let removedMaxTtl = 0;
|
|
114
|
+
let removedInactivity = 0;
|
|
115
|
+
for (const [keyId, session] of this.store.entries()) {
|
|
116
|
+
const age = now - session.createdAt;
|
|
117
|
+
const inactivity = now - session.lastAccessedAt;
|
|
118
|
+
if (age > this.maxTtlMs) {
|
|
119
|
+
this.store.delete(keyId);
|
|
120
|
+
removedMaxTtl++;
|
|
121
|
+
}
|
|
122
|
+
else if (inactivity > this.inactivityTtlMs) {
|
|
123
|
+
this.store.delete(keyId);
|
|
124
|
+
removedInactivity++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const totalRemoved = removedMaxTtl + removedInactivity;
|
|
128
|
+
if (totalRemoved > 0) {
|
|
129
|
+
logger.info('Cleanup completed', {
|
|
130
|
+
removedMaxTtl,
|
|
131
|
+
removedInactivity,
|
|
132
|
+
remaining: this.store.size
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get store statistics
|
|
138
|
+
*/
|
|
139
|
+
getStats() {
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
const sessions = Array.from(this.store.entries()).map(([keyId, session]) => ({
|
|
142
|
+
keyId: keyId.substring(0, 8) + '...',
|
|
143
|
+
ageMinutes: Math.round((now - session.createdAt) / (1000 * 60)),
|
|
144
|
+
inactivityMinutes: Math.round((now - session.lastAccessedAt) / (1000 * 60)),
|
|
145
|
+
}));
|
|
146
|
+
return {
|
|
147
|
+
size: this.store.size,
|
|
148
|
+
config: {
|
|
149
|
+
maxTtlHours: Math.round(this.maxTtlMs / (1000 * 60 * 60)),
|
|
150
|
+
inactivityMinutes: Math.round(this.inactivityTtlMs / (1000 * 60)),
|
|
151
|
+
},
|
|
152
|
+
sessions,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Stop cleanup interval (for graceful shutdown)
|
|
157
|
+
*/
|
|
158
|
+
destroy() {
|
|
159
|
+
if (this.cleanupInterval) {
|
|
160
|
+
clearInterval(this.cleanupInterval);
|
|
161
|
+
this.cleanupInterval = null;
|
|
162
|
+
}
|
|
163
|
+
logger.info('SessionStore destroyed');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
exports.SessionStore = SessionStore;
|
|
167
|
+
// Singleton instance
|
|
168
|
+
exports.sessionStore = new SessionStore();
|
|
169
|
+
//# sourceMappingURL=session-store.js.map
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SignalHandler = void 0;
|
|
4
4
|
const logger_1 = require("../lib/logger");
|
|
5
5
|
const webhooks_1 = require("../bot-config/webhooks");
|
|
6
|
+
const types_1 = require("./utils/types");
|
|
6
7
|
// Optional: bot-config only available in bot server mode, not MCP terminal
|
|
7
8
|
let reloadConfigFromHailer = null;
|
|
8
9
|
let setActiveWorkspace = null;
|
|
@@ -239,6 +240,7 @@ class SignalHandler {
|
|
|
239
240
|
// Refresh cache by fetching fresh data - API requires array of keys to fetch
|
|
240
241
|
// IMPORTANT: Include 'processes' to get updated workflow list (e.g., after template install)
|
|
241
242
|
const init = await this.client.socket.request('v2.core.init', [['users', 'network', 'networks', 'processes']]);
|
|
243
|
+
(0, types_1.normalizeInitProcesses)(init);
|
|
242
244
|
// Update workspace cache with fresh data
|
|
243
245
|
if (init.processes) {
|
|
244
246
|
this.workspaceCache.rawInit.processes = init.processes;
|