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,240 @@
|
|
|
1
|
+
// server/lib/ssrf-guard.ts — Shared SSRF validation with DNS resolve-then-check
|
|
2
|
+
//
|
|
3
|
+
// Used by workflow steps (webhook_out, custom), email attachments, and any other
|
|
4
|
+
// user-controlled URL fetching. Centralizes all IP blocking logic in one place.
|
|
5
|
+
import { resolve as dnsResolve } from "node:dns/promises";
|
|
6
|
+
// Private/internal IPv4 ranges
|
|
7
|
+
const PRIVATE_IPV4_RANGES = [
|
|
8
|
+
/^127\./, // Loopback
|
|
9
|
+
/^10\./, // RFC 1918
|
|
10
|
+
/^172\.(1[6-9]|2\d|3[01])\./, // RFC 1918
|
|
11
|
+
/^192\.168\./, // RFC 1918
|
|
12
|
+
/^169\.254\./, // Link-local / cloud metadata
|
|
13
|
+
/^0\./, // "This" network
|
|
14
|
+
/^100\.(6[4-9]|[7-9]\d|1[0-1]\d|12[0-7])\./, // CGNAT (RFC 6598)
|
|
15
|
+
/^198\.18\./, // Benchmarking (RFC 2544)
|
|
16
|
+
];
|
|
17
|
+
// IPv6-mapped IPv4 patterns
|
|
18
|
+
// Dotted decimal form: ::ffff:127.0.0.1
|
|
19
|
+
const IPV6_MAPPED_V4 = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i;
|
|
20
|
+
// Hex form (URL parser normalizes to this): ::ffff:7f00:1
|
|
21
|
+
const IPV6_MAPPED_V4_HEX = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;
|
|
22
|
+
// Private/internal IPv6 ranges
|
|
23
|
+
const PRIVATE_IPV6_PATTERNS = [
|
|
24
|
+
/^::1$/, // Loopback
|
|
25
|
+
/^fe80:/i, // Link-local
|
|
26
|
+
/^fc00:/i, // Unique local
|
|
27
|
+
/^fd/i, // Unique local
|
|
28
|
+
/^::$/, // Unspecified address
|
|
29
|
+
];
|
|
30
|
+
const BLOCKED_HOSTS = new Set([
|
|
31
|
+
"localhost",
|
|
32
|
+
"0.0.0.0",
|
|
33
|
+
"metadata.google.internal",
|
|
34
|
+
"metadata.internal",
|
|
35
|
+
]);
|
|
36
|
+
const BLOCKED_TLDS = [".internal", ".local", ".localhost"];
|
|
37
|
+
/**
|
|
38
|
+
* P0 FIX: Normalize alternative IP encodings to standard dotted-decimal.
|
|
39
|
+
* Catches bypass attempts via decimal, hex, octal, and mixed representations:
|
|
40
|
+
* - Decimal: 2130706433 → 127.0.0.1
|
|
41
|
+
* - Hex: 0x7f000001 → 127.0.0.1
|
|
42
|
+
* - Octal: 0177.0.0.01 → 127.0.0.1
|
|
43
|
+
* - Mixed: 0x7f.0.0.1 → 127.0.0.1
|
|
44
|
+
* - Leading zeros: 0127.0.0.01 → 87.0.0.1 (octal interpretation)
|
|
45
|
+
*
|
|
46
|
+
* Returns the normalized dotted-decimal string, or null if not a recognizable IP.
|
|
47
|
+
*/
|
|
48
|
+
function normalizeIp(input) {
|
|
49
|
+
const cleaned = input.trim();
|
|
50
|
+
// Case 1: Pure decimal integer (e.g. 2130706433 = 127.0.0.1)
|
|
51
|
+
if (/^\d{1,10}$/.test(cleaned) && !cleaned.startsWith("0")) {
|
|
52
|
+
const num = parseInt(cleaned, 10);
|
|
53
|
+
if (num >= 0 && num <= 0xffffffff) {
|
|
54
|
+
return `${(num >>> 24) & 0xff}.${(num >>> 16) & 0xff}.${(num >>> 8) & 0xff}.${num & 0xff}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Case 2: Pure hex integer (e.g. 0x7f000001)
|
|
58
|
+
if (/^0x[0-9a-f]+$/i.test(cleaned)) {
|
|
59
|
+
const num = parseInt(cleaned, 16);
|
|
60
|
+
if (num >= 0 && num <= 0xffffffff) {
|
|
61
|
+
return `${(num >>> 24) & 0xff}.${(num >>> 16) & 0xff}.${(num >>> 8) & 0xff}.${num & 0xff}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Case 3: Dotted form with octal/hex/decimal octets (e.g. 0177.0.0.01, 0x7f.0.0.1)
|
|
65
|
+
const parts = cleaned.split(".");
|
|
66
|
+
if (parts.length === 4) {
|
|
67
|
+
const octets = [];
|
|
68
|
+
for (const part of parts) {
|
|
69
|
+
let val;
|
|
70
|
+
if (/^0x[0-9a-f]+$/i.test(part)) {
|
|
71
|
+
// Hex octet
|
|
72
|
+
val = parseInt(part, 16);
|
|
73
|
+
}
|
|
74
|
+
else if (/^0\d+$/.test(part)) {
|
|
75
|
+
// Octal octet (leading zero)
|
|
76
|
+
val = parseInt(part, 8);
|
|
77
|
+
}
|
|
78
|
+
else if (/^\d+$/.test(part)) {
|
|
79
|
+
// Decimal octet
|
|
80
|
+
val = parseInt(part, 10);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
return null; // Not a valid octet
|
|
84
|
+
}
|
|
85
|
+
if (isNaN(val) || val < 0 || val > 255)
|
|
86
|
+
return null;
|
|
87
|
+
octets.push(val);
|
|
88
|
+
}
|
|
89
|
+
return octets.join(".");
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if an IP address is in a private/internal range.
|
|
95
|
+
* P0 FIX: Now normalizes alternative IP encodings before checking.
|
|
96
|
+
*/
|
|
97
|
+
function isPrivateIp(ip) {
|
|
98
|
+
// Strip brackets from IPv6
|
|
99
|
+
const cleaned = ip.replace(/^\[|\]$/g, "");
|
|
100
|
+
// Check IPv6-mapped IPv4 — dotted decimal form (::ffff:x.x.x.x)
|
|
101
|
+
const mapped = cleaned.match(IPV6_MAPPED_V4);
|
|
102
|
+
if (mapped) {
|
|
103
|
+
const v4 = mapped[1];
|
|
104
|
+
const normalized = normalizeIp(v4) || v4;
|
|
105
|
+
return PRIVATE_IPV4_RANGES.some((r) => r.test(normalized));
|
|
106
|
+
}
|
|
107
|
+
// Check IPv6-mapped IPv4 — hex form (::ffff:7f00:1, as normalized by URL parser)
|
|
108
|
+
const mappedHex = cleaned.match(IPV6_MAPPED_V4_HEX);
|
|
109
|
+
if (mappedHex) {
|
|
110
|
+
const hi = parseInt(mappedHex[1], 16);
|
|
111
|
+
const lo = parseInt(mappedHex[2], 16);
|
|
112
|
+
const v4 = `${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`;
|
|
113
|
+
return PRIVATE_IPV4_RANGES.some((r) => r.test(v4));
|
|
114
|
+
}
|
|
115
|
+
// P0 FIX: Normalize alternative encodings (decimal, hex, octal, mixed)
|
|
116
|
+
const normalized = normalizeIp(cleaned);
|
|
117
|
+
if (normalized) {
|
|
118
|
+
return PRIVATE_IPV4_RANGES.some((r) => r.test(normalized));
|
|
119
|
+
}
|
|
120
|
+
// Check plain IPv4 (standard dotted decimal — no leading zeros)
|
|
121
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(cleaned)) {
|
|
122
|
+
return PRIVATE_IPV4_RANGES.some((r) => r.test(cleaned));
|
|
123
|
+
}
|
|
124
|
+
// Check IPv6
|
|
125
|
+
return PRIVATE_IPV6_PATTERNS.some((r) => r.test(cleaned));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Check if a hostname looks like a direct IP address (v4, v6, or alternative encoding).
|
|
129
|
+
* Used to decide whether DNS failure should block or allow.
|
|
130
|
+
*/
|
|
131
|
+
function looksLikeIp(host) {
|
|
132
|
+
const cleaned = host.replace(/^\[|\]$/g, "");
|
|
133
|
+
// Standard dotted decimal
|
|
134
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(cleaned))
|
|
135
|
+
return true;
|
|
136
|
+
// Pure decimal integer
|
|
137
|
+
if (/^\d{1,10}$/.test(cleaned))
|
|
138
|
+
return true;
|
|
139
|
+
// Hex integer
|
|
140
|
+
if (/^0x[0-9a-f]+$/i.test(cleaned))
|
|
141
|
+
return true;
|
|
142
|
+
// Dotted with octal/hex octets
|
|
143
|
+
if (/^[\d.x]+$/i.test(cleaned) && cleaned.split(".").length === 4)
|
|
144
|
+
return true;
|
|
145
|
+
// IPv6 (contains colons)
|
|
146
|
+
if (cleaned.includes(":"))
|
|
147
|
+
return true;
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Validate a URL is safe to fetch (not targeting internal/private addresses).
|
|
152
|
+
* Does basic hostname checks first, then resolves DNS to catch rebinding attacks.
|
|
153
|
+
*
|
|
154
|
+
* @returns null if safe, or a string error message if blocked.
|
|
155
|
+
*/
|
|
156
|
+
export async function validateUrl(urlStr) {
|
|
157
|
+
let u;
|
|
158
|
+
try {
|
|
159
|
+
u = new URL(urlStr);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return "Malformed URL";
|
|
163
|
+
}
|
|
164
|
+
// Block non-HTTP(S) schemes
|
|
165
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") {
|
|
166
|
+
return `Blocked scheme: ${u.protocol}`;
|
|
167
|
+
}
|
|
168
|
+
const host = u.hostname.toLowerCase();
|
|
169
|
+
// Block known internal hostnames
|
|
170
|
+
if (BLOCKED_HOSTS.has(host)) {
|
|
171
|
+
return "URL targets a blocked internal host";
|
|
172
|
+
}
|
|
173
|
+
// Block internal TLDs
|
|
174
|
+
for (const tld of BLOCKED_TLDS) {
|
|
175
|
+
if (host.endsWith(tld)) {
|
|
176
|
+
return "URL targets a blocked internal domain";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Block .fly.dev (our own infrastructure)
|
|
180
|
+
if (host.endsWith(".fly.dev")) {
|
|
181
|
+
return "URL targets internal infrastructure";
|
|
182
|
+
}
|
|
183
|
+
// Block bare IPs that resolve to private ranges (including alternative encodings)
|
|
184
|
+
if (isPrivateIp(host)) {
|
|
185
|
+
return "URL targets a private/internal IP address";
|
|
186
|
+
}
|
|
187
|
+
// DNS resolve-then-check: catch DNS rebinding attacks
|
|
188
|
+
try {
|
|
189
|
+
const addresses = await dnsResolve(host);
|
|
190
|
+
for (const addr of addresses) {
|
|
191
|
+
if (isPrivateIp(addr)) {
|
|
192
|
+
return `DNS resolved to private address: ${addr}`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// P0 FIX: DNS resolution failed — only allow through if host is already a
|
|
198
|
+
// validated direct IP (which was checked above). If it's a hostname that
|
|
199
|
+
// failed DNS, block it: we can't verify where it points.
|
|
200
|
+
if (!looksLikeIp(host)) {
|
|
201
|
+
return "DNS resolution failed — cannot verify target is safe";
|
|
202
|
+
}
|
|
203
|
+
// Direct IP that passed isPrivateIp check above — safe to proceed
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Synchronous fast-path check (no DNS resolution).
|
|
209
|
+
* Use this when you can't await, or for backward compatibility.
|
|
210
|
+
* For full protection (including DNS rebinding), use validateUrl().
|
|
211
|
+
*/
|
|
212
|
+
export function isBlockedUrl(urlStr) {
|
|
213
|
+
try {
|
|
214
|
+
const u = new URL(urlStr);
|
|
215
|
+
const host = u.hostname.toLowerCase();
|
|
216
|
+
if (BLOCKED_HOSTS.has(host) || host === "127.0.0.1" || host === "::1" || host === "[::1]")
|
|
217
|
+
return true;
|
|
218
|
+
if (PRIVATE_IPV4_RANGES.some((r) => r.test(host)))
|
|
219
|
+
return true;
|
|
220
|
+
if (PRIVATE_IPV6_PATTERNS.some((r) => r.test(host.replace(/^\[|\]$/g, ""))))
|
|
221
|
+
return true;
|
|
222
|
+
// P0 FIX: Check alternative IP encodings (decimal, hex, octal)
|
|
223
|
+
if (isPrivateIp(host))
|
|
224
|
+
return true;
|
|
225
|
+
if (host === "169.254.169.254")
|
|
226
|
+
return true;
|
|
227
|
+
for (const tld of BLOCKED_TLDS) {
|
|
228
|
+
if (host.endsWith(tld))
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
if (host.endsWith(".fly.dev"))
|
|
232
|
+
return true;
|
|
233
|
+
if (u.protocol !== "http:" && u.protocol !== "https:")
|
|
234
|
+
return true;
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
/** Initialize the shared Supabase client (call once at startup) */
|
|
3
|
+
export declare function initSupabase(url: string, serviceRoleKey: string): void;
|
|
4
|
+
/** Get the shared service role Supabase client (singleton, retry-enabled) */
|
|
5
|
+
export declare function getServiceClient(): SupabaseClient;
|
|
6
|
+
/** Create a user-scoped client (for RLS context) — uses retry fetch */
|
|
7
|
+
export declare function createUserClient(url: string, anonKey: string, token: string): SupabaseClient;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// lib/supabase-client.ts — Resilient Supabase client with retry logic
|
|
2
|
+
// Fixes intermittent 520 errors from Cloudflare by retrying failed requests
|
|
3
|
+
// and reusing client instances instead of creating new ones per request.
|
|
4
|
+
import { createClient } from "@supabase/supabase-js";
|
|
5
|
+
const MAX_RETRIES = 3;
|
|
6
|
+
const INITIAL_BACKOFF_MS = 500;
|
|
7
|
+
const MAX_BACKOFF_MS = 5_000;
|
|
8
|
+
/** Custom fetch with retry for 5xx errors (Cloudflare 520/522/524) */
|
|
9
|
+
function createRetryFetch(maxRetries = MAX_RETRIES) {
|
|
10
|
+
return async (input, init) => {
|
|
11
|
+
let lastError = null;
|
|
12
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(input, {
|
|
15
|
+
...init,
|
|
16
|
+
// Keep connection alive to reduce TCP handshake overhead
|
|
17
|
+
keepalive: true,
|
|
18
|
+
});
|
|
19
|
+
// Retry on 5xx errors (Cloudflare 520 = origin error, 522 = timeout, 524 = timeout)
|
|
20
|
+
if (res.status >= 500 && attempt < maxRetries) {
|
|
21
|
+
const backoff = Math.min(INITIAL_BACKOFF_MS * Math.pow(2, attempt), MAX_BACKOFF_MS);
|
|
22
|
+
console.warn(`[supabase] ${res.status} on ${typeof input === 'string' ? input.split('?')[0] : 'request'}, retry ${attempt + 1}/${maxRetries} in ${backoff}ms`);
|
|
23
|
+
await new Promise(r => setTimeout(r, backoff));
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
return res;
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
lastError = err;
|
|
30
|
+
// Retry on network errors (ECONNRESET, ETIMEDOUT, etc.)
|
|
31
|
+
if (attempt < maxRetries) {
|
|
32
|
+
const backoff = Math.min(INITIAL_BACKOFF_MS * Math.pow(2, attempt), MAX_BACKOFF_MS);
|
|
33
|
+
console.warn(`[supabase] Network error: ${lastError.message}, retry ${attempt + 1}/${maxRetries} in ${backoff}ms`);
|
|
34
|
+
await new Promise(r => setTimeout(r, backoff));
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw lastError || new Error("Supabase request failed after retries");
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// Singleton service role client — reused across all requests
|
|
43
|
+
let _serviceClient = null;
|
|
44
|
+
let _supabaseUrl = "";
|
|
45
|
+
let _serviceRoleKey = "";
|
|
46
|
+
/** Initialize the shared Supabase client (call once at startup) */
|
|
47
|
+
export function initSupabase(url, serviceRoleKey) {
|
|
48
|
+
_supabaseUrl = url;
|
|
49
|
+
_serviceRoleKey = serviceRoleKey;
|
|
50
|
+
_serviceClient = null; // Reset on re-init
|
|
51
|
+
}
|
|
52
|
+
/** Get the shared service role Supabase client (singleton, retry-enabled) */
|
|
53
|
+
export function getServiceClient() {
|
|
54
|
+
if (!_serviceClient) {
|
|
55
|
+
if (!_supabaseUrl || !_serviceRoleKey) {
|
|
56
|
+
throw new Error("Supabase not initialized. Call initSupabase() first.");
|
|
57
|
+
}
|
|
58
|
+
_serviceClient = createClient(_supabaseUrl, _serviceRoleKey, {
|
|
59
|
+
global: {
|
|
60
|
+
fetch: createRetryFetch(),
|
|
61
|
+
},
|
|
62
|
+
auth: {
|
|
63
|
+
autoRefreshToken: false,
|
|
64
|
+
persistSession: false,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return _serviceClient;
|
|
69
|
+
}
|
|
70
|
+
/** Create a user-scoped client (for RLS context) — uses retry fetch */
|
|
71
|
+
export function createUserClient(url, anonKey, token) {
|
|
72
|
+
return createClient(url, anonKey, {
|
|
73
|
+
global: {
|
|
74
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
75
|
+
fetch: createRetryFetch(2), // Fewer retries for user requests
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface TemplateContext {
|
|
2
|
+
steps: Record<string, {
|
|
3
|
+
output?: unknown;
|
|
4
|
+
status?: string;
|
|
5
|
+
duration_ms?: number;
|
|
6
|
+
}>;
|
|
7
|
+
trigger: Record<string, unknown>;
|
|
8
|
+
input?: unknown;
|
|
9
|
+
env?: Record<string, unknown>;
|
|
10
|
+
workflow?: Record<string, unknown>;
|
|
11
|
+
run?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Recursively resolve templates in any JSON-compatible value.
|
|
15
|
+
* - Strings: resolve {{...}} patterns
|
|
16
|
+
* - Objects: recurse into values
|
|
17
|
+
* - Arrays: recurse into elements
|
|
18
|
+
* - Primitives: return as-is
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveTemplate(value: unknown, ctx: TemplateContext, depth?: number): unknown;
|
|
21
|
+
/**
|
|
22
|
+
* Evaluate a simple condition expression.
|
|
23
|
+
* Supports: ==, !=, >, <, >=, <=, contains, !contains, exists, !exists
|
|
24
|
+
* Templates in the expression are resolved first.
|
|
25
|
+
*
|
|
26
|
+
* Examples:
|
|
27
|
+
* "{{steps.check.output.count}} > 10"
|
|
28
|
+
* "{{steps.fetch.output.status}} == 'active'"
|
|
29
|
+
* "{{trigger.type}} != 'test'"
|
|
30
|
+
*/
|
|
31
|
+
export declare function evaluateCondition(expression: string, ctx: TemplateContext): boolean;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// server/lib/template-resolver.ts — Resolves {{steps.X.output.Y}} and {{trigger.field}} templates
|
|
2
|
+
//
|
|
3
|
+
// Used by workflow engine to flow data between steps.
|
|
4
|
+
// Extends the fillTemplate() pattern from utils.ts with workflow-specific contexts.
|
|
5
|
+
const TEMPLATE_REGEX = /\{\{([^}]+)\}\}/g;
|
|
6
|
+
/** Block prototype pollution / traversal attacks */
|
|
7
|
+
const BLOCKED_PROPS = new Set([
|
|
8
|
+
"__proto__", "constructor", "prototype",
|
|
9
|
+
"__defineGetter__", "__defineSetter__",
|
|
10
|
+
"__lookupGetter__", "__lookupSetter__",
|
|
11
|
+
]);
|
|
12
|
+
/**
|
|
13
|
+
* Safely walk a dot-path into an object, blocking dangerous property names.
|
|
14
|
+
*/
|
|
15
|
+
function walkPath(value, parts, startIndex) {
|
|
16
|
+
let current = value;
|
|
17
|
+
for (let i = startIndex; i < parts.length; i++) {
|
|
18
|
+
if (current === null || current === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (typeof current !== "object")
|
|
21
|
+
return undefined;
|
|
22
|
+
if (BLOCKED_PROPS.has(parts[i]))
|
|
23
|
+
return undefined; // Block prototype traversal
|
|
24
|
+
current = current[parts[i]];
|
|
25
|
+
}
|
|
26
|
+
return current;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a single template expression like "steps.check.output.count" against the context.
|
|
30
|
+
* Returns the resolved value, or undefined if the path doesn't exist.
|
|
31
|
+
*/
|
|
32
|
+
function resolveExpression(expr, ctx) {
|
|
33
|
+
const trimmed = expr.trim();
|
|
34
|
+
// Built-in variables
|
|
35
|
+
if (trimmed === "now")
|
|
36
|
+
return new Date().toISOString();
|
|
37
|
+
if (trimmed === "today")
|
|
38
|
+
return new Date().toISOString().slice(0, 10);
|
|
39
|
+
if (trimmed === "timestamp")
|
|
40
|
+
return Date.now();
|
|
41
|
+
// Navigate the dot path
|
|
42
|
+
const parts = trimmed.split(".");
|
|
43
|
+
const root = parts[0];
|
|
44
|
+
let value;
|
|
45
|
+
if (root === "steps" && parts.length >= 2) {
|
|
46
|
+
const stepKey = parts[1];
|
|
47
|
+
const stepData = ctx.steps[stepKey];
|
|
48
|
+
if (!stepData)
|
|
49
|
+
return undefined;
|
|
50
|
+
value = walkPath(stepData, parts, 2);
|
|
51
|
+
}
|
|
52
|
+
else if (root === "trigger") {
|
|
53
|
+
value = walkPath(ctx.trigger, parts, 1);
|
|
54
|
+
}
|
|
55
|
+
else if (root === "input") {
|
|
56
|
+
value = walkPath(ctx.input, parts, 1);
|
|
57
|
+
}
|
|
58
|
+
else if (root === "workflow" && ctx.workflow) {
|
|
59
|
+
value = walkPath(ctx.workflow, parts, 1);
|
|
60
|
+
}
|
|
61
|
+
else if (root === "run" && ctx.run) {
|
|
62
|
+
value = walkPath(ctx.run, parts, 1);
|
|
63
|
+
}
|
|
64
|
+
else if (root === "env" && ctx.env) {
|
|
65
|
+
// P0 FIX: Only allow explicitly whitelisted env keys — never expose process.env
|
|
66
|
+
const ALLOWED_ENV_KEYS = new Set(["APP_URL", "APP_NAME", "NODE_ENV"]);
|
|
67
|
+
if (parts.length >= 2 && !ALLOWED_ENV_KEYS.has(parts[1]))
|
|
68
|
+
return undefined;
|
|
69
|
+
value = walkPath(ctx.env, parts, 1);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve all {{...}} placeholders in a string.
|
|
78
|
+
* If the entire string is a single template AND the resolved value is not a string,
|
|
79
|
+
* return the raw value (preserving numbers, objects, arrays, booleans).
|
|
80
|
+
*/
|
|
81
|
+
function resolveString(template, ctx) {
|
|
82
|
+
// Check if the entire string is a single template expression
|
|
83
|
+
const singleMatch = template.match(/^\{\{([^}]+)\}\}$/);
|
|
84
|
+
if (singleMatch) {
|
|
85
|
+
const resolved = resolveExpression(singleMatch[1], ctx);
|
|
86
|
+
return resolved !== undefined ? resolved : template;
|
|
87
|
+
}
|
|
88
|
+
// Multiple templates or mixed text — string interpolation
|
|
89
|
+
return template.replace(TEMPLATE_REGEX, (match, expr) => {
|
|
90
|
+
const resolved = resolveExpression(expr, ctx);
|
|
91
|
+
if (resolved === undefined)
|
|
92
|
+
return match;
|
|
93
|
+
if (typeof resolved === "object" && resolved !== null)
|
|
94
|
+
return JSON.stringify(resolved);
|
|
95
|
+
return String(resolved);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Recursively resolve templates in any JSON-compatible value.
|
|
100
|
+
* - Strings: resolve {{...}} patterns
|
|
101
|
+
* - Objects: recurse into values
|
|
102
|
+
* - Arrays: recurse into elements
|
|
103
|
+
* - Primitives: return as-is
|
|
104
|
+
*/
|
|
105
|
+
export function resolveTemplate(value, ctx, depth = 0) {
|
|
106
|
+
// Guard against infinite recursion from circular or deeply nested structures
|
|
107
|
+
if (depth > 20)
|
|
108
|
+
return value;
|
|
109
|
+
if (typeof value === "string") {
|
|
110
|
+
return resolveString(value, ctx);
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(value)) {
|
|
113
|
+
return value.map((item) => resolveTemplate(item, ctx, depth + 1));
|
|
114
|
+
}
|
|
115
|
+
if (value !== null && typeof value === "object") {
|
|
116
|
+
const resolved = {};
|
|
117
|
+
for (const [k, v] of Object.entries(value)) {
|
|
118
|
+
resolved[k] = resolveTemplate(v, ctx, depth + 1);
|
|
119
|
+
}
|
|
120
|
+
return resolved;
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Evaluate a simple condition expression.
|
|
126
|
+
* Supports: ==, !=, >, <, >=, <=, contains, !contains, exists, !exists
|
|
127
|
+
* Templates in the expression are resolved first.
|
|
128
|
+
*
|
|
129
|
+
* Examples:
|
|
130
|
+
* "{{steps.check.output.count}} > 10"
|
|
131
|
+
* "{{steps.fetch.output.status}} == 'active'"
|
|
132
|
+
* "{{trigger.type}} != 'test'"
|
|
133
|
+
*/
|
|
134
|
+
export function evaluateCondition(expression, ctx) {
|
|
135
|
+
// Resolve all templates in the expression first
|
|
136
|
+
const resolved = resolveString(expression, ctx);
|
|
137
|
+
const expr = typeof resolved === 'object' && resolved !== null
|
|
138
|
+
? JSON.stringify(resolved)
|
|
139
|
+
: String(resolved);
|
|
140
|
+
// exists / !exists checks
|
|
141
|
+
if (expr.includes(" exists")) {
|
|
142
|
+
const parts = expr.split(/\s+exists/);
|
|
143
|
+
const val = parts[0].trim();
|
|
144
|
+
const isNegated = expression.trim().startsWith("!");
|
|
145
|
+
const exists = val !== "" && val !== "undefined" && val !== "null" && !val.includes("{{");
|
|
146
|
+
return isNegated ? !exists : exists;
|
|
147
|
+
}
|
|
148
|
+
// P1 FIX: Use regex-based matching with word boundaries to prevent data containing
|
|
149
|
+
// operator strings (e.g., "value >= 10" in a field) from being misinterpreted
|
|
150
|
+
const operatorPatterns = [
|
|
151
|
+
[/\s!==\s/, "!=="],
|
|
152
|
+
[/\s===\s/, "==="],
|
|
153
|
+
[/\s!=\s/, "!="],
|
|
154
|
+
[/\s==\s/, "=="],
|
|
155
|
+
[/\s>=\s/, ">="],
|
|
156
|
+
[/\s<=\s/, "<="],
|
|
157
|
+
[/\s>\s/, ">"],
|
|
158
|
+
[/\s<\s/, "<"],
|
|
159
|
+
[/\scontains\s/, "contains"],
|
|
160
|
+
[/\s!contains\s/, "!contains"],
|
|
161
|
+
];
|
|
162
|
+
for (const [pattern, op] of operatorPatterns) {
|
|
163
|
+
// Use the LAST match: resolved template data (left side) may contain
|
|
164
|
+
// operator-like strings, but the actual operator is the rightmost one.
|
|
165
|
+
const globalRe = new RegExp(pattern.source, "g");
|
|
166
|
+
let lastMatch = null;
|
|
167
|
+
let m;
|
|
168
|
+
while ((m = globalRe.exec(expr)) !== null)
|
|
169
|
+
lastMatch = m;
|
|
170
|
+
if (!lastMatch || lastMatch.index === undefined)
|
|
171
|
+
continue;
|
|
172
|
+
let left = expr.substring(0, lastMatch.index).trim();
|
|
173
|
+
let right = expr.substring(lastMatch.index + lastMatch[0].length).trim();
|
|
174
|
+
// Strip quotes from right side
|
|
175
|
+
if ((right.startsWith("'") && right.endsWith("'")) || (right.startsWith('"') && right.endsWith('"'))) {
|
|
176
|
+
right = right.slice(1, -1);
|
|
177
|
+
}
|
|
178
|
+
if ((left.startsWith("'") && left.endsWith("'")) || (left.startsWith('"') && left.endsWith('"'))) {
|
|
179
|
+
left = left.slice(1, -1);
|
|
180
|
+
}
|
|
181
|
+
const numLeft = Number(left);
|
|
182
|
+
const numRight = Number(right);
|
|
183
|
+
const bothNumeric = !isNaN(numLeft) && !isNaN(numRight) && left !== "" && right !== "";
|
|
184
|
+
switch (op.trim()) {
|
|
185
|
+
case "===":
|
|
186
|
+
case "==":
|
|
187
|
+
return bothNumeric ? numLeft === numRight : left === right;
|
|
188
|
+
case "!==":
|
|
189
|
+
case "!=":
|
|
190
|
+
return bothNumeric ? numLeft !== numRight : left !== right;
|
|
191
|
+
case ">":
|
|
192
|
+
return bothNumeric ? numLeft > numRight : left > right;
|
|
193
|
+
case "<":
|
|
194
|
+
return bothNumeric ? numLeft < numRight : left < right;
|
|
195
|
+
case ">=":
|
|
196
|
+
return bothNumeric ? numLeft >= numRight : left >= right;
|
|
197
|
+
case "<=":
|
|
198
|
+
return bothNumeric ? numLeft <= numRight : left <= right;
|
|
199
|
+
case "contains":
|
|
200
|
+
return left.includes(right);
|
|
201
|
+
case "!contains":
|
|
202
|
+
return !left.includes(right);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Truthy/falsy check — if expression resolved to a value
|
|
206
|
+
if (expr === "true" || expr === "1")
|
|
207
|
+
return true;
|
|
208
|
+
if (expr === "false" || expr === "0" || expr === "" || expr === "null" || expr === "undefined")
|
|
209
|
+
return false;
|
|
210
|
+
// If there are still unresolved templates, condition is false
|
|
211
|
+
if (expr.includes("{{"))
|
|
212
|
+
return false;
|
|
213
|
+
// Non-empty string is truthy
|
|
214
|
+
return expr.length > 0;
|
|
215
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Escape characters that could manipulate PostgREST/ILIKE filter syntax.
|
|
2
|
+
* Escapes ILIKE wildcards (%, _) and backslashes, plus strips commas and parens
|
|
3
|
+
* that could manipulate PostgREST operators. */
|
|
4
|
+
export declare function sanitizeFilterValue(val: string): string;
|
|
5
|
+
/** Group array items by a key, returning counts */
|
|
6
|
+
export declare function groupBy(arr: Record<string, unknown>[], key: string): Record<string, number>;
|
|
7
|
+
/** Escape a value for CSV output */
|
|
8
|
+
export declare function escapeCSV(val: unknown): string;
|
|
9
|
+
/** Fill {{key}} placeholders in a template string */
|
|
10
|
+
export declare function fillTemplate(template: string, data: Record<string, unknown>): string;
|
|
11
|
+
/** Extract concise metrics from tool results for audit logging (never full payloads). */
|
|
12
|
+
export declare function summarizeResult(toolName: string, action: string | undefined, data: unknown): Record<string, unknown>;
|
|
13
|
+
/** Timeout wrapper for tool execution with AbortSignal support.
|
|
14
|
+
* Creates an AbortController, races the promise against a timeout,
|
|
15
|
+
* and aborts the signal on timeout so underlying work (fetch etc.) can cancel. */
|
|
16
|
+
export declare function withTimeout<T>(promiseOrFactory: Promise<T> | ((signal: AbortSignal) => Promise<T>), ms: number, name: string): Promise<T>;
|