whale-code 6.4.0
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/README.md +95 -0
- package/bin/swag-agent.js +9 -0
- package/bin/swagmanager-mcp.js +321 -0
- package/dist/cli/app.d.ts +26 -0
- package/dist/cli/app.js +64 -0
- package/dist/cli/chat/AgentSelector.d.ts +14 -0
- package/dist/cli/chat/AgentSelector.js +14 -0
- package/dist/cli/chat/ChatApp.d.ts +9 -0
- package/dist/cli/chat/ChatApp.js +267 -0
- package/dist/cli/chat/ChatInput.d.ts +39 -0
- package/dist/cli/chat/ChatInput.js +509 -0
- package/dist/cli/chat/MarkdownText.d.ts +10 -0
- package/dist/cli/chat/MarkdownText.js +20 -0
- package/dist/cli/chat/MessageList.d.ts +37 -0
- package/dist/cli/chat/MessageList.js +80 -0
- package/dist/cli/chat/ModelSelector.d.ts +20 -0
- package/dist/cli/chat/ModelSelector.js +73 -0
- package/dist/cli/chat/RewindViewer.d.ts +26 -0
- package/dist/cli/chat/RewindViewer.js +185 -0
- package/dist/cli/chat/StoreSelector.d.ts +14 -0
- package/dist/cli/chat/StoreSelector.js +24 -0
- package/dist/cli/chat/StreamingText.d.ts +12 -0
- package/dist/cli/chat/StreamingText.js +12 -0
- package/dist/cli/chat/SubagentPanel.d.ts +45 -0
- package/dist/cli/chat/SubagentPanel.js +110 -0
- package/dist/cli/chat/TeamPanel.d.ts +21 -0
- package/dist/cli/chat/TeamPanel.js +42 -0
- package/dist/cli/chat/ToolIndicator.d.ts +25 -0
- package/dist/cli/chat/ToolIndicator.js +436 -0
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +39 -0
- package/dist/cli/chat/hooks/useAgentLoop.js +382 -0
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +37 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +387 -0
- package/dist/cli/commands/config-cmd.d.ts +10 -0
- package/dist/cli/commands/config-cmd.js +99 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +172 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.js +278 -0
- package/dist/cli/commands/mcp.d.ts +12 -0
- package/dist/cli/commands/mcp.js +162 -0
- package/dist/cli/login/LoginApp.d.ts +7 -0
- package/dist/cli/login/LoginApp.js +157 -0
- package/dist/cli/print-mode.d.ts +31 -0
- package/dist/cli/print-mode.js +202 -0
- package/dist/cli/serve-mode.d.ts +37 -0
- package/dist/cli/serve-mode.js +636 -0
- package/dist/cli/services/agent-definitions.d.ts +25 -0
- package/dist/cli/services/agent-definitions.js +91 -0
- package/dist/cli/services/agent-events.d.ts +178 -0
- package/dist/cli/services/agent-events.js +175 -0
- package/dist/cli/services/agent-loop.d.ts +90 -0
- package/dist/cli/services/agent-loop.js +762 -0
- package/dist/cli/services/agent-worker-base.d.ts +97 -0
- package/dist/cli/services/agent-worker-base.js +220 -0
- package/dist/cli/services/auth-service.d.ts +30 -0
- package/dist/cli/services/auth-service.js +160 -0
- package/dist/cli/services/background-processes.d.ts +126 -0
- package/dist/cli/services/background-processes.js +318 -0
- package/dist/cli/services/browser-auth.d.ts +24 -0
- package/dist/cli/services/browser-auth.js +180 -0
- package/dist/cli/services/claude-md-loader.d.ts +16 -0
- package/dist/cli/services/claude-md-loader.js +58 -0
- package/dist/cli/services/config-store.d.ts +47 -0
- package/dist/cli/services/config-store.js +79 -0
- package/dist/cli/services/debug-log.d.ts +10 -0
- package/dist/cli/services/debug-log.js +52 -0
- package/dist/cli/services/error-logger.d.ts +58 -0
- package/dist/cli/services/error-logger.js +269 -0
- package/dist/cli/services/file-history.d.ts +21 -0
- package/dist/cli/services/file-history.js +83 -0
- package/dist/cli/services/format-server-response.d.ts +16 -0
- package/dist/cli/services/format-server-response.js +440 -0
- package/dist/cli/services/git-context.d.ts +11 -0
- package/dist/cli/services/git-context.js +66 -0
- package/dist/cli/services/hooks.d.ts +85 -0
- package/dist/cli/services/hooks.js +258 -0
- package/dist/cli/services/interactive-tools.d.ts +125 -0
- package/dist/cli/services/interactive-tools.js +260 -0
- package/dist/cli/services/keybinding-manager.d.ts +52 -0
- package/dist/cli/services/keybinding-manager.js +115 -0
- package/dist/cli/services/local-tools.d.ts +22 -0
- package/dist/cli/services/local-tools.js +697 -0
- package/dist/cli/services/lsp-manager.d.ts +18 -0
- package/dist/cli/services/lsp-manager.js +717 -0
- package/dist/cli/services/mcp-client.d.ts +48 -0
- package/dist/cli/services/mcp-client.js +157 -0
- package/dist/cli/services/memory-manager.d.ts +16 -0
- package/dist/cli/services/memory-manager.js +57 -0
- package/dist/cli/services/model-manager.d.ts +18 -0
- package/dist/cli/services/model-manager.js +71 -0
- package/dist/cli/services/model-router.d.ts +26 -0
- package/dist/cli/services/model-router.js +149 -0
- package/dist/cli/services/permission-modes.d.ts +13 -0
- package/dist/cli/services/permission-modes.js +43 -0
- package/dist/cli/services/rewind.d.ts +84 -0
- package/dist/cli/services/rewind.js +194 -0
- package/dist/cli/services/ripgrep.d.ts +28 -0
- package/dist/cli/services/ripgrep.js +138 -0
- package/dist/cli/services/sandbox.d.ts +29 -0
- package/dist/cli/services/sandbox.js +97 -0
- package/dist/cli/services/server-tools.d.ts +61 -0
- package/dist/cli/services/server-tools.js +543 -0
- package/dist/cli/services/session-persistence.d.ts +23 -0
- package/dist/cli/services/session-persistence.js +99 -0
- package/dist/cli/services/subagent-worker.d.ts +19 -0
- package/dist/cli/services/subagent-worker.js +41 -0
- package/dist/cli/services/subagent.d.ts +47 -0
- package/dist/cli/services/subagent.js +647 -0
- package/dist/cli/services/system-prompt.d.ts +7 -0
- package/dist/cli/services/system-prompt.js +198 -0
- package/dist/cli/services/team-lead.d.ts +73 -0
- package/dist/cli/services/team-lead.js +512 -0
- package/dist/cli/services/team-state.d.ts +77 -0
- package/dist/cli/services/team-state.js +398 -0
- package/dist/cli/services/teammate.d.ts +31 -0
- package/dist/cli/services/teammate.js +689 -0
- package/dist/cli/services/telemetry.d.ts +61 -0
- package/dist/cli/services/telemetry.js +209 -0
- package/dist/cli/services/tools/agent-tools.d.ts +14 -0
- package/dist/cli/services/tools/agent-tools.js +347 -0
- package/dist/cli/services/tools/file-ops.d.ts +15 -0
- package/dist/cli/services/tools/file-ops.js +487 -0
- package/dist/cli/services/tools/search-tools.d.ts +8 -0
- package/dist/cli/services/tools/search-tools.js +186 -0
- package/dist/cli/services/tools/shell-exec.d.ts +10 -0
- package/dist/cli/services/tools/shell-exec.js +168 -0
- package/dist/cli/services/tools/task-manager.d.ts +28 -0
- package/dist/cli/services/tools/task-manager.js +209 -0
- package/dist/cli/services/tools/web-tools.d.ts +11 -0
- package/dist/cli/services/tools/web-tools.js +395 -0
- package/dist/cli/setup/SetupApp.d.ts +9 -0
- package/dist/cli/setup/SetupApp.js +191 -0
- package/dist/cli/shared/MatrixIntro.d.ts +4 -0
- package/dist/cli/shared/MatrixIntro.js +83 -0
- package/dist/cli/shared/Theme.d.ts +74 -0
- package/dist/cli/shared/Theme.js +127 -0
- package/dist/cli/shared/WhaleBanner.d.ts +10 -0
- package/dist/cli/shared/WhaleBanner.js +12 -0
- package/dist/cli/shared/markdown.d.ts +21 -0
- package/dist/cli/shared/markdown.js +756 -0
- package/dist/cli/status/StatusApp.d.ts +4 -0
- package/dist/cli/status/StatusApp.js +105 -0
- package/dist/cli/stores/StoreApp.d.ts +7 -0
- package/dist/cli/stores/StoreApp.js +81 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +538 -0
- package/dist/local-agent/connection.d.ts +48 -0
- package/dist/local-agent/connection.js +332 -0
- package/dist/local-agent/discovery.d.ts +18 -0
- package/dist/local-agent/discovery.js +146 -0
- package/dist/local-agent/executor.d.ts +34 -0
- package/dist/local-agent/executor.js +241 -0
- package/dist/local-agent/index.d.ts +14 -0
- package/dist/local-agent/index.js +198 -0
- package/dist/node/adapters/base.d.ts +35 -0
- package/dist/node/adapters/base.js +10 -0
- package/dist/node/adapters/discord.d.ts +29 -0
- package/dist/node/adapters/discord.js +299 -0
- package/dist/node/adapters/email.d.ts +23 -0
- package/dist/node/adapters/email.js +218 -0
- package/dist/node/adapters/imessage.d.ts +17 -0
- package/dist/node/adapters/imessage.js +118 -0
- package/dist/node/adapters/slack.d.ts +26 -0
- package/dist/node/adapters/slack.js +259 -0
- package/dist/node/adapters/sms.d.ts +23 -0
- package/dist/node/adapters/sms.js +161 -0
- package/dist/node/adapters/telegram.d.ts +17 -0
- package/dist/node/adapters/telegram.js +101 -0
- package/dist/node/adapters/webchat.d.ts +27 -0
- package/dist/node/adapters/webchat.js +160 -0
- package/dist/node/adapters/whatsapp.d.ts +28 -0
- package/dist/node/adapters/whatsapp.js +230 -0
- package/dist/node/cli.d.ts +2 -0
- package/dist/node/cli.js +325 -0
- package/dist/node/config.d.ts +17 -0
- package/dist/node/config.js +31 -0
- package/dist/node/runtime.d.ts +50 -0
- package/dist/node/runtime.js +351 -0
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +11 -0
- package/dist/server/handlers/__test-utils__/mock-supabase.js +393 -0
- package/dist/server/handlers/analytics.d.ts +17 -0
- package/dist/server/handlers/analytics.js +266 -0
- package/dist/server/handlers/api-keys.d.ts +6 -0
- package/dist/server/handlers/api-keys.js +221 -0
- package/dist/server/handlers/billing.d.ts +33 -0
- package/dist/server/handlers/billing.js +272 -0
- package/dist/server/handlers/browser.d.ts +10 -0
- package/dist/server/handlers/browser.js +517 -0
- package/dist/server/handlers/catalog.d.ts +99 -0
- package/dist/server/handlers/catalog.js +976 -0
- package/dist/server/handlers/comms.d.ts +254 -0
- package/dist/server/handlers/comms.js +588 -0
- package/dist/server/handlers/creations.d.ts +6 -0
- package/dist/server/handlers/creations.js +479 -0
- package/dist/server/handlers/crm.d.ts +89 -0
- package/dist/server/handlers/crm.js +538 -0
- package/dist/server/handlers/discovery.d.ts +6 -0
- package/dist/server/handlers/discovery.js +288 -0
- package/dist/server/handlers/embeddings.d.ts +92 -0
- package/dist/server/handlers/embeddings.js +197 -0
- package/dist/server/handlers/enrichment.d.ts +8 -0
- package/dist/server/handlers/enrichment.js +768 -0
- package/dist/server/handlers/image-gen.d.ts +6 -0
- package/dist/server/handlers/image-gen.js +409 -0
- package/dist/server/handlers/inventory.d.ts +319 -0
- package/dist/server/handlers/inventory.js +447 -0
- package/dist/server/handlers/kali.d.ts +10 -0
- package/dist/server/handlers/kali.js +210 -0
- package/dist/server/handlers/llm-providers.d.ts +6 -0
- package/dist/server/handlers/llm-providers.js +673 -0
- package/dist/server/handlers/local-agent.d.ts +6 -0
- package/dist/server/handlers/local-agent.js +118 -0
- package/dist/server/handlers/meta-ads.d.ts +111 -0
- package/dist/server/handlers/meta-ads.js +2279 -0
- package/dist/server/handlers/nodes.d.ts +33 -0
- package/dist/server/handlers/nodes.js +699 -0
- package/dist/server/handlers/operations.d.ts +138 -0
- package/dist/server/handlers/operations.js +131 -0
- package/dist/server/handlers/platform.d.ts +23 -0
- package/dist/server/handlers/platform.js +227 -0
- package/dist/server/handlers/supply-chain.d.ts +19 -0
- package/dist/server/handlers/supply-chain.js +327 -0
- package/dist/server/handlers/transcription.d.ts +17 -0
- package/dist/server/handlers/transcription.js +121 -0
- package/dist/server/handlers/video-gen.d.ts +6 -0
- package/dist/server/handlers/video-gen.js +466 -0
- package/dist/server/handlers/voice.d.ts +8 -0
- package/dist/server/handlers/voice.js +1146 -0
- package/dist/server/handlers/workflow-steps.d.ts +86 -0
- package/dist/server/handlers/workflow-steps.js +2349 -0
- package/dist/server/handlers/workflows.d.ts +7 -0
- package/dist/server/handlers/workflows.js +989 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2427 -0
- package/dist/server/lib/batch-client.d.ts +80 -0
- package/dist/server/lib/batch-client.js +467 -0
- package/dist/server/lib/code-worker-pool.d.ts +31 -0
- package/dist/server/lib/code-worker-pool.js +224 -0
- package/dist/server/lib/code-worker.d.ts +1 -0
- package/dist/server/lib/code-worker.js +188 -0
- package/dist/server/lib/compaction-service.d.ts +32 -0
- package/dist/server/lib/compaction-service.js +162 -0
- package/dist/server/lib/logger.d.ts +19 -0
- package/dist/server/lib/logger.js +46 -0
- package/dist/server/lib/otel.d.ts +38 -0
- package/dist/server/lib/otel.js +126 -0
- package/dist/server/lib/pg-rate-limiter.d.ts +21 -0
- package/dist/server/lib/pg-rate-limiter.js +86 -0
- package/dist/server/lib/prompt-sanitizer.d.ts +37 -0
- package/dist/server/lib/prompt-sanitizer.js +177 -0
- package/dist/server/lib/provider-capabilities.d.ts +85 -0
- package/dist/server/lib/provider-capabilities.js +190 -0
- package/dist/server/lib/provider-failover.d.ts +74 -0
- package/dist/server/lib/provider-failover.js +210 -0
- package/dist/server/lib/rate-limiter.d.ts +39 -0
- package/dist/server/lib/rate-limiter.js +147 -0
- package/dist/server/lib/server-agent-loop.d.ts +107 -0
- package/dist/server/lib/server-agent-loop.js +667 -0
- package/dist/server/lib/server-subagent.d.ts +78 -0
- package/dist/server/lib/server-subagent.js +203 -0
- package/dist/server/lib/session-checkpoint.d.ts +51 -0
- package/dist/server/lib/session-checkpoint.js +145 -0
- package/dist/server/lib/ssrf-guard.d.ts +13 -0
- package/dist/server/lib/ssrf-guard.js +240 -0
- package/dist/server/lib/supabase-client.d.ts +7 -0
- package/dist/server/lib/supabase-client.js +78 -0
- package/dist/server/lib/template-resolver.d.ts +31 -0
- package/dist/server/lib/template-resolver.js +215 -0
- package/dist/server/lib/utils.d.ts +16 -0
- package/dist/server/lib/utils.js +147 -0
- package/dist/server/local-agent-gateway.d.ts +82 -0
- package/dist/server/local-agent-gateway.js +426 -0
- package/dist/server/providers/anthropic.d.ts +20 -0
- package/dist/server/providers/anthropic.js +199 -0
- package/dist/server/providers/bedrock.d.ts +20 -0
- package/dist/server/providers/bedrock.js +194 -0
- package/dist/server/providers/gemini.d.ts +24 -0
- package/dist/server/providers/gemini.js +486 -0
- package/dist/server/providers/openai.d.ts +24 -0
- package/dist/server/providers/openai.js +522 -0
- package/dist/server/providers/registry.d.ts +32 -0
- package/dist/server/providers/registry.js +58 -0
- package/dist/server/providers/shared.d.ts +32 -0
- package/dist/server/providers/shared.js +124 -0
- package/dist/server/providers/types.d.ts +92 -0
- package/dist/server/providers/types.js +12 -0
- package/dist/server/proxy-handlers.d.ts +6 -0
- package/dist/server/proxy-handlers.js +89 -0
- package/dist/server/tool-router.d.ts +149 -0
- package/dist/server/tool-router.js +803 -0
- package/dist/server/validation.d.ts +24 -0
- package/dist/server/validation.js +301 -0
- package/dist/server/worker.d.ts +19 -0
- package/dist/server/worker.js +201 -0
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +181 -0
- package/dist/shared/agent-core.d.ts +157 -0
- package/dist/shared/agent-core.js +534 -0
- package/dist/shared/anthropic-types.d.ts +105 -0
- package/dist/shared/anthropic-types.js +7 -0
- package/dist/shared/api-client.d.ts +90 -0
- package/dist/shared/api-client.js +379 -0
- package/dist/shared/constants.d.ts +33 -0
- package/dist/shared/constants.js +80 -0
- package/dist/shared/sse-parser.d.ts +26 -0
- package/dist/shared/sse-parser.js +259 -0
- package/dist/shared/tool-dispatch.d.ts +52 -0
- package/dist/shared/tool-dispatch.js +191 -0
- package/dist/shared/types.d.ts +72 -0
- package/dist/shared/types.js +7 -0
- package/dist/updater.d.ts +25 -0
- package/dist/updater.js +140 -0
- package/dist/webchat/widget.d.ts +0 -0
- package/dist/webchat/widget.js +397 -0
- package/package.json +95 -0
- package/src/cli/services/builtin-skills/commit.md +19 -0
- package/src/cli/services/builtin-skills/review-pr.md +21 -0
- package/src/cli/services/builtin-skills/review.md +18 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
// handlers/nodes.ts — Node & Channel management endpoints
|
|
2
|
+
// Auth: User JWT for management, Node API key for node operations
|
|
3
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
|
+
import { checkPlanLimits, incrementUsage } from "./billing.js";
|
|
5
|
+
let agentInvoker = null;
|
|
6
|
+
/** Set the agent invoker — called once from index.ts to break circular dependency */
|
|
7
|
+
export function setNodeAgentInvoker(invoker) {
|
|
8
|
+
agentInvoker = invoker;
|
|
9
|
+
}
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// HELPERS
|
|
12
|
+
// ============================================================================
|
|
13
|
+
function hashApiKey(key) {
|
|
14
|
+
return createHash("sha256").update(key).digest("hex");
|
|
15
|
+
}
|
|
16
|
+
function generateNodeApiKey() {
|
|
17
|
+
return randomBytes(32).toString("hex");
|
|
18
|
+
}
|
|
19
|
+
/** Authenticate a node by API key, returns node row or null */
|
|
20
|
+
async function authenticateNode(supabase, apiKey) {
|
|
21
|
+
const hash = hashApiKey(apiKey);
|
|
22
|
+
const { data } = await supabase
|
|
23
|
+
.from("nodes")
|
|
24
|
+
.select("id, store_id, name")
|
|
25
|
+
.eq("api_key_hash", hash)
|
|
26
|
+
.single();
|
|
27
|
+
return data;
|
|
28
|
+
}
|
|
29
|
+
/** Log a node event to both node_events and audit_logs (unified telemetry) */
|
|
30
|
+
async function logNodeEvent(supabase, storeId, nodeId, eventType, details = {}) {
|
|
31
|
+
// node_events — existing table for node lifecycle
|
|
32
|
+
await supabase.from("node_events").insert({
|
|
33
|
+
store_id: storeId,
|
|
34
|
+
node_id: nodeId,
|
|
35
|
+
event_type: eventType,
|
|
36
|
+
details,
|
|
37
|
+
});
|
|
38
|
+
// audit_logs — unified telemetry (same schema as tool/chat spans)
|
|
39
|
+
try {
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const auditRow = {
|
|
42
|
+
action: `node.${eventType}`,
|
|
43
|
+
severity: "info",
|
|
44
|
+
store_id: storeId,
|
|
45
|
+
resource_type: "whale_node",
|
|
46
|
+
resource_id: nodeId,
|
|
47
|
+
source: "whale-node",
|
|
48
|
+
user_id: (details.registered_by || details.deleted_by || null),
|
|
49
|
+
trace_id: randomUUID(),
|
|
50
|
+
span_kind: "INTERNAL",
|
|
51
|
+
service_name: "whale-node",
|
|
52
|
+
status_code: "OK",
|
|
53
|
+
start_time: now.toISOString(),
|
|
54
|
+
end_time: now.toISOString(),
|
|
55
|
+
details: { ...details, node_id: nodeId, event_type: eventType },
|
|
56
|
+
};
|
|
57
|
+
const { error } = await supabase.from("audit_logs").insert(auditRow);
|
|
58
|
+
// Retry without store_id on FK constraint
|
|
59
|
+
if (error?.message?.includes("store_id")) {
|
|
60
|
+
auditRow.store_id = null;
|
|
61
|
+
await supabase.from("audit_logs").insert(auditRow);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Audit must never break node operations
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Resolve or create a conversation for a sender on a channel.
|
|
69
|
+
* Reuses conversation if last message from same sender was < 30 min ago.
|
|
70
|
+
* Creates an ai_conversations row for new sessions so telemetry/history works.
|
|
71
|
+
* Resolves customer identity from the dynamic bridge table. */
|
|
72
|
+
async function resolveConversation(supabase, channelId, senderId, storeId, channelType, senderName) {
|
|
73
|
+
// ── Resolve customer identity (best-effort, any channel type) ──
|
|
74
|
+
let customerId = null;
|
|
75
|
+
let customerName = null;
|
|
76
|
+
if (channelType) {
|
|
77
|
+
try {
|
|
78
|
+
const { data } = await supabase.rpc("resolve_sender_to_customer", {
|
|
79
|
+
p_store_id: storeId,
|
|
80
|
+
p_channel_type: channelType,
|
|
81
|
+
p_sender_id: senderId,
|
|
82
|
+
p_sender_name: senderName || null,
|
|
83
|
+
});
|
|
84
|
+
if (data?.length) {
|
|
85
|
+
customerId = data[0].customer_id;
|
|
86
|
+
customerName = data[0].customer_name;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch { /* customer resolution non-critical */ }
|
|
90
|
+
}
|
|
91
|
+
// ── 30-min window — reuse existing conversation ──
|
|
92
|
+
const thirtyMinAgo = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
93
|
+
const { data: recent } = await supabase
|
|
94
|
+
.from("channel_messages")
|
|
95
|
+
.select("conversation_id")
|
|
96
|
+
.eq("channel_id", channelId)
|
|
97
|
+
.eq("sender_id", senderId)
|
|
98
|
+
.gt("created_at", thirtyMinAgo)
|
|
99
|
+
.not("conversation_id", "is", null)
|
|
100
|
+
.order("created_at", { ascending: false })
|
|
101
|
+
.limit(1);
|
|
102
|
+
if (recent?.length && recent[0].conversation_id) {
|
|
103
|
+
// Backfill customer_id on existing conversation if newly resolved
|
|
104
|
+
if (customerId) {
|
|
105
|
+
Promise.resolve(supabase.from("ai_conversations")
|
|
106
|
+
.update({ customer_id: customerId })
|
|
107
|
+
.eq("id", recent[0].conversation_id)
|
|
108
|
+
.is("customer_id", null)).catch(() => { });
|
|
109
|
+
}
|
|
110
|
+
return { conversationId: recent[0].conversation_id, customerId, customerName, isNewSession: false };
|
|
111
|
+
}
|
|
112
|
+
// ── New session — create ai_conversations row ──
|
|
113
|
+
const newId = randomUUID();
|
|
114
|
+
const title = senderName
|
|
115
|
+
? `${senderName} via ${channelType || "channel"}`
|
|
116
|
+
: `${senderId} via ${channelType || "channel"}`;
|
|
117
|
+
try {
|
|
118
|
+
await supabase.from("ai_conversations").insert({
|
|
119
|
+
id: newId,
|
|
120
|
+
store_id: storeId,
|
|
121
|
+
agent_id: null, // set later by invokeAgentForChannel
|
|
122
|
+
channel_id: channelId,
|
|
123
|
+
sender_id: senderId,
|
|
124
|
+
channel_type: channelType || null,
|
|
125
|
+
customer_id: customerId,
|
|
126
|
+
title,
|
|
127
|
+
metadata: { source: "channel", channel_type: channelType || "unknown" },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch { /* ai_conversations insert non-critical — agent still works without it */ }
|
|
131
|
+
return { conversationId: newId, customerId, customerName, isNewSession: true };
|
|
132
|
+
}
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// ROUTE HANDLER
|
|
135
|
+
// ============================================================================
|
|
136
|
+
export async function handleNodeRoutes(pathname, method, body, supabase, auth, queryParams) {
|
|
137
|
+
// ── POST /nodes/register ──────────────────────────────────────
|
|
138
|
+
// User auth required. Creates a node and returns API key (shown once).
|
|
139
|
+
if (pathname === "/nodes/register" && method === "POST") {
|
|
140
|
+
if (!auth.userId && !auth.isServiceRole) {
|
|
141
|
+
return { status: 401, body: { error: "User authentication required" } };
|
|
142
|
+
}
|
|
143
|
+
if (!body)
|
|
144
|
+
return { status: 400, body: { error: "Request body required" } };
|
|
145
|
+
const reg = body;
|
|
146
|
+
if (!reg.name || !reg.store_id) {
|
|
147
|
+
return { status: 400, body: { error: "name and store_id required" } };
|
|
148
|
+
}
|
|
149
|
+
// Verify user has access to this store
|
|
150
|
+
if (auth.userId) {
|
|
151
|
+
const { data: stores } = await supabase
|
|
152
|
+
.from("user_stores")
|
|
153
|
+
.select("store_id")
|
|
154
|
+
.eq("user_id", auth.userId)
|
|
155
|
+
.eq("store_id", reg.store_id);
|
|
156
|
+
if (!stores?.length) {
|
|
157
|
+
return { status: 403, body: { error: "No access to this store" } };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Check node limit against store plan
|
|
161
|
+
const { data: planRow } = await supabase
|
|
162
|
+
.from("store_plans")
|
|
163
|
+
.select("plan, limits")
|
|
164
|
+
.eq("store_id", reg.store_id)
|
|
165
|
+
.single();
|
|
166
|
+
const planLimits = planRow?.limits;
|
|
167
|
+
const nodesMax = planLimits?.nodes_max ?? 1; // free plan default
|
|
168
|
+
const { count } = await supabase
|
|
169
|
+
.from("nodes")
|
|
170
|
+
.select("id", { count: "exact", head: true })
|
|
171
|
+
.eq("store_id", reg.store_id);
|
|
172
|
+
if ((count || 0) >= nodesMax) {
|
|
173
|
+
return { status: 429, body: { error: `Node limit reached (${count}/${nodesMax} on ${planRow?.plan || "free"} plan)` } };
|
|
174
|
+
}
|
|
175
|
+
const apiKey = generateNodeApiKey();
|
|
176
|
+
const apiKeyHash = hashApiKey(apiKey);
|
|
177
|
+
const { data: node, error } = await supabase
|
|
178
|
+
.from("nodes")
|
|
179
|
+
.insert({
|
|
180
|
+
store_id: reg.store_id,
|
|
181
|
+
name: reg.name,
|
|
182
|
+
api_key_hash: apiKeyHash,
|
|
183
|
+
capabilities: reg.capabilities || [],
|
|
184
|
+
hardware: reg.hardware || {},
|
|
185
|
+
version: reg.version || "1.0.0",
|
|
186
|
+
status: "offline",
|
|
187
|
+
})
|
|
188
|
+
.select("id, name, store_id, status, created_at")
|
|
189
|
+
.single();
|
|
190
|
+
if (error) {
|
|
191
|
+
return { status: 500, body: { error: error.message } };
|
|
192
|
+
}
|
|
193
|
+
await logNodeEvent(supabase, reg.store_id, node.id, "registered", {
|
|
194
|
+
name: reg.name,
|
|
195
|
+
registered_by: auth.userId,
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
status: 201,
|
|
199
|
+
body: {
|
|
200
|
+
success: true,
|
|
201
|
+
node,
|
|
202
|
+
api_key: apiKey, // Shown ONCE — user must save this
|
|
203
|
+
message: "Save your API key — it cannot be retrieved later.",
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
// ── POST /nodes/heartbeat ─────────────────────────────────────
|
|
208
|
+
// Node API key auth. Updates status + hardware info.
|
|
209
|
+
if (pathname === "/nodes/heartbeat" && method === "POST") {
|
|
210
|
+
const node = await authenticateNode(supabase, auth.rawToken);
|
|
211
|
+
if (!node) {
|
|
212
|
+
return { status: 401, body: { error: "Invalid node API key" } };
|
|
213
|
+
}
|
|
214
|
+
const hb = body;
|
|
215
|
+
const updates = {
|
|
216
|
+
status: "online",
|
|
217
|
+
last_heartbeat: new Date().toISOString(),
|
|
218
|
+
updated_at: new Date().toISOString(),
|
|
219
|
+
};
|
|
220
|
+
if (hb.hardware)
|
|
221
|
+
updates.hardware = hb.hardware;
|
|
222
|
+
if (hb.capabilities)
|
|
223
|
+
updates.capabilities = hb.capabilities;
|
|
224
|
+
if (hb.version)
|
|
225
|
+
updates.version = hb.version;
|
|
226
|
+
await supabase.from("nodes").update(updates).eq("id", node.id);
|
|
227
|
+
// Sync channel statuses if reported
|
|
228
|
+
if (hb.channels?.length) {
|
|
229
|
+
for (const ch of hb.channels) {
|
|
230
|
+
await supabase
|
|
231
|
+
.from("channels")
|
|
232
|
+
.update({ status: ch.status, updated_at: new Date().toISOString() })
|
|
233
|
+
.eq("node_id", node.id)
|
|
234
|
+
.eq("type", ch.type);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
status: 200,
|
|
239
|
+
body: { success: true, node_id: node.id },
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// ── GET|POST /nodes ────────────────────────────────────────────
|
|
243
|
+
// User auth. Lists nodes for a store.
|
|
244
|
+
if (pathname === "/nodes" && (method === "GET" || method === "POST")) {
|
|
245
|
+
// store_id from body or query params
|
|
246
|
+
const storeId = body?.store_id || queryParams?.get("store_id");
|
|
247
|
+
if (!storeId) {
|
|
248
|
+
return { status: 400, body: { error: "store_id required" } };
|
|
249
|
+
}
|
|
250
|
+
const { data: nodes, error } = await supabase
|
|
251
|
+
.from("nodes")
|
|
252
|
+
.select("id, name, status, hardware, capabilities, version, ip_address, last_heartbeat, created_at")
|
|
253
|
+
.eq("store_id", storeId)
|
|
254
|
+
.order("created_at", { ascending: false });
|
|
255
|
+
if (error) {
|
|
256
|
+
return { status: 500, body: { error: error.message } };
|
|
257
|
+
}
|
|
258
|
+
// Also fetch channels per node
|
|
259
|
+
const nodeIds = (nodes || []).map((n) => n.id);
|
|
260
|
+
const { data: channels } = nodeIds.length
|
|
261
|
+
? await supabase
|
|
262
|
+
.from("channels")
|
|
263
|
+
.select("id, node_id, type, name, status, stats")
|
|
264
|
+
.in("node_id", nodeIds)
|
|
265
|
+
: { data: [] };
|
|
266
|
+
// Attach channels to nodes
|
|
267
|
+
const result = (nodes || []).map((n) => ({
|
|
268
|
+
...n,
|
|
269
|
+
channels: (channels || []).filter((c) => c.node_id === n.id),
|
|
270
|
+
}));
|
|
271
|
+
return { status: 200, body: { success: true, nodes: result } };
|
|
272
|
+
}
|
|
273
|
+
// ── DELETE /nodes/:id ─────────────────────────────────────────
|
|
274
|
+
// User auth. Deletes a node and all its channels.
|
|
275
|
+
const deleteMatch = pathname.match(/^\/nodes\/([a-f0-9-]+)$/);
|
|
276
|
+
if (deleteMatch && method === "DELETE") {
|
|
277
|
+
const nodeId = deleteMatch[1];
|
|
278
|
+
// Verify ownership
|
|
279
|
+
const { data: node } = await supabase
|
|
280
|
+
.from("nodes")
|
|
281
|
+
.select("id, store_id, name")
|
|
282
|
+
.eq("id", nodeId)
|
|
283
|
+
.single();
|
|
284
|
+
if (!node) {
|
|
285
|
+
return { status: 404, body: { error: "Node not found" } };
|
|
286
|
+
}
|
|
287
|
+
if (auth.userId) {
|
|
288
|
+
const { data: stores } = await supabase
|
|
289
|
+
.from("user_stores")
|
|
290
|
+
.select("store_id")
|
|
291
|
+
.eq("user_id", auth.userId)
|
|
292
|
+
.eq("store_id", node.store_id);
|
|
293
|
+
if (!stores?.length) {
|
|
294
|
+
return { status: 403, body: { error: "No access to this node" } };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
await logNodeEvent(supabase, node.store_id, nodeId, "deleted", {
|
|
298
|
+
name: node.name,
|
|
299
|
+
deleted_by: auth.userId,
|
|
300
|
+
});
|
|
301
|
+
const { error } = await supabase.from("nodes").delete().eq("id", nodeId);
|
|
302
|
+
if (error) {
|
|
303
|
+
return { status: 500, body: { error: error.message } };
|
|
304
|
+
}
|
|
305
|
+
return { status: 200, body: { success: true, deleted: nodeId } };
|
|
306
|
+
}
|
|
307
|
+
// ── POST /channels ───────────────────────────────────────────
|
|
308
|
+
// User auth OR node API key. Registers a channel on a node.
|
|
309
|
+
if (pathname === "/channels" && method === "POST") {
|
|
310
|
+
if (!body)
|
|
311
|
+
return { status: 400, body: { error: "Request body required" } };
|
|
312
|
+
const reg = body;
|
|
313
|
+
if (!reg.store_id || !reg.node_id || !reg.type || !reg.name) {
|
|
314
|
+
return { status: 400, body: { error: "store_id, node_id, type, and name required" } };
|
|
315
|
+
}
|
|
316
|
+
// Verify node exists and belongs to store
|
|
317
|
+
const { data: node } = await supabase
|
|
318
|
+
.from("nodes")
|
|
319
|
+
.select("id, store_id")
|
|
320
|
+
.eq("id", reg.node_id)
|
|
321
|
+
.eq("store_id", reg.store_id)
|
|
322
|
+
.single();
|
|
323
|
+
if (!node) {
|
|
324
|
+
return { status: 404, body: { error: "Node not found in this store" } };
|
|
325
|
+
}
|
|
326
|
+
// Check channel-per-node limit against store plan
|
|
327
|
+
const { data: chanPlanRow } = await supabase
|
|
328
|
+
.from("store_plans")
|
|
329
|
+
.select("plan, limits")
|
|
330
|
+
.eq("store_id", reg.store_id)
|
|
331
|
+
.single();
|
|
332
|
+
const chanPlanLimits = chanPlanRow?.limits;
|
|
333
|
+
const channelsPerNode = chanPlanLimits?.channels_per_node ?? 2; // free plan default
|
|
334
|
+
const { count: existingChannels } = await supabase
|
|
335
|
+
.from("channels")
|
|
336
|
+
.select("id", { count: "exact", head: true })
|
|
337
|
+
.eq("node_id", reg.node_id);
|
|
338
|
+
if ((existingChannels || 0) >= channelsPerNode) {
|
|
339
|
+
return { status: 429, body: { error: `Channel limit reached (${existingChannels}/${channelsPerNode} per node on ${chanPlanRow?.plan || "free"} plan)` } };
|
|
340
|
+
}
|
|
341
|
+
const { data: channel, error } = await supabase
|
|
342
|
+
.from("channels")
|
|
343
|
+
.insert({
|
|
344
|
+
store_id: reg.store_id,
|
|
345
|
+
node_id: reg.node_id,
|
|
346
|
+
type: reg.type,
|
|
347
|
+
name: reg.name,
|
|
348
|
+
config: reg.config || {},
|
|
349
|
+
agent_id: reg.agent_id || null,
|
|
350
|
+
status: "inactive",
|
|
351
|
+
})
|
|
352
|
+
.select("id, type, name, status, created_at")
|
|
353
|
+
.single();
|
|
354
|
+
if (error) {
|
|
355
|
+
return { status: 500, body: { error: error.message } };
|
|
356
|
+
}
|
|
357
|
+
await logNodeEvent(supabase, reg.store_id, reg.node_id, "channel_added", {
|
|
358
|
+
channel_id: channel.id,
|
|
359
|
+
type: reg.type,
|
|
360
|
+
name: reg.name,
|
|
361
|
+
});
|
|
362
|
+
return { status: 201, body: { success: true, channel } };
|
|
363
|
+
}
|
|
364
|
+
// ── GET|POST /channels/list ──────────────────────────────────
|
|
365
|
+
// User auth. Lists channels for a store.
|
|
366
|
+
if (pathname === "/channels/list" && (method === "GET" || method === "POST")) {
|
|
367
|
+
const storeId = body?.store_id || queryParams?.get("store_id");
|
|
368
|
+
if (!storeId) {
|
|
369
|
+
return { status: 400, body: { error: "store_id required" } };
|
|
370
|
+
}
|
|
371
|
+
const { data: channels, error } = await supabase
|
|
372
|
+
.from("channels")
|
|
373
|
+
.select(`
|
|
374
|
+
id, node_id, type, name, status, config, agent_id, stats,
|
|
375
|
+
error_message, created_at, updated_at,
|
|
376
|
+
nodes!inner(id, name, status)
|
|
377
|
+
`)
|
|
378
|
+
.eq("store_id", storeId)
|
|
379
|
+
.order("created_at", { ascending: false });
|
|
380
|
+
if (error) {
|
|
381
|
+
return { status: 500, body: { error: error.message } };
|
|
382
|
+
}
|
|
383
|
+
return { status: 200, body: { success: true, channels: channels || [] } };
|
|
384
|
+
}
|
|
385
|
+
// ── PATCH /channels/:id ────────────────────────────────────────
|
|
386
|
+
// User auth. Update channel config/agent assignment.
|
|
387
|
+
const channelPatchMatch = pathname.match(/^\/channels\/([a-f0-9-]+)$/);
|
|
388
|
+
if (channelPatchMatch && (method === "PUT" || method === "POST") && body?._method === "PATCH") {
|
|
389
|
+
const channelId = channelPatchMatch[1];
|
|
390
|
+
const updates = {};
|
|
391
|
+
if (body.agent_id !== undefined)
|
|
392
|
+
updates.agent_id = body.agent_id;
|
|
393
|
+
if (body.config !== undefined)
|
|
394
|
+
updates.config = body.config;
|
|
395
|
+
if (body.name !== undefined)
|
|
396
|
+
updates.name = body.name;
|
|
397
|
+
if (body.status !== undefined)
|
|
398
|
+
updates.status = body.status;
|
|
399
|
+
updates.updated_at = new Date().toISOString();
|
|
400
|
+
const { data: channel, error } = await supabase
|
|
401
|
+
.from("channels")
|
|
402
|
+
.update(updates)
|
|
403
|
+
.eq("id", channelId)
|
|
404
|
+
.select("id, type, name, status, agent_id, config, updated_at")
|
|
405
|
+
.single();
|
|
406
|
+
if (error) {
|
|
407
|
+
return { status: 500, body: { error: error.message } };
|
|
408
|
+
}
|
|
409
|
+
return { status: 200, body: { success: true, channel } };
|
|
410
|
+
}
|
|
411
|
+
// ── POST /channels/:id/messages ──────────────────────────────
|
|
412
|
+
// Node API key auth. Ingests a message from a channel.
|
|
413
|
+
// If direction=inbound and channel has agent_id, auto-invokes agent.
|
|
414
|
+
const messageMatch = pathname.match(/^\/channels\/([a-f0-9-]+)\/messages$/);
|
|
415
|
+
if (messageMatch && method === "POST") {
|
|
416
|
+
const channelId = messageMatch[1];
|
|
417
|
+
if (!body)
|
|
418
|
+
return { status: 400, body: { error: "Request body required" } };
|
|
419
|
+
// Authenticate node (or service role for admin access)
|
|
420
|
+
const node = await authenticateNode(supabase, auth.rawToken);
|
|
421
|
+
if (!node && !auth.isServiceRole) {
|
|
422
|
+
return { status: 401, body: { error: "Invalid node API key" } };
|
|
423
|
+
}
|
|
424
|
+
// Verify channel exists (and belongs to this node if node auth)
|
|
425
|
+
let channelQuery = supabase
|
|
426
|
+
.from("channels")
|
|
427
|
+
.select("id, store_id, node_id, agent_id, config, type, name")
|
|
428
|
+
.eq("id", channelId);
|
|
429
|
+
if (node)
|
|
430
|
+
channelQuery = channelQuery.eq("node_id", node.id);
|
|
431
|
+
const { data: channel } = await channelQuery.single();
|
|
432
|
+
if (!channel) {
|
|
433
|
+
return { status: 404, body: { error: "Channel not found" } };
|
|
434
|
+
}
|
|
435
|
+
const msg = body;
|
|
436
|
+
const direction = msg.direction || "inbound";
|
|
437
|
+
const senderId = msg.sender_id || "unknown";
|
|
438
|
+
// Check plan message limits before accepting
|
|
439
|
+
const limitCheck = await checkPlanLimits(supabase, channel.store_id, "message");
|
|
440
|
+
if (!limitCheck.allowed) {
|
|
441
|
+
return { status: 429, body: { error: limitCheck.reason || "Message limit exceeded" } };
|
|
442
|
+
}
|
|
443
|
+
// Resolve conversation ID (reuse if < 30 min gap from same sender)
|
|
444
|
+
// Channel type is dynamic — read from channel.config.type or channel row's type column
|
|
445
|
+
const channelType = channel.config?.type || channel.type || "unknown";
|
|
446
|
+
const channelName = channel.name || "";
|
|
447
|
+
let conversationId = msg.conversation_id;
|
|
448
|
+
let senderContext;
|
|
449
|
+
if (!conversationId && direction === "inbound") {
|
|
450
|
+
const ctx = await resolveConversation(supabase, channelId, senderId, channel.store_id, channelType, msg.sender_name);
|
|
451
|
+
conversationId = ctx.conversationId;
|
|
452
|
+
senderContext = {
|
|
453
|
+
senderId,
|
|
454
|
+
senderName: msg.sender_name || undefined,
|
|
455
|
+
customerId: ctx.customerId,
|
|
456
|
+
customerName: ctx.customerName,
|
|
457
|
+
channelType,
|
|
458
|
+
channelId,
|
|
459
|
+
channelName,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
// Insert inbound message
|
|
463
|
+
const { data: message, error } = await supabase
|
|
464
|
+
.from("channel_messages")
|
|
465
|
+
.insert({
|
|
466
|
+
store_id: channel.store_id,
|
|
467
|
+
channel_id: channelId,
|
|
468
|
+
direction,
|
|
469
|
+
sender_id: senderId,
|
|
470
|
+
sender_name: msg.sender_name,
|
|
471
|
+
content: msg.content,
|
|
472
|
+
content_type: msg.content_type || "text",
|
|
473
|
+
metadata: msg.metadata || {},
|
|
474
|
+
agent_id: channel.agent_id,
|
|
475
|
+
conversation_id: conversationId,
|
|
476
|
+
})
|
|
477
|
+
.select("id, direction, content, conversation_id, created_at")
|
|
478
|
+
.single();
|
|
479
|
+
if (error) {
|
|
480
|
+
return { status: 500, body: { error: error.message } };
|
|
481
|
+
}
|
|
482
|
+
// Update channel stats (best-effort)
|
|
483
|
+
try {
|
|
484
|
+
await supabase.rpc("increment_channel_stats", {
|
|
485
|
+
p_channel_id: channelId,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
// Stats function may not exist yet — that's fine
|
|
490
|
+
}
|
|
491
|
+
// Audit log for inbound message (unified telemetry)
|
|
492
|
+
if (direction === "inbound") {
|
|
493
|
+
try {
|
|
494
|
+
const auditRow = {
|
|
495
|
+
action: `node.message.inbound`,
|
|
496
|
+
severity: "info",
|
|
497
|
+
store_id: channel.store_id,
|
|
498
|
+
resource_type: "whale_node",
|
|
499
|
+
resource_id: channelId,
|
|
500
|
+
source: "whale-node",
|
|
501
|
+
conversation_id: conversationId || message.conversation_id || null,
|
|
502
|
+
trace_id: randomUUID(),
|
|
503
|
+
span_kind: "INTERNAL",
|
|
504
|
+
service_name: "whale-node",
|
|
505
|
+
status_code: "OK",
|
|
506
|
+
start_time: new Date().toISOString(),
|
|
507
|
+
end_time: new Date().toISOString(),
|
|
508
|
+
details: {
|
|
509
|
+
channel_id: channelId,
|
|
510
|
+
channel_type: channelType,
|
|
511
|
+
sender_id: senderId,
|
|
512
|
+
sender_name: msg.sender_name || null,
|
|
513
|
+
node_id: node?.id || null,
|
|
514
|
+
has_agent: !!channel.agent_id,
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
const { error: auditErr } = await supabase.from("audit_logs").insert(auditRow);
|
|
518
|
+
if (auditErr?.message?.includes("store_id")) {
|
|
519
|
+
auditRow.store_id = null;
|
|
520
|
+
await supabase.from("audit_logs").insert(auditRow);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
// Audit must never break message flow
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// Track message usage (best-effort, non-blocking)
|
|
528
|
+
incrementUsage(supabase, channel.store_id, direction === "inbound" ? { messages_in: 1 } : { messages_out: 1 }).catch((err) => console.error("[billing] usage increment failed:", err.message));
|
|
529
|
+
// ── Agent auto-invocation ───────────────────────────────────
|
|
530
|
+
// If inbound message and channel has an agent assigned, invoke it
|
|
531
|
+
let agentResponse = null;
|
|
532
|
+
if (direction === "inbound" && channel.agent_id && agentInvoker) {
|
|
533
|
+
// Check agent invocation limits before calling
|
|
534
|
+
const agentLimitCheck = await checkPlanLimits(supabase, channel.store_id, "agent_invocation");
|
|
535
|
+
if (!agentLimitCheck.allowed) {
|
|
536
|
+
console.warn(`[channel-agent] Agent invocation blocked: ${agentLimitCheck.reason}`);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
try {
|
|
540
|
+
console.log(`[channel-agent] Invoking agent ${channel.agent_id} for channel ${channelId}`);
|
|
541
|
+
const result = await agentInvoker(supabase, channel.agent_id, msg.content, channel.store_id, conversationId || message.conversation_id, senderContext);
|
|
542
|
+
// Track agent invocation usage
|
|
543
|
+
incrementUsage(supabase, channel.store_id, { agent_invocations: 1 })
|
|
544
|
+
.catch((err) => console.error("[billing] agent usage increment failed:", err.message));
|
|
545
|
+
if (result.success && result.response) {
|
|
546
|
+
// Insert agent response as outbound message
|
|
547
|
+
const { data: outMsg, error: outErr } = await supabase
|
|
548
|
+
.from("channel_messages")
|
|
549
|
+
.insert({
|
|
550
|
+
store_id: channel.store_id,
|
|
551
|
+
channel_id: channelId,
|
|
552
|
+
direction: "outbound",
|
|
553
|
+
sender_id: "agent",
|
|
554
|
+
sender_name: "AI Agent",
|
|
555
|
+
content: result.response,
|
|
556
|
+
content_type: "text",
|
|
557
|
+
metadata: { agent_id: channel.agent_id, auto_response: true },
|
|
558
|
+
agent_id: channel.agent_id,
|
|
559
|
+
conversation_id: conversationId || message.conversation_id,
|
|
560
|
+
})
|
|
561
|
+
.select("id, direction, content, conversation_id, created_at")
|
|
562
|
+
.single();
|
|
563
|
+
if (!outErr && outMsg) {
|
|
564
|
+
agentResponse = outMsg;
|
|
565
|
+
// Track outbound message usage
|
|
566
|
+
incrementUsage(supabase, channel.store_id, { messages_out: 1 })
|
|
567
|
+
.catch((err) => console.error("[billing] outbound usage increment failed:", err.message));
|
|
568
|
+
console.log(`[channel-agent] Response stored: ${outMsg.id}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
else if (result.error) {
|
|
572
|
+
console.error(`[channel-agent] Agent error: ${result.error}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
console.error(`[channel-agent] Invocation failed:`, err.message);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
status: 201,
|
|
582
|
+
body: {
|
|
583
|
+
success: true,
|
|
584
|
+
message,
|
|
585
|
+
agent_response: agentResponse,
|
|
586
|
+
conversation_id: conversationId || message.conversation_id,
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
// ── GET /channels/:id/messages ─────────────────────────────────
|
|
591
|
+
// Node API key auth. Retrieves messages for a channel.
|
|
592
|
+
// Query params (via body): direction, undelivered, limit, after
|
|
593
|
+
const getMessagesMatch = pathname.match(/^\/channels\/([a-f0-9-]+)\/messages$/);
|
|
594
|
+
if (getMessagesMatch && method === "GET") {
|
|
595
|
+
const channelId = getMessagesMatch[1];
|
|
596
|
+
// Authenticate node (or service role for admin access)
|
|
597
|
+
const node = await authenticateNode(supabase, auth.rawToken);
|
|
598
|
+
if (!node && !auth.isServiceRole) {
|
|
599
|
+
return { status: 401, body: { error: "Invalid node API key" } };
|
|
600
|
+
}
|
|
601
|
+
// Verify channel exists (and belongs to this node if node auth)
|
|
602
|
+
let chQuery = supabase.from("channels").select("id, node_id").eq("id", channelId);
|
|
603
|
+
if (node)
|
|
604
|
+
chQuery = chQuery.eq("node_id", node.id);
|
|
605
|
+
const { data: channel } = await chQuery.single();
|
|
606
|
+
if (!channel) {
|
|
607
|
+
return { status: 404, body: { error: "Channel not found" } };
|
|
608
|
+
}
|
|
609
|
+
let query = supabase
|
|
610
|
+
.from("channel_messages")
|
|
611
|
+
.select("id, direction, sender_id, sender_name, content, content_type, metadata, conversation_id, delivered_at, created_at")
|
|
612
|
+
.eq("channel_id", channelId);
|
|
613
|
+
// Filter by direction (body or query param)
|
|
614
|
+
const direction = body?.direction || queryParams?.get("direction");
|
|
615
|
+
if (direction) {
|
|
616
|
+
query = query.eq("direction", direction);
|
|
617
|
+
}
|
|
618
|
+
// Filter undelivered outbound messages (for node polling)
|
|
619
|
+
const undelivered = body?.undelivered || queryParams?.get("undelivered");
|
|
620
|
+
if (undelivered === true || undelivered === "true") {
|
|
621
|
+
query = query.eq("direction", "outbound").is("delivered_at", null);
|
|
622
|
+
}
|
|
623
|
+
// After a specific timestamp
|
|
624
|
+
const after = body?.after || queryParams?.get("after");
|
|
625
|
+
if (after) {
|
|
626
|
+
query = query.gt("created_at", after);
|
|
627
|
+
}
|
|
628
|
+
const rawLimit = body?.limit || queryParams?.get("limit") || 50;
|
|
629
|
+
const limit = Math.min(Number(rawLimit), 200);
|
|
630
|
+
query = query.order("created_at", { ascending: true }).limit(limit);
|
|
631
|
+
const { data: messages, error: msgErr } = await query;
|
|
632
|
+
if (msgErr) {
|
|
633
|
+
return { status: 500, body: { error: msgErr.message } };
|
|
634
|
+
}
|
|
635
|
+
return { status: 200, body: { success: true, messages: messages || [] } };
|
|
636
|
+
}
|
|
637
|
+
// ── PATCH /channels/:id/messages/:msgId ──────────────────────
|
|
638
|
+
// Node API key auth. Mark message as delivered.
|
|
639
|
+
const deliverMatch = pathname.match(/^\/channels\/([a-f0-9-]+)\/messages\/([a-f0-9-]+)$/);
|
|
640
|
+
if (deliverMatch && (method === "PUT" || method === "POST") && body?._method === "PATCH") {
|
|
641
|
+
const channelId = deliverMatch[1];
|
|
642
|
+
const msgId = deliverMatch[2];
|
|
643
|
+
const node = await authenticateNode(supabase, auth.rawToken);
|
|
644
|
+
if (!node) {
|
|
645
|
+
return { status: 401, body: { error: "Invalid node API key" } };
|
|
646
|
+
}
|
|
647
|
+
const { error } = await supabase
|
|
648
|
+
.from("channel_messages")
|
|
649
|
+
.update({
|
|
650
|
+
delivered_at: body?.delivered_at || new Date().toISOString(),
|
|
651
|
+
metadata: { ...(body?.metadata || {}), delivered_by_node: node.id },
|
|
652
|
+
})
|
|
653
|
+
.eq("id", msgId)
|
|
654
|
+
.eq("channel_id", channelId);
|
|
655
|
+
if (error) {
|
|
656
|
+
return { status: 500, body: { error: error.message } };
|
|
657
|
+
}
|
|
658
|
+
return { status: 200, body: { success: true, delivered: msgId } };
|
|
659
|
+
}
|
|
660
|
+
// ── POST /channels/:id/messages/:msgId/delivered ─────────────
|
|
661
|
+
// Simpler delivery marking endpoint (no _method hack needed)
|
|
662
|
+
const deliverSimpleMatch = pathname.match(/^\/channels\/([a-f0-9-]+)\/messages\/([a-f0-9-]+)\/delivered$/);
|
|
663
|
+
if (deliverSimpleMatch && method === "POST") {
|
|
664
|
+
const channelId = deliverSimpleMatch[1];
|
|
665
|
+
const msgId = deliverSimpleMatch[2];
|
|
666
|
+
const node = await authenticateNode(supabase, auth.rawToken);
|
|
667
|
+
if (!node) {
|
|
668
|
+
return { status: 401, body: { error: "Invalid node API key" } };
|
|
669
|
+
}
|
|
670
|
+
const { error } = await supabase
|
|
671
|
+
.from("channel_messages")
|
|
672
|
+
.update({ delivered_at: new Date().toISOString() })
|
|
673
|
+
.eq("id", msgId)
|
|
674
|
+
.eq("channel_id", channelId);
|
|
675
|
+
if (error) {
|
|
676
|
+
return { status: 500, body: { error: error.message } };
|
|
677
|
+
}
|
|
678
|
+
return { status: 200, body: { success: true, delivered: msgId } };
|
|
679
|
+
}
|
|
680
|
+
// ── GET /nodes/:id/events ────────────────────────────────────
|
|
681
|
+
// User auth. Lists recent events for a node.
|
|
682
|
+
const eventsMatch = pathname.match(/^\/nodes\/([a-f0-9-]+)\/events$/);
|
|
683
|
+
if (eventsMatch && method === "GET") {
|
|
684
|
+
const nodeId = eventsMatch[1];
|
|
685
|
+
const limit = body?.limit || 50;
|
|
686
|
+
const { data: events, error } = await supabase
|
|
687
|
+
.from("node_events")
|
|
688
|
+
.select("id, event_type, details, created_at")
|
|
689
|
+
.eq("node_id", nodeId)
|
|
690
|
+
.order("created_at", { ascending: false })
|
|
691
|
+
.limit(limit);
|
|
692
|
+
if (error) {
|
|
693
|
+
return { status: 500, body: { error: error.message } };
|
|
694
|
+
}
|
|
695
|
+
return { status: 200, body: { success: true, events: events || [] } };
|
|
696
|
+
}
|
|
697
|
+
// No route matched
|
|
698
|
+
return null;
|
|
699
|
+
}
|