@poolzin/pool-bot 2026.3.11 → 2026.3.14
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 +121 -0
- package/dist/.buildstamp +1 -1
- package/dist/agents/checkpoint-manager.js +291 -0
- package/dist/agents/poolbot-tools.js +5 -0
- package/dist/agents/subagent-announce-reliability.js +160 -0
- package/dist/agents/tool-result-truncation.js +299 -0
- package/dist/agents/tools/nodes-file-tool.js +197 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/config-cli.js +60 -0
- package/dist/cron/cron-improvements.js +195 -0
- package/dist/discord/discord-improvements.js +167 -0
- package/dist/gateway/auth-rate-limit.js +19 -0
- package/dist/gateway/auth.js +41 -0
- package/dist/gateway/gateway-improvements.js +294 -0
- package/dist/gateway/node-command-policy.js +7 -2
- package/dist/infra/net/ssrf.js +15 -2
- package/dist/infra/shell-security.js +201 -0
- package/dist/memory/memory-improvements.js +239 -0
- package/dist/node-host/runner.js +146 -79
- package/dist/security/prototype-pollution.js +141 -0
- package/dist/security/webhook-security.js +253 -0
- package/dist/shared/net/ip.js +52 -1
- package/dist/slack/slack-improvements.js +225 -0
- package/dist/telegram/telegram-improvements.js +220 -0
- package/dist/ui-plugins/ui-plugins-improvements.js +191 -0
- package/docs/ANALISE_OPENCLAW_PROFISSIONAL.md +520 -0
- package/docs/competitive-analysis.md +421 -0
- package/docs/implementation-analysis.md +393 -0
- package/docs/plans/2026-03-11-file-operations-security-hardening.md +307 -0
- package/docs/plans/2026-03-11-integracao-projetos-poolbot.md +666 -0
- package/docs/refactor/plugin-development-guide.md +281 -0
- package/extensions/agency-agents/README.md +301 -0
- package/extensions/agency-agents/agents/CONTRIBUTING.md +353 -0
- package/extensions/agency-agents/agents/README.md +602 -0
- package/extensions/agency-agents/agents/design/design-brand-guardian.md +320 -0
- package/extensions/agency-agents/agents/design/design-image-prompt-engineer.md +234 -0
- package/extensions/agency-agents/agents/design/design-ui-designer.md +381 -0
- package/extensions/agency-agents/agents/design/design-ux-architect.md +467 -0
- package/extensions/agency-agents/agents/design/design-ux-researcher.md +327 -0
- package/extensions/agency-agents/agents/design/design-visual-storyteller.md +147 -0
- package/extensions/agency-agents/agents/design/design-whimsy-injector.md +436 -0
- package/extensions/agency-agents/agents/engineering/engineering-ai-engineer.md +144 -0
- package/extensions/agency-agents/agents/engineering/engineering-backend-architect.md +233 -0
- package/extensions/agency-agents/agents/engineering/engineering-devops-automator.md +374 -0
- package/extensions/agency-agents/agents/engineering/engineering-frontend-developer.md +223 -0
- package/extensions/agency-agents/agents/engineering/engineering-mobile-app-builder.md +491 -0
- package/extensions/agency-agents/agents/engineering/engineering-rapid-prototyper.md +460 -0
- package/extensions/agency-agents/agents/engineering/engineering-security-engineer.md +275 -0
- package/extensions/agency-agents/agents/engineering/engineering-senior-developer.md +174 -0
- package/extensions/agency-agents/agents/examples/README.md +48 -0
- package/extensions/agency-agents/agents/examples/nexus-spatial-discovery.md +852 -0
- package/extensions/agency-agents/agents/examples/workflow-landing-page.md +119 -0
- package/extensions/agency-agents/agents/examples/workflow-startup-mvp.md +155 -0
- package/extensions/agency-agents/agents/integrations/README.md +117 -0
- package/extensions/agency-agents/agents/integrations/aider/README.md +38 -0
- package/extensions/agency-agents/agents/integrations/antigravity/README.md +49 -0
- package/extensions/agency-agents/agents/integrations/claude-code/README.md +31 -0
- package/extensions/agency-agents/agents/integrations/cursor/README.md +38 -0
- package/extensions/agency-agents/agents/integrations/gemini-cli/README.md +36 -0
- package/extensions/agency-agents/agents/integrations/opencode/README.md +58 -0
- package/extensions/agency-agents/agents/integrations/windsurf/README.md +26 -0
- package/extensions/agency-agents/agents/marketing/marketing-app-store-optimizer.md +319 -0
- package/extensions/agency-agents/agents/marketing/marketing-content-creator.md +52 -0
- package/extensions/agency-agents/agents/marketing/marketing-growth-hacker.md +52 -0
- package/extensions/agency-agents/agents/marketing/marketing-instagram-curator.md +111 -0
- package/extensions/agency-agents/agents/marketing/marketing-reddit-community-builder.md +121 -0
- package/extensions/agency-agents/agents/marketing/marketing-social-media-strategist.md +123 -0
- package/extensions/agency-agents/agents/marketing/marketing-tiktok-strategist.md +123 -0
- package/extensions/agency-agents/agents/marketing/marketing-twitter-engager.md +124 -0
- package/extensions/agency-agents/agents/marketing/marketing-wechat-official-account.md +143 -0
- package/extensions/agency-agents/agents/marketing/marketing-xiaohongshu-specialist.md +136 -0
- package/extensions/agency-agents/agents/marketing/marketing-zhihu-strategist.md +160 -0
- package/extensions/agency-agents/agents/product/product-feedback-synthesizer.md +117 -0
- package/extensions/agency-agents/agents/product/product-sprint-prioritizer.md +152 -0
- package/extensions/agency-agents/agents/product/product-trend-researcher.md +157 -0
- package/extensions/agency-agents/agents/project-management/project-management-experiment-tracker.md +196 -0
- package/extensions/agency-agents/agents/project-management/project-management-project-shepherd.md +192 -0
- package/extensions/agency-agents/agents/project-management/project-management-studio-operations.md +198 -0
- package/extensions/agency-agents/agents/project-management/project-management-studio-producer.md +201 -0
- package/extensions/agency-agents/agents/project-management/project-manager-senior.md +133 -0
- package/extensions/agency-agents/agents/scripts/convert.sh +362 -0
- package/extensions/agency-agents/agents/scripts/install.sh +465 -0
- package/extensions/agency-agents/agents/scripts/lint-agents.sh +115 -0
- package/extensions/agency-agents/agents/spatial-computing/macos-spatial-metal-engineer.md +335 -0
- package/extensions/agency-agents/agents/spatial-computing/terminal-integration-specialist.md +68 -0
- package/extensions/agency-agents/agents/spatial-computing/visionos-spatial-engineer.md +52 -0
- package/extensions/agency-agents/agents/spatial-computing/xr-cockpit-interaction-specialist.md +30 -0
- package/extensions/agency-agents/agents/spatial-computing/xr-immersive-developer.md +30 -0
- package/extensions/agency-agents/agents/spatial-computing/xr-interface-architect.md +30 -0
- package/extensions/agency-agents/agents/specialized/agentic-identity-trust.md +367 -0
- package/extensions/agency-agents/agents/specialized/agents-orchestrator.md +365 -0
- package/extensions/agency-agents/agents/specialized/data-analytics-reporter.md +52 -0
- package/extensions/agency-agents/agents/specialized/data-consolidation-agent.md +58 -0
- package/extensions/agency-agents/agents/specialized/lsp-index-engineer.md +312 -0
- package/extensions/agency-agents/agents/specialized/report-distribution-agent.md +63 -0
- package/extensions/agency-agents/agents/specialized/sales-data-extraction-agent.md +65 -0
- package/extensions/agency-agents/agents/strategy/EXECUTIVE-BRIEF.md +95 -0
- package/extensions/agency-agents/agents/strategy/QUICKSTART.md +194 -0
- package/extensions/agency-agents/agents/strategy/coordination/agent-activation-prompts.md +401 -0
- package/extensions/agency-agents/agents/strategy/coordination/handoff-templates.md +357 -0
- package/extensions/agency-agents/agents/strategy/nexus-strategy.md +1110 -0
- package/extensions/agency-agents/agents/strategy/playbooks/phase-0-discovery.md +178 -0
- package/extensions/agency-agents/agents/strategy/playbooks/phase-1-strategy.md +238 -0
- package/extensions/agency-agents/agents/strategy/playbooks/phase-2-foundation.md +278 -0
- package/extensions/agency-agents/agents/strategy/playbooks/phase-3-build.md +286 -0
- package/extensions/agency-agents/agents/strategy/playbooks/phase-4-hardening.md +332 -0
- package/extensions/agency-agents/agents/strategy/playbooks/phase-5-launch.md +277 -0
- package/extensions/agency-agents/agents/strategy/playbooks/phase-6-operate.md +318 -0
- package/extensions/agency-agents/agents/strategy/runbooks/scenario-enterprise-feature.md +157 -0
- package/extensions/agency-agents/agents/strategy/runbooks/scenario-incident-response.md +217 -0
- package/extensions/agency-agents/agents/strategy/runbooks/scenario-marketing-campaign.md +187 -0
- package/extensions/agency-agents/agents/strategy/runbooks/scenario-startup-mvp.md +154 -0
- package/extensions/agency-agents/agents/support/support-analytics-reporter.md +363 -0
- package/extensions/agency-agents/agents/support/support-executive-summary-generator.md +210 -0
- package/extensions/agency-agents/agents/support/support-finance-tracker.md +440 -0
- package/extensions/agency-agents/agents/support/support-infrastructure-maintainer.md +616 -0
- package/extensions/agency-agents/agents/support/support-legal-compliance-checker.md +586 -0
- package/extensions/agency-agents/agents/support/support-support-responder.md +583 -0
- package/extensions/agency-agents/agents/testing/testing-accessibility-auditor.md +313 -0
- package/extensions/agency-agents/agents/testing/testing-api-tester.md +304 -0
- package/extensions/agency-agents/agents/testing/testing-evidence-collector.md +208 -0
- package/extensions/agency-agents/agents/testing/testing-performance-benchmarker.md +266 -0
- package/extensions/agency-agents/agents/testing/testing-reality-checker.md +236 -0
- package/extensions/agency-agents/agents/testing/testing-test-results-analyzer.md +303 -0
- package/extensions/agency-agents/agents/testing/testing-tool-evaluator.md +392 -0
- package/extensions/agency-agents/agents/testing/testing-workflow-optimizer.md +448 -0
- package/extensions/agency-agents/index.ts +733 -0
- package/extensions/agency-agents/node_modules/.bin/jiti +21 -0
- package/extensions/agency-agents/node_modules/.bin/tsc +21 -0
- package/extensions/agency-agents/node_modules/.bin/tsserver +21 -0
- package/extensions/agency-agents/node_modules/.bin/tsx +21 -0
- package/extensions/agency-agents/node_modules/.bin/vite +21 -0
- package/extensions/agency-agents/node_modules/.bin/vitest +21 -0
- package/extensions/agency-agents/node_modules/.bin/yaml +21 -0
- package/extensions/agency-agents/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/extensions/agency-agents/package.json +25 -0
- package/extensions/agency-agents/src/AgencyAgentsService.test.ts +443 -0
- package/extensions/agency-agents/src/AgencyAgentsService.ts +288 -0
- package/extensions/agency-agents/src/types.ts +147 -0
- package/extensions/agency-agents/vitest.config.ts +8 -0
- package/extensions/hexstrike-ai/README.md +98 -0
- package/extensions/hexstrike-ai/node_modules/.bin/tsc +21 -0
- package/extensions/hexstrike-ai/node_modules/.bin/tsserver +21 -0
- package/extensions/hexstrike-ai/package.json +29 -0
- package/extensions/hexstrike-ai/poolbot.plugin.json +31 -0
- package/extensions/hexstrike-ai/src/client.ts +91 -0
- package/extensions/hexstrike-ai/src/index.ts +170 -0
- package/extensions/hexstrike-ai/src/server/hexstrike_mcp.py +5470 -0
- package/extensions/hexstrike-ai/src/server/hexstrike_server.py +17289 -0
- package/extensions/hexstrike-ai/src/server/requirements.txt +84 -0
- package/extensions/hexstrike-ai/src/server-manager.ts +83 -0
- package/extensions/hexstrike-ai/tsconfig.json +20 -0
- package/extensions/hexstrike-bridge/package.json +1 -1
- package/extensions/hexstrike-bridge/poolbot.plugin.json +23 -0
- package/extensions/mcp-server/poolbot.plugin.json +10 -0
- package/extensions/page-agent/README.md +159 -0
- package/extensions/page-agent/index.ts +595 -0
- package/extensions/page-agent/node_modules/.bin/jiti +21 -0
- package/extensions/page-agent/node_modules/.bin/playwright +21 -0
- package/extensions/page-agent/node_modules/.bin/tsc +21 -0
- package/extensions/page-agent/node_modules/.bin/tsserver +21 -0
- package/extensions/page-agent/node_modules/.bin/tsx +21 -0
- package/extensions/page-agent/node_modules/.bin/vitest +21 -0
- package/extensions/page-agent/node_modules/.bin/yaml +21 -0
- package/extensions/page-agent/package.json +43 -0
- package/extensions/page-agent/src/PageAgentService.test.ts +517 -0
- package/extensions/page-agent/src/PageAgentService.ts +636 -0
- package/extensions/page-agent/src/PoolBotPageController.test.ts +358 -0
- package/extensions/page-agent/src/PoolBotPageController.ts +245 -0
- package/extensions/page-agent/src/index.ts +20 -0
- package/extensions/page-agent/src/tools.test.ts +231 -0
- package/extensions/page-agent/src/tools.ts +167 -0
- package/extensions/page-agent/src/types.ts +198 -0
- package/extensions/template/README.md +101 -0
- package/extensions/template/index.ts +38 -0
- package/extensions/template/package.json +15 -0
- package/extensions/template/poolbot.plugin.json +10 -0
- package/extensions/xyops/README.md +227 -0
- package/extensions/xyops/index.ts +342 -0
- package/extensions/xyops/node_modules/.bin/jiti +21 -0
- package/extensions/xyops/node_modules/.bin/tsc +21 -0
- package/extensions/xyops/node_modules/.bin/tsserver +21 -0
- package/extensions/xyops/node_modules/.bin/tsx +21 -0
- package/extensions/xyops/node_modules/.bin/vitest +21 -0
- package/extensions/xyops/node_modules/.bin/yaml +21 -0
- package/extensions/xyops/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/extensions/xyops/package.json +39 -0
- package/extensions/xyops/src/client.test.ts +467 -0
- package/extensions/xyops/src/client.ts +157 -0
- package/extensions/xyops/src/types.ts +147 -0
- package/extensions/xyops/vitest.config.ts +8 -0
- package/package.json +1 -1
- package/extensions/mavalie/README.md +0 -97
- package/extensions/mavalie/package.json +0 -15
- package/extensions/mavalie/src/index.ts +0 -62
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook Security Hardening
|
|
3
|
+
*
|
|
4
|
+
* Implements:
|
|
5
|
+
* - Auth before body parsing (fail-closed)
|
|
6
|
+
* - Rate limiting per-path
|
|
7
|
+
* - Replay dedupe with signature timestamps
|
|
8
|
+
* - Constant-time secret comparison
|
|
9
|
+
*
|
|
10
|
+
* OpenClaw #32384, #30951
|
|
11
|
+
*/
|
|
12
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
13
|
+
/**
|
|
14
|
+
* Webhook signature header names
|
|
15
|
+
*/
|
|
16
|
+
export const WEBHOOK_SIGNATURE_HEADERS = {
|
|
17
|
+
SIGNATURE: "x-webhook-signature",
|
|
18
|
+
TIMESTAMP: "x-webhook-timestamp",
|
|
19
|
+
ID: "x-webhook-id",
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Replay protection window in milliseconds (5 minutes)
|
|
23
|
+
*/
|
|
24
|
+
export const REPLAY_PROTECTION_WINDOW_MS = 5 * 60 * 1000;
|
|
25
|
+
/**
|
|
26
|
+
* Rate limit for webhooks per path per minute
|
|
27
|
+
*/
|
|
28
|
+
export const WEBHOOK_RATE_LIMIT_PER_MINUTE = 60;
|
|
29
|
+
/**
|
|
30
|
+
* Cache for seen webhook IDs (replay protection)
|
|
31
|
+
*/
|
|
32
|
+
const seenWebhookIds = new Map();
|
|
33
|
+
/**
|
|
34
|
+
* Prune old webhook IDs from cache every 10 minutes
|
|
35
|
+
*/
|
|
36
|
+
setInterval(() => {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
for (const [id, timestamp] of seenWebhookIds.entries()) {
|
|
39
|
+
if (now - timestamp > REPLAY_PROTECTION_WINDOW_MS * 2) {
|
|
40
|
+
seenWebhookIds.delete(id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}, 10 * 60 * 1000).unref();
|
|
44
|
+
/**
|
|
45
|
+
* Compute HMAC-SHA256 signature for webhook payload.
|
|
46
|
+
*/
|
|
47
|
+
export function computeWebhookSignature(params) {
|
|
48
|
+
const { payload, secret, timestamp } = params;
|
|
49
|
+
const payloadStr = typeof payload === "string" ? payload : payload.toString("utf8");
|
|
50
|
+
const signedContent = `${timestamp}.${payloadStr}`;
|
|
51
|
+
return createHmac("sha256", secret).update(signedContent).digest("hex");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Verify webhook signature with constant-time comparison.
|
|
55
|
+
* OpenClaw #32384 - prevents timing attacks on signature validation.
|
|
56
|
+
*/
|
|
57
|
+
export function verifyWebhookSignature(params) {
|
|
58
|
+
const { signature, payload, secret, timestamp, toleranceMs = 5000 } = params;
|
|
59
|
+
// Check timestamp tolerance (prevent old replay attacks)
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const timestampAge = Math.abs(now - timestamp);
|
|
62
|
+
if (timestampAge > toleranceMs) {
|
|
63
|
+
return {
|
|
64
|
+
valid: false,
|
|
65
|
+
reason: `Signature timestamp outside tolerance (${timestampAge}ms > ${toleranceMs}ms)`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// Compute expected signature
|
|
69
|
+
const expectedSignature = computeWebhookSignature({ payload, secret, timestamp });
|
|
70
|
+
// Constant-time comparison to prevent timing attacks
|
|
71
|
+
try {
|
|
72
|
+
const signatureBuffer = Buffer.from(signature, "hex");
|
|
73
|
+
const expectedBuffer = Buffer.from(expectedSignature, "hex");
|
|
74
|
+
if (signatureBuffer.length !== expectedBuffer.length) {
|
|
75
|
+
return { valid: false, reason: "Signature length mismatch" };
|
|
76
|
+
}
|
|
77
|
+
if (!timingSafeEqual(signatureBuffer, expectedBuffer)) {
|
|
78
|
+
return { valid: false, reason: "Signature mismatch" };
|
|
79
|
+
}
|
|
80
|
+
return { valid: true };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return { valid: false, reason: "Invalid signature format" };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Extract webhook signature from request headers.
|
|
88
|
+
*/
|
|
89
|
+
export function extractWebhookSignature(req) {
|
|
90
|
+
const signatureHeader = req.headers[WEBHOOK_SIGNATURE_HEADERS.SIGNATURE.toLowerCase()];
|
|
91
|
+
const timestampHeader = req.headers[WEBHOOK_SIGNATURE_HEADERS.TIMESTAMP.toLowerCase()];
|
|
92
|
+
const idHeader = req.headers[WEBHOOK_SIGNATURE_HEADERS.ID.toLowerCase()];
|
|
93
|
+
if (!signatureHeader || !timestampHeader) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const signature = Array.isArray(signatureHeader) ? signatureHeader[0] : signatureHeader;
|
|
97
|
+
const timestampStr = Array.isArray(timestampHeader) ? timestampHeader[0] : timestampHeader;
|
|
98
|
+
const timestamp = Number.parseInt(timestampStr, 10);
|
|
99
|
+
if (isNaN(timestamp)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const id = idHeader ? (Array.isArray(idHeader) ? idHeader[0] : idHeader) : undefined;
|
|
103
|
+
return { signature, timestamp, id };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check for webhook replay attacks using signature ID and timestamp.
|
|
107
|
+
* OpenClaw #32384
|
|
108
|
+
*/
|
|
109
|
+
export function checkWebhookReplay(params) {
|
|
110
|
+
const { id, timestamp } = params;
|
|
111
|
+
// If no ID provided, use timestamp as fallback (less secure)
|
|
112
|
+
const replayKey = id ?? `ts:${timestamp}`;
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
// Check if we've seen this webhook before
|
|
115
|
+
const seenAt = seenWebhookIds.get(replayKey);
|
|
116
|
+
if (seenAt !== undefined) {
|
|
117
|
+
return {
|
|
118
|
+
valid: false,
|
|
119
|
+
reason: `Duplicate webhook detected (ID: ${replayKey}, first seen: ${new Date(seenAt).toISOString()})`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Check timestamp window
|
|
123
|
+
if (now - timestamp > REPLAY_PROTECTION_WINDOW_MS) {
|
|
124
|
+
return {
|
|
125
|
+
valid: false,
|
|
126
|
+
reason: `Webhook timestamp too old (${Math.floor((now - timestamp) / 1000)}s > ${REPLAY_PROTECTION_WINDOW_MS / 1000}s)`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// Mark as seen
|
|
130
|
+
seenWebhookIds.set(replayKey, now);
|
|
131
|
+
return { valid: true };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Rate limiter for webhooks (per-path sliding window)
|
|
135
|
+
*/
|
|
136
|
+
const webhookRateLimits = new Map();
|
|
137
|
+
/**
|
|
138
|
+
* Check webhook rate limit for a given path.
|
|
139
|
+
*/
|
|
140
|
+
export function checkWebhookRateLimit(params) {
|
|
141
|
+
const { path, limit = WEBHOOK_RATE_LIMIT_PER_MINUTE, windowMs = 60_000 } = params;
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
const timestamps = webhookRateLimits.get(path) ?? [];
|
|
144
|
+
const recentTimestamps = timestamps.filter((ts) => now - ts < windowMs);
|
|
145
|
+
if (recentTimestamps.length >= limit) {
|
|
146
|
+
const oldestTimestamp = recentTimestamps[0];
|
|
147
|
+
const retryAfterMs = windowMs - (now - oldestTimestamp);
|
|
148
|
+
return { allowed: false, retryAfterMs };
|
|
149
|
+
}
|
|
150
|
+
recentTimestamps.push(now);
|
|
151
|
+
webhookRateLimits.set(path, recentTimestamps);
|
|
152
|
+
return { allowed: true };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Complete webhook authentication middleware.
|
|
156
|
+
* Implements auth-before-body parsing (fail-closed).
|
|
157
|
+
* OpenClaw #32384
|
|
158
|
+
*/
|
|
159
|
+
export async function authenticateWebhook(params) {
|
|
160
|
+
const { req, secret, path, toleranceMs } = params;
|
|
161
|
+
// Step 1: Check rate limit FIRST (before any parsing)
|
|
162
|
+
const rateLimitResult = checkWebhookRateLimit({ path });
|
|
163
|
+
if (!rateLimitResult.allowed) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
reason: `Rate limit exceeded (${WEBHOOK_RATE_LIMIT_PER_MINUTE} req/min)`,
|
|
167
|
+
retryAfterMs: rateLimitResult.retryAfterMs,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// Step 2: Extract signature (headers only, no body parsing yet)
|
|
171
|
+
const sigData = extractWebhookSignature(req);
|
|
172
|
+
if (!sigData) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
reason: "Missing webhook signature headers",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// Step 3: Check for replay attacks
|
|
179
|
+
const replayCheck = checkWebhookReplay({
|
|
180
|
+
id: sigData.id,
|
|
181
|
+
timestamp: sigData.timestamp,
|
|
182
|
+
});
|
|
183
|
+
if (!replayCheck.valid) {
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
reason: replayCheck.reason,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// Step 4: Read body (now that auth checks passed)
|
|
190
|
+
// Note: Caller must ensure body is read as raw Buffer, not parsed JSON
|
|
191
|
+
const body = await readRawBody(req);
|
|
192
|
+
if (!body) {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
reason: "Empty webhook body",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
// Step 5: Verify signature with constant-time comparison
|
|
199
|
+
const sigResult = verifyWebhookSignature({
|
|
200
|
+
signature: sigData.signature,
|
|
201
|
+
payload: body,
|
|
202
|
+
secret,
|
|
203
|
+
timestamp: sigData.timestamp,
|
|
204
|
+
toleranceMs,
|
|
205
|
+
});
|
|
206
|
+
if (!sigResult.valid) {
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
reason: sigResult.reason,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return { ok: true, method: "signature", path };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Read raw body from IncomingMessage as Buffer.
|
|
216
|
+
* Must be called before any body parsing middleware.
|
|
217
|
+
*/
|
|
218
|
+
function readRawBody(req) {
|
|
219
|
+
return new Promise((resolve) => {
|
|
220
|
+
const chunks = [];
|
|
221
|
+
req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
222
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
223
|
+
req.on("error", () => resolve(Buffer.alloc(0)));
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Middleware factory for Express-style frameworks.
|
|
228
|
+
* Enforces auth-before-body parsing.
|
|
229
|
+
*/
|
|
230
|
+
export function createWebhookAuthMiddleware(params) {
|
|
231
|
+
const { secret, toleranceMs } = params;
|
|
232
|
+
return async (req, res, next) => {
|
|
233
|
+
const url = req.url ?? "/";
|
|
234
|
+
const path = url.split("?")[0];
|
|
235
|
+
const result = await authenticateWebhook({
|
|
236
|
+
req,
|
|
237
|
+
secret,
|
|
238
|
+
path,
|
|
239
|
+
toleranceMs,
|
|
240
|
+
});
|
|
241
|
+
if (!result.ok) {
|
|
242
|
+
res.statusCode = result.retryAfterMs ? 429 : 401;
|
|
243
|
+
if (result.retryAfterMs) {
|
|
244
|
+
res.setHeader("Retry-After", String(Math.ceil(result.retryAfterMs / 1000)));
|
|
245
|
+
}
|
|
246
|
+
res.setHeader("Content-Type", "application/json");
|
|
247
|
+
res.end(JSON.stringify({ error: result.reason }));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// Auth passed - continue to handler
|
|
251
|
+
next();
|
|
252
|
+
};
|
|
253
|
+
}
|
package/dist/shared/net/ip.js
CHANGED
|
@@ -22,6 +22,34 @@ const BLOCKED_IPV6_SPECIAL_USE_RANGES = new Set([
|
|
|
22
22
|
"uniqueLocal",
|
|
23
23
|
"multicast",
|
|
24
24
|
]);
|
|
25
|
+
// IPv6 transition mechanisms that embed IPv4 addresses (SSRF bypass vectors)
|
|
26
|
+
const IPV6_TRANSITION_RANGES = [
|
|
27
|
+
{
|
|
28
|
+
name: "NAT64",
|
|
29
|
+
prefix: [ipaddr.IPv6.parse("64:ff9b::"), 96],
|
|
30
|
+
description: "NAT64 prefix (RFC 6146)",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "NAT64-1",
|
|
34
|
+
prefix: [ipaddr.IPv6.parse("64:ff9b:1::"), 48],
|
|
35
|
+
description: "NAT64 prefix variant (RFC 8215)",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "6to4",
|
|
39
|
+
prefix: [ipaddr.IPv6.parse("2002::"), 16],
|
|
40
|
+
description: "6to4 transition (RFC 3056)",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "Teredo",
|
|
44
|
+
prefix: [ipaddr.IPv6.parse("2001::"), 32],
|
|
45
|
+
description: "Teredo tunneling (RFC 4380)",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "ISATAP",
|
|
49
|
+
prefix: [ipaddr.IPv6.parse("fe80::"), 10],
|
|
50
|
+
description: "ISATAP link-local (RFC 5214)",
|
|
51
|
+
},
|
|
52
|
+
];
|
|
25
53
|
const RFC2544_BENCHMARK_PREFIX = [ipaddr.IPv4.parse("198.18.0.0"), 15];
|
|
26
54
|
const EMBEDDED_IPV4_SENTINEL_RULES = [
|
|
27
55
|
{
|
|
@@ -201,10 +229,26 @@ export function isPrivateOrLoopbackIpAddress(raw) {
|
|
|
201
229
|
}
|
|
202
230
|
return isBlockedSpecialUseIpv6Address(normalized);
|
|
203
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Check if an IPv6 address uses transition mechanisms that embed IPv4 addresses.
|
|
234
|
+
* These are SSRF bypass vectors (NAT64, 6to4, Teredo, ISATAP).
|
|
235
|
+
*/
|
|
236
|
+
export function isIpv6TransitionAddress(address) {
|
|
237
|
+
for (const range of IPV6_TRANSITION_RANGES) {
|
|
238
|
+
if (address.match(range.prefix)) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
204
244
|
export function isBlockedSpecialUseIpv6Address(address) {
|
|
205
245
|
if (BLOCKED_IPV6_SPECIAL_USE_RANGES.has(address.range())) {
|
|
206
246
|
return true;
|
|
207
247
|
}
|
|
248
|
+
// Block IPv6 transition mechanisms (SSRF bypass vectors)
|
|
249
|
+
if (isIpv6TransitionAddress(address)) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
208
252
|
// ipaddr.js does not classify deprecated site-local fec0::/10 as private.
|
|
209
253
|
return (address.parts[0] & 0xffc0) === 0xfec0;
|
|
210
254
|
}
|
|
@@ -222,8 +266,15 @@ export function isCarrierGradeNatIpv4Address(raw) {
|
|
|
222
266
|
}
|
|
223
267
|
return parsed.range() === "carrierGradeNat";
|
|
224
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Check if an IPv4 address is in the RFC2544 benchmark range (198.18.0.0/15).
|
|
271
|
+
* This range is used for network device benchmarking and should be blocked by default.
|
|
272
|
+
*/
|
|
273
|
+
export function isRfc2544BenchmarkAddress(address) {
|
|
274
|
+
return address.match(RFC2544_BENCHMARK_PREFIX);
|
|
275
|
+
}
|
|
225
276
|
export function isBlockedSpecialUseIpv4Address(address, options = {}) {
|
|
226
|
-
const inRfc2544BenchmarkRange = address
|
|
277
|
+
const inRfc2544BenchmarkRange = isRfc2544BenchmarkAddress(address);
|
|
227
278
|
if (inRfc2544BenchmarkRange && options.allowRfc2544BenchmarkRange === true) {
|
|
228
279
|
return false;
|
|
229
280
|
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack Channel Improvements
|
|
3
|
+
*
|
|
4
|
+
* Implements OpenClaw improvements:
|
|
5
|
+
* - Native streaming (chat.startStream/appendStream/stopStream)
|
|
6
|
+
* - Thread session isolation
|
|
7
|
+
* - User-token resolution for multi-account
|
|
8
|
+
* - Socket Mode reconnect reliability
|
|
9
|
+
*
|
|
10
|
+
* OpenClaw #32384, #30951
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_SLACK_STREAMING_CONFIG = {
|
|
13
|
+
enabled: true,
|
|
14
|
+
streamInThreads: true,
|
|
15
|
+
streamInChannels: false, // Don't spam channels by default
|
|
16
|
+
chunkDelayMs: 300,
|
|
17
|
+
minChunkSize: 100,
|
|
18
|
+
maxStreamDurationMs: 5 * 60 * 1000, // 5 minutes
|
|
19
|
+
};
|
|
20
|
+
const streamingSessions = new Map();
|
|
21
|
+
/**
|
|
22
|
+
* Start a new streaming session
|
|
23
|
+
*/
|
|
24
|
+
export function startSlackStreamingSession(params) {
|
|
25
|
+
const { channelId, threadTs } = params;
|
|
26
|
+
const key = threadTs ? `${channelId}:${threadTs}` : channelId;
|
|
27
|
+
const session = {
|
|
28
|
+
channelId,
|
|
29
|
+
threadTs,
|
|
30
|
+
startedAt: Date.now(),
|
|
31
|
+
lastChunkAt: Date.now(),
|
|
32
|
+
chunkCount: 0,
|
|
33
|
+
};
|
|
34
|
+
streamingSessions.set(key, session);
|
|
35
|
+
return session;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Update streaming session with new chunk
|
|
39
|
+
*/
|
|
40
|
+
export function appendSlackStreamingChunk(params) {
|
|
41
|
+
const { channelId, threadTs } = params;
|
|
42
|
+
const key = threadTs ? `${channelId}:${threadTs}` : channelId;
|
|
43
|
+
const session = streamingSessions.get(key);
|
|
44
|
+
if (!session) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
session.chunkCount += 1;
|
|
48
|
+
session.lastChunkAt = Date.now();
|
|
49
|
+
return session;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* End streaming session
|
|
53
|
+
*/
|
|
54
|
+
export function endSlackStreamingSession(params) {
|
|
55
|
+
const { channelId, threadTs } = params;
|
|
56
|
+
const key = threadTs ? `${channelId}:${threadTs}` : channelId;
|
|
57
|
+
const session = streamingSessions.get(key);
|
|
58
|
+
if (!session) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
streamingSessions.delete(key);
|
|
62
|
+
return session;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get active streaming session
|
|
66
|
+
*/
|
|
67
|
+
export function getSlackStreamingSession(params) {
|
|
68
|
+
const { channelId, threadTs } = params;
|
|
69
|
+
const key = threadTs ? `${channelId}:${threadTs}` : channelId;
|
|
70
|
+
return streamingSessions.get(key) ?? null;
|
|
71
|
+
}
|
|
72
|
+
export const DEFAULT_THREAD_SESSION_ISOLATION_CONFIG = {
|
|
73
|
+
enabled: true,
|
|
74
|
+
includeChannelId: true,
|
|
75
|
+
includeThreadTs: true,
|
|
76
|
+
separator: ":",
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Build session key with thread isolation
|
|
80
|
+
*/
|
|
81
|
+
export function buildSlackSessionKey(params) {
|
|
82
|
+
const { channelId, threadTs, userId, config } = params;
|
|
83
|
+
const parts = [];
|
|
84
|
+
if (config.includeChannelId) {
|
|
85
|
+
parts.push(channelId);
|
|
86
|
+
}
|
|
87
|
+
if (config.includeThreadTs && threadTs) {
|
|
88
|
+
parts.push(config.separator, "thread", config.separator, threadTs);
|
|
89
|
+
}
|
|
90
|
+
if (userId) {
|
|
91
|
+
parts.push(config.separator, "user", config.separator, userId);
|
|
92
|
+
}
|
|
93
|
+
return parts.join("");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Parse session key to extract thread info
|
|
97
|
+
*/
|
|
98
|
+
export function parseSlackSessionKey(sessionKey) {
|
|
99
|
+
const parts = sessionKey.split(":");
|
|
100
|
+
const result = {
|
|
101
|
+
channelId: parts[0],
|
|
102
|
+
};
|
|
103
|
+
for (let i = 1; i < parts.length; i += 2) {
|
|
104
|
+
const key = parts[i];
|
|
105
|
+
const value = parts[i + 1];
|
|
106
|
+
if (key === "thread") {
|
|
107
|
+
result.threadTs = value;
|
|
108
|
+
}
|
|
109
|
+
else if (key === "user") {
|
|
110
|
+
result.userId = value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
const userTokenCache = new Map();
|
|
116
|
+
/**
|
|
117
|
+
* Cache user token resolution
|
|
118
|
+
*/
|
|
119
|
+
export function cacheUserTokenResolution(resolution) {
|
|
120
|
+
const key = `${resolution.userId}:${resolution.teamId}`;
|
|
121
|
+
userTokenCache.set(key, resolution);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get cached user token resolution
|
|
125
|
+
*/
|
|
126
|
+
export function getCachedUserTokenResolution(params) {
|
|
127
|
+
const { userId, teamId } = params;
|
|
128
|
+
const key = `${userId}:${teamId}`;
|
|
129
|
+
const cached = userTokenCache.get(key);
|
|
130
|
+
if (!cached) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
// Check if expired
|
|
134
|
+
if (cached.expiresAt && Date.now() > cached.expiresAt) {
|
|
135
|
+
userTokenCache.delete(key);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return cached;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Clear user token cache
|
|
142
|
+
*/
|
|
143
|
+
export function clearUserTokenCache() {
|
|
144
|
+
userTokenCache.clear();
|
|
145
|
+
}
|
|
146
|
+
export const DEFAULT_SOCKET_MODE_RECONNECT_CONFIG = {
|
|
147
|
+
enabled: true,
|
|
148
|
+
maxReconnectAttempts: 10,
|
|
149
|
+
baseReconnectDelayMs: 1000,
|
|
150
|
+
maxReconnectDelayMs: 60_000, // 1 minute
|
|
151
|
+
backoffMultiplier: 2,
|
|
152
|
+
reconnectOnErrorCodes: [
|
|
153
|
+
1000, // Normal closure
|
|
154
|
+
1001, // Going away
|
|
155
|
+
1006, // Abnormal closure
|
|
156
|
+
1012, // Service restart
|
|
157
|
+
1013, // Try again later
|
|
158
|
+
1014, // Bad gateway
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
const socketModeStates = new Map();
|
|
162
|
+
/**
|
|
163
|
+
* Get or create Socket Mode state
|
|
164
|
+
*/
|
|
165
|
+
export function getOrCreateSocketModeState(appId) {
|
|
166
|
+
const existing = socketModeStates.get(appId);
|
|
167
|
+
if (existing) {
|
|
168
|
+
return existing;
|
|
169
|
+
}
|
|
170
|
+
const newState = {
|
|
171
|
+
attempt: 0,
|
|
172
|
+
reconnecting: false,
|
|
173
|
+
connected: false,
|
|
174
|
+
};
|
|
175
|
+
socketModeStates.set(appId, newState);
|
|
176
|
+
return newState;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Record Socket Mode disconnection
|
|
180
|
+
*/
|
|
181
|
+
export function recordSocketModeDisconnect(params) {
|
|
182
|
+
const { appId, errorCode, errorMessage, config } = params;
|
|
183
|
+
const state = getOrCreateSocketModeState(appId);
|
|
184
|
+
state.connected = false;
|
|
185
|
+
state.lastErrorCode = errorCode;
|
|
186
|
+
state.lastErrorMessage = errorMessage;
|
|
187
|
+
if (!config.enabled) {
|
|
188
|
+
return { canReconnect: false, delayMs: 0, shouldGiveUp: false };
|
|
189
|
+
}
|
|
190
|
+
// Check if error code warrants reconnection
|
|
191
|
+
if (errorCode && !config.reconnectOnErrorCodes.includes(errorCode)) {
|
|
192
|
+
state.reconnecting = false;
|
|
193
|
+
return { canReconnect: false, delayMs: 0, shouldGiveUp: true };
|
|
194
|
+
}
|
|
195
|
+
state.attempt += 1;
|
|
196
|
+
if (state.attempt > config.maxReconnectAttempts) {
|
|
197
|
+
state.reconnecting = false;
|
|
198
|
+
return { canReconnect: false, delayMs: 0, shouldGiveUp: true };
|
|
199
|
+
}
|
|
200
|
+
state.reconnecting = true;
|
|
201
|
+
// Calculate exponential backoff delay
|
|
202
|
+
const exponentialDelay = config.baseReconnectDelayMs * Math.pow(config.backoffMultiplier, state.attempt - 1);
|
|
203
|
+
const delayMs = Math.min(exponentialDelay, config.maxReconnectDelayMs);
|
|
204
|
+
return { canReconnect: true, delayMs, shouldGiveUp: false };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Record Socket Mode reconnection success
|
|
208
|
+
*/
|
|
209
|
+
export function recordSocketModeReconnect(appId) {
|
|
210
|
+
const state = socketModeStates.get(appId);
|
|
211
|
+
if (state) {
|
|
212
|
+
state.attempt = 0;
|
|
213
|
+
state.reconnecting = false;
|
|
214
|
+
state.connected = true;
|
|
215
|
+
state.lastErrorCode = undefined;
|
|
216
|
+
state.lastErrorMessage = undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Check if Socket Mode is connected
|
|
221
|
+
*/
|
|
222
|
+
export function isSocketModeConnected(appId) {
|
|
223
|
+
const state = socketModeStates.get(appId);
|
|
224
|
+
return state?.connected ?? false;
|
|
225
|
+
}
|