@poolzin/pool-bot 2026.3.22 → 2026.3.23
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 +54 -0
- package/dist/acp/bindings-store.js +209 -0
- package/dist/acp/control-plane/runtime-cache.js +54 -0
- package/dist/acp/control-plane/runtime-options.js +215 -0
- package/dist/acp/control-plane/session-actor-queue.js +36 -0
- package/dist/acp/runtime/errors.js +47 -0
- package/dist/acp/runtime/registry.js +86 -0
- package/dist/acp/runtime/types.js +1 -0
- package/dist/acp/translator.js +97 -0
- package/dist/agents/failover-error.js +145 -47
- package/dist/browser/browser-profile-manager.js +319 -0
- package/dist/browser/cdp-proxy-bypass.js +129 -0
- package/dist/browser/cdp-timeouts.js +41 -0
- package/dist/browser/chrome-extension-validator.js +406 -0
- package/dist/browser/chrome-mcp-snapshot.js +222 -0
- package/dist/browser/chrome-mcp.js +421 -0
- package/dist/browser/chrome-mcp.snapshot.js +133 -0
- package/dist/browser/errors.js +67 -0
- package/dist/browser/form-fields.js +22 -0
- package/dist/browser/output-atomic.js +44 -0
- package/dist/browser/profile-capabilities.js +47 -0
- package/dist/browser/safe-filename.js +25 -0
- package/dist/browser/snapshot-roles.js +60 -0
- package/dist/build-info.json +3 -3
- package/dist/commands/security-owner-only.js +86 -0
- package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
- package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/cron/cron-filters.js +150 -0
- package/dist/gateway/device-pairing-security.js +197 -0
- package/dist/gateway/event-deduplication.js +167 -0
- package/dist/gateway/run-tracker.js +253 -0
- package/dist/gateway/server-methods/nodes.js +14 -0
- package/dist/gateway/websocket-preauth-security.js +188 -0
- package/dist/infra/errors.js +53 -13
- package/dist/infra/exec-approvals-security.js +217 -0
- package/dist/infra/security/command-analyzer.js +257 -0
- package/dist/plugins/loader.js +16 -8
- package/dist/security/external-content.js +51 -1
- package/dist/sessions/session-costs.js +228 -0
- package/dist/shared/param-key.js +16 -0
- package/dist/shared/poll-params.js +58 -0
- package/dist/shared/polls.js +55 -0
- package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
- package/docs/FEATURES.md +523 -0
- package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
- package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
- package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
- package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
- package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
- package/docs/MIKRODASH-ANALYSIS.md +412 -0
- package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
- package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
- package/docs/PHASE-7-SUMMARY.md +144 -0
- package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
- package/docs/PROJECT-FINAL-STATUS.md +237 -0
- package/docs/README.md +116 -0
- package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
- package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
- package/docs/channels/googlechat.md +235 -206
- package/docs/channels/irc.md +332 -0
- package/docs/channels/nostr.md +255 -168
- package/docs/components/command-palette.md +166 -0
- package/docs/components/login-gate.md +219 -0
- package/docs/getting-started/installation.md +191 -0
- package/docs/getting-started/introduction.md +120 -0
- package/docs/improvements/USAGE-GUIDE.md +359 -0
- package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
- package/docs/reference/deadcode-detection.md +72 -0
- package/extensions/acpx/node_modules/.bin/acpx +21 -0
- package/extensions/agency-agents/node_modules/.bin/vite +4 -4
- package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
- package/extensions/googlechat/node_modules/.bin/tsc +21 -0
- package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
- package/extensions/googlechat/node_modules/.bin/vitest +21 -0
- package/extensions/googlechat/package.json +11 -28
- package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
- package/extensions/googlechat/src/googlechat-channel.ts +120 -0
- package/extensions/googlechat/src/index.ts +14 -0
- package/extensions/irc/node_modules/.bin/tsc +21 -0
- package/extensions/irc/node_modules/.bin/tsserver +21 -0
- package/extensions/irc/node_modules/.bin/vitest +21 -0
- package/extensions/irc/package.json +16 -8
- package/extensions/irc/src/index.ts +14 -0
- package/extensions/irc/src/irc-channel.test.ts +43 -0
- package/extensions/irc/src/irc-channel.ts +191 -0
- package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
- package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
- package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
- package/extensions/keyed-async-queue/package.json +20 -0
- package/extensions/keyed-async-queue/src/index.ts +14 -0
- package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
- package/extensions/keyed-async-queue/src/queue.ts +200 -0
- package/extensions/memory-core/node_modules/.bin/tsc +21 -0
- package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
- package/extensions/memory-core/node_modules/.bin/vitest +21 -0
- package/extensions/memory-core/package.json +11 -8
- package/extensions/memory-core/src/index.ts +14 -0
- package/extensions/memory-core/src/memory-manager.test.ts +124 -0
- package/extensions/memory-core/src/memory-manager.ts +186 -0
- package/extensions/nostr/node_modules/.bin/tsc +2 -2
- package/extensions/nostr/node_modules/.bin/tsserver +2 -2
- package/extensions/nostr/node_modules/.bin/vitest +21 -0
- package/extensions/nostr/package.json +15 -24
- package/extensions/nostr/src/index.ts +14 -0
- package/extensions/nostr/src/nostr-channel.test.ts +55 -0
- package/extensions/nostr/src/nostr-channel.ts +228 -0
- package/extensions/page-agent/node_modules/.bin/vitest +2 -2
- package/extensions/test-utils/node_modules/.bin/jiti +21 -0
- package/extensions/test-utils/node_modules/.bin/playwright +21 -0
- package/extensions/test-utils/node_modules/.bin/tsx +21 -0
- package/extensions/test-utils/node_modules/.bin/vite +21 -0
- package/extensions/test-utils/node_modules/.bin/vitest +21 -0
- package/extensions/test-utils/node_modules/.bin/yaml +21 -0
- package/extensions/xyops/node_modules/.bin/vitest +2 -2
- package/package.json +2 -1
- package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
- package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
- package/extensions/memory-core/node_modules/.bin/poolbot +0 -21
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run Tracking System
|
|
3
|
+
*
|
|
4
|
+
* Tracks active agent runs with AbortController support for cancellation.
|
|
5
|
+
* Provides run lifecycle management and resource cleanup.
|
|
6
|
+
*/
|
|
7
|
+
export class RunTracker {
|
|
8
|
+
runs = new Map();
|
|
9
|
+
completedRuns = new Map();
|
|
10
|
+
MAX_COMPLETED_RUNS;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.MAX_COMPLETED_RUNS = options?.maxCompletedRuns ?? 1000;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Start tracking a new run
|
|
16
|
+
*/
|
|
17
|
+
startRun(runId, params) {
|
|
18
|
+
if (this.runs.has(runId)) {
|
|
19
|
+
throw new Error(`Run ${runId} already exists`);
|
|
20
|
+
}
|
|
21
|
+
const abortController = new AbortController();
|
|
22
|
+
const buffer = new TransformStream();
|
|
23
|
+
const writer = buffer.writable.getWriter();
|
|
24
|
+
const reader = buffer.readable.getReader();
|
|
25
|
+
const state = {
|
|
26
|
+
abortController,
|
|
27
|
+
buffer,
|
|
28
|
+
writer,
|
|
29
|
+
reader,
|
|
30
|
+
startTime: Date.now(),
|
|
31
|
+
model: params.model,
|
|
32
|
+
sessionKey: params.sessionKey,
|
|
33
|
+
runId,
|
|
34
|
+
status: "running",
|
|
35
|
+
metadata: params.metadata,
|
|
36
|
+
};
|
|
37
|
+
this.runs.set(runId, state);
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get run state by ID
|
|
42
|
+
*/
|
|
43
|
+
getRun(runId) {
|
|
44
|
+
return this.runs.get(runId);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Check if run exists and is active
|
|
48
|
+
*/
|
|
49
|
+
hasRun(runId) {
|
|
50
|
+
return this.runs.has(runId);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get all active runs
|
|
54
|
+
*/
|
|
55
|
+
getActiveRuns() {
|
|
56
|
+
return Array.from(this.runs.values());
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get runs by session key
|
|
60
|
+
*/
|
|
61
|
+
getRunsBySession(sessionKey) {
|
|
62
|
+
return Array.from(this.runs.values()).filter((run) => run.sessionKey === sessionKey);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Abort a specific run
|
|
66
|
+
*/
|
|
67
|
+
async abortRun(runId, reason) {
|
|
68
|
+
const run = this.runs.get(runId);
|
|
69
|
+
if (!run) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
run.status = "aborted";
|
|
74
|
+
run.abortController.abort(reason);
|
|
75
|
+
// Cancel writer and reader (ignore errors)
|
|
76
|
+
try {
|
|
77
|
+
await run.writer.abort();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Ignore writer abort errors
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
await run.reader.cancel();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Ignore reader cancel errors
|
|
87
|
+
}
|
|
88
|
+
// Cleanup
|
|
89
|
+
this.runs.delete(runId);
|
|
90
|
+
this.markCompleted(run, reason ? new Error(reason) : undefined);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.error(`Error aborting run ${runId}:`, error);
|
|
95
|
+
// Still cleanup even if there was an error
|
|
96
|
+
this.runs.delete(runId);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Mark run as completed
|
|
102
|
+
*/
|
|
103
|
+
async completeRun(runId, metadata) {
|
|
104
|
+
const run = this.runs.get(runId);
|
|
105
|
+
if (!run) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
run.status = "completed";
|
|
110
|
+
// Close streams
|
|
111
|
+
await run.writer.close();
|
|
112
|
+
// Cleanup
|
|
113
|
+
this.runs.delete(runId);
|
|
114
|
+
this.markCompleted(run, undefined, metadata);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
console.error(`Error completing run ${runId}:`, error);
|
|
119
|
+
run.status = "failed";
|
|
120
|
+
run.error = error instanceof Error ? error : new Error(String(error));
|
|
121
|
+
this.runs.delete(runId);
|
|
122
|
+
this.markCompleted(run, run.error);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Mark run as failed
|
|
128
|
+
*/
|
|
129
|
+
async failRun(runId, error) {
|
|
130
|
+
const run = this.runs.get(runId);
|
|
131
|
+
if (!run) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
run.status = "failed";
|
|
136
|
+
run.error = error;
|
|
137
|
+
// Cleanup streams (ignore errors)
|
|
138
|
+
try {
|
|
139
|
+
await run.writer.abort(error);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Ignore writer abort errors
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
await run.reader.cancel(error);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Ignore reader cancel errors
|
|
149
|
+
}
|
|
150
|
+
// Cleanup
|
|
151
|
+
this.runs.delete(runId);
|
|
152
|
+
this.markCompleted(run, error);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
catch (cleanupError) {
|
|
156
|
+
console.error(`Error failing run ${runId}:`, cleanupError);
|
|
157
|
+
// Still cleanup even if there was an error
|
|
158
|
+
this.runs.delete(runId);
|
|
159
|
+
this.markCompleted(run, error);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Write data to run buffer
|
|
165
|
+
*/
|
|
166
|
+
async writeToRun(runId, data) {
|
|
167
|
+
const run = this.runs.get(runId);
|
|
168
|
+
if (!run) {
|
|
169
|
+
throw new Error(`Run ${runId} not found`);
|
|
170
|
+
}
|
|
171
|
+
if (run.status !== "running") {
|
|
172
|
+
throw new Error(`Run ${runId} is not running (status: ${run.status})`);
|
|
173
|
+
}
|
|
174
|
+
if (run.abortController.signal.aborted) {
|
|
175
|
+
throw new Error(`Run ${runId} was aborted`);
|
|
176
|
+
}
|
|
177
|
+
await run.writer.write(data);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get run duration in milliseconds
|
|
181
|
+
*/
|
|
182
|
+
getRunDuration(runId) {
|
|
183
|
+
const run = this.runs.get(runId);
|
|
184
|
+
if (run) {
|
|
185
|
+
return Date.now() - run.startTime;
|
|
186
|
+
}
|
|
187
|
+
const completed = this.completedRuns.get(runId);
|
|
188
|
+
if (completed && completed.endTime) {
|
|
189
|
+
return completed.endTime - completed.startTime;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get statistics about runs
|
|
195
|
+
*/
|
|
196
|
+
getStats() {
|
|
197
|
+
const completed = Array.from(this.completedRuns.values());
|
|
198
|
+
const durations = completed
|
|
199
|
+
.filter((r) => r.endTime && r.startTime)
|
|
200
|
+
.map((r) => r.endTime - r.startTime);
|
|
201
|
+
const avgDuration = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0;
|
|
202
|
+
return {
|
|
203
|
+
activeRuns: this.runs.size,
|
|
204
|
+
completedRuns: completed.filter((r) => r.status === "completed").length,
|
|
205
|
+
abortedRuns: completed.filter((r) => r.status === "aborted").length,
|
|
206
|
+
failedRuns: completed.filter((r) => r.status === "failed").length,
|
|
207
|
+
averageDuration: avgDuration,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Clear all completed runs from memory
|
|
212
|
+
*/
|
|
213
|
+
clearCompletedRuns() {
|
|
214
|
+
this.completedRuns.clear();
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Destroy tracker and cleanup all runs
|
|
218
|
+
*/
|
|
219
|
+
async destroy() {
|
|
220
|
+
// Abort all active runs
|
|
221
|
+
const abortPromises = Array.from(this.runs.keys()).map((runId) => this.abortRun(runId, "Tracker destroyed"));
|
|
222
|
+
await Promise.all(abortPromises);
|
|
223
|
+
this.runs.clear();
|
|
224
|
+
this.completedRuns.clear();
|
|
225
|
+
}
|
|
226
|
+
markCompleted(run, error, metadata) {
|
|
227
|
+
const completedMetadata = {
|
|
228
|
+
runId: run.runId,
|
|
229
|
+
sessionKey: run.sessionKey,
|
|
230
|
+
model: run.model,
|
|
231
|
+
startTime: run.startTime,
|
|
232
|
+
endTime: Date.now(),
|
|
233
|
+
status: run.status,
|
|
234
|
+
error: error?.message,
|
|
235
|
+
...run.metadata,
|
|
236
|
+
...metadata,
|
|
237
|
+
};
|
|
238
|
+
this.completedRuns.set(run.runId, completedMetadata);
|
|
239
|
+
// Evict oldest if at capacity
|
|
240
|
+
if (this.completedRuns.size > this.MAX_COMPLETED_RUNS) {
|
|
241
|
+
const oldest = Array.from(this.completedRuns.entries()).sort((a, b) => a[1].startTime - b[1].startTime)[0];
|
|
242
|
+
if (oldest) {
|
|
243
|
+
this.completedRuns.delete(oldest[0]);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Generate unique run ID
|
|
250
|
+
*/
|
|
251
|
+
export function generateRunId() {
|
|
252
|
+
return `run_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
253
|
+
}
|
|
@@ -2,6 +2,7 @@ import { loadConfig } from "../../config/config.js";
|
|
|
2
2
|
import { listDevicePairing } from "../../infra/device-pairing.js";
|
|
3
3
|
import { approveNodePairing, listNodePairing, rejectNodePairing, renamePairedNode, requestNodePairing, verifyNodeToken, } from "../../infra/node-pairing.js";
|
|
4
4
|
import { loadApnsRegistration, resolveApnsAuthConfigFromEnv, sendApnsAlert, sendApnsBackgroundWake, } from "../../infra/push-apns.js";
|
|
5
|
+
import { CommandAnalyzer } from "../../infra/security/command-analyzer.js";
|
|
5
6
|
import { isNodeCommandAllowed, resolveNodeCommandAllowlist } from "../node-command-policy.js";
|
|
6
7
|
import { sanitizeNodeInvokeParamsForForwarding } from "../node-invoke-sanitize.js";
|
|
7
8
|
import { ErrorCodes, errorShape, validateNodeDescribeParams, validateNodeEventParams, validateNodeInvokeParams, validateNodeListParams, validateNodePairApproveParams, validateNodePairListParams, validateNodePairRejectParams, validateNodePairRequestParams, validateNodePairVerifyParams, validateNodeRenameParams, } from "../protocol/index.js";
|
|
@@ -529,6 +530,19 @@ export const nodeHandlers = {
|
|
|
529
530
|
}));
|
|
530
531
|
return;
|
|
531
532
|
}
|
|
533
|
+
// Command security analysis
|
|
534
|
+
const commandAnalyzer = new CommandAnalyzer();
|
|
535
|
+
const analysis = commandAnalyzer.analyze(command, []);
|
|
536
|
+
if (analysis.blockedReason) {
|
|
537
|
+
context.logGateway.warn(`node.invoke blocked command=${command} nodeId=${nodeId} reason=${analysis.blockedReason}`);
|
|
538
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, analysis.blockedReason, {
|
|
539
|
+
details: { command, riskLevel: analysis.riskLevel },
|
|
540
|
+
}));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (analysis.requiresApproval && analysis.riskLevel !== "low") {
|
|
544
|
+
context.logGateway.info(`node.invoke requires approval command=${command} nodeId=${nodeId} riskLevel=${analysis.riskLevel}`);
|
|
545
|
+
}
|
|
532
546
|
const forwardedParams = sanitizeNodeInvokeParamsForForwarding({
|
|
533
547
|
command,
|
|
534
548
|
rawParams: p.params,
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Pre-Authentication Security Hardening
|
|
3
|
+
*
|
|
4
|
+
* Security fixes for WebSocket pre-auth:
|
|
5
|
+
* - Shorten unauthenticated handshake retention
|
|
6
|
+
* - Reject oversized pre-auth frames
|
|
7
|
+
* - Fail-closed for malformed frames
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Default security configuration
|
|
11
|
+
*
|
|
12
|
+
* Security: Hardened values for production
|
|
13
|
+
* - 5 second handshake timeout (reduced from 10s)
|
|
14
|
+
* - 1KB max pre-auth frame size (prevents DoS)
|
|
15
|
+
* - 3 max pre-auth frames (limits attack surface)
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_PREAUTH_CONFIG = {
|
|
18
|
+
handshakeTimeoutMs: 5_000, // 5 seconds (reduced from 10s)
|
|
19
|
+
maxPreAuthFrameSize: 1024, // 1KB
|
|
20
|
+
maxPreAuthFrames: 3,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Create a new pre-auth connection state
|
|
24
|
+
*/
|
|
25
|
+
export function createPreAuthConnectionState(ws, config = DEFAULT_PREAUTH_CONFIG) {
|
|
26
|
+
return {
|
|
27
|
+
ws,
|
|
28
|
+
createdAt: Date.now(),
|
|
29
|
+
frameCount: 0,
|
|
30
|
+
totalBytesReceived: 0,
|
|
31
|
+
authenticated: false,
|
|
32
|
+
config,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Start handshake timeout timer
|
|
37
|
+
*
|
|
38
|
+
* Security: Shorter timeout reduces attack surface for unauthenticated connections
|
|
39
|
+
*/
|
|
40
|
+
export function startHandshakeTimeout(state, onTimeout) {
|
|
41
|
+
// Clear any existing timeout
|
|
42
|
+
if (state.handshakeTimeout) {
|
|
43
|
+
clearTimeout(state.handshakeTimeout);
|
|
44
|
+
}
|
|
45
|
+
// Set new timeout with hardened value
|
|
46
|
+
state.handshakeTimeout = setTimeout(() => {
|
|
47
|
+
if (!state.authenticated) {
|
|
48
|
+
onTimeout();
|
|
49
|
+
}
|
|
50
|
+
}, state.config.handshakeTimeoutMs);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Clear handshake timeout
|
|
54
|
+
*/
|
|
55
|
+
export function clearHandshakeTimeout(state) {
|
|
56
|
+
if (state.handshakeTimeout) {
|
|
57
|
+
clearTimeout(state.handshakeTimeout);
|
|
58
|
+
state.handshakeTimeout = undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Validate pre-auth frame size
|
|
63
|
+
*
|
|
64
|
+
* Security: Reject oversized frames before authentication to prevent DoS
|
|
65
|
+
*
|
|
66
|
+
* Returns true if frame is acceptable, false if it should be rejected
|
|
67
|
+
*/
|
|
68
|
+
export function validatePreAuthFrameSize(state, frameSize) {
|
|
69
|
+
// Check single frame size
|
|
70
|
+
if (frameSize > state.config.maxPreAuthFrameSize) {
|
|
71
|
+
return {
|
|
72
|
+
valid: false,
|
|
73
|
+
reason: `Pre-auth frame too large: ${frameSize} bytes (max: ${state.config.maxPreAuthFrameSize})`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Check cumulative size
|
|
77
|
+
const newTotal = state.totalBytesReceived + frameSize;
|
|
78
|
+
const maxCumulativeSize = state.config.maxPreAuthFrameSize * state.config.maxPreAuthFrames;
|
|
79
|
+
if (newTotal > maxCumulativeSize) {
|
|
80
|
+
return {
|
|
81
|
+
valid: false,
|
|
82
|
+
reason: `Pre-auth cumulative size exceeded: ${newTotal} bytes (max: ${maxCumulativeSize})`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return { valid: true };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Track pre-auth frame reception
|
|
89
|
+
*
|
|
90
|
+
* Security: Enforce frame count and size limits
|
|
91
|
+
*
|
|
92
|
+
* Returns true if frame is acceptable, false if connection should be closed
|
|
93
|
+
*/
|
|
94
|
+
export function trackPreAuthFrame(state, frameSize) {
|
|
95
|
+
// Check frame count limit
|
|
96
|
+
if (state.frameCount >= state.config.maxPreAuthFrames) {
|
|
97
|
+
return {
|
|
98
|
+
accepted: false,
|
|
99
|
+
reason: `Max pre-auth frames exceeded: ${state.frameCount} (max: ${state.config.maxPreAuthFrames})`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Validate frame size
|
|
103
|
+
const sizeValidation = validatePreAuthFrameSize(state, frameSize);
|
|
104
|
+
if (!sizeValidation.valid) {
|
|
105
|
+
return { accepted: false, reason: sizeValidation.reason };
|
|
106
|
+
}
|
|
107
|
+
// Update counters
|
|
108
|
+
state.frameCount++;
|
|
109
|
+
state.totalBytesReceived += frameSize;
|
|
110
|
+
return { accepted: true };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Mark connection as authenticated
|
|
114
|
+
*
|
|
115
|
+
* Security: Clear pre-auth restrictions after successful authentication
|
|
116
|
+
*/
|
|
117
|
+
export function markAuthenticated(state) {
|
|
118
|
+
state.authenticated = true;
|
|
119
|
+
clearHandshakeTimeout(state);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Check if connection is still in pre-auth state
|
|
123
|
+
*/
|
|
124
|
+
export function isPreAuth(state) {
|
|
125
|
+
return !state.authenticated;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get connection age in milliseconds
|
|
129
|
+
*/
|
|
130
|
+
export function getConnectionAge(state) {
|
|
131
|
+
return Date.now() - state.createdAt;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Analyze WebSocket connection security
|
|
135
|
+
*/
|
|
136
|
+
export function analyzeWebSocketSecurity(state) {
|
|
137
|
+
const age = getConnectionAge(state);
|
|
138
|
+
const timeoutRemaining = state.handshakeTimeout
|
|
139
|
+
? Math.max(0, state.config.handshakeTimeoutMs - age)
|
|
140
|
+
: undefined;
|
|
141
|
+
// Determine security level
|
|
142
|
+
let securityLevel;
|
|
143
|
+
if (!state.authenticated && age > state.config.handshakeTimeoutMs) {
|
|
144
|
+
securityLevel = "critical"; // Overdue for auth
|
|
145
|
+
}
|
|
146
|
+
else if (!state.authenticated && age > state.config.handshakeTimeoutMs / 2) {
|
|
147
|
+
securityLevel = "low"; // Approaching timeout
|
|
148
|
+
}
|
|
149
|
+
else if (!state.authenticated) {
|
|
150
|
+
securityLevel = "medium"; // Normal pre-auth
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
securityLevel = "high"; // Authenticated
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
isPreAuth: isPreAuth(state),
|
|
157
|
+
connectionAge: age,
|
|
158
|
+
frameCount: state.frameCount,
|
|
159
|
+
totalBytesReceived: state.totalBytesReceived,
|
|
160
|
+
handshakeTimeoutRemaining: timeoutRemaining,
|
|
161
|
+
securityLevel,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get security event message for logging
|
|
166
|
+
*/
|
|
167
|
+
export function getSecurityEventMessage(event, state) {
|
|
168
|
+
const analysis = analyzeWebSocketSecurity(state);
|
|
169
|
+
switch (event) {
|
|
170
|
+
case "handshake_timeout":
|
|
171
|
+
return `WebSocket handshake timeout after ${analysis.connectionAge}ms (pre-auth)`;
|
|
172
|
+
case "frame_size_exceeded":
|
|
173
|
+
return `WebSocket pre-auth frame size exceeded: ${analysis.totalBytesReceived} bytes`;
|
|
174
|
+
case "frame_count_exceeded":
|
|
175
|
+
return `WebSocket pre-auth frame count exceeded: ${analysis.frameCount} frames`;
|
|
176
|
+
case "authenticated":
|
|
177
|
+
return `WebSocket authenticated successfully after ${analysis.connectionAge}ms`;
|
|
178
|
+
default:
|
|
179
|
+
return `WebSocket security event: ${event}`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Cleanup pre-auth connection state
|
|
184
|
+
*/
|
|
185
|
+
export function cleanupPreAuthConnectionState(state) {
|
|
186
|
+
clearHandshakeTimeout(state);
|
|
187
|
+
state.ws.removeAllListeners();
|
|
188
|
+
}
|
package/dist/infra/errors.js
CHANGED
|
@@ -1,13 +1,46 @@
|
|
|
1
|
+
import { redactSensitiveText } from "../logging/redact.js";
|
|
1
2
|
export function extractErrorCode(err) {
|
|
2
|
-
if (!err || typeof err !== "object")
|
|
3
|
+
if (!err || typeof err !== "object") {
|
|
3
4
|
return undefined;
|
|
5
|
+
}
|
|
4
6
|
const code = err.code;
|
|
5
|
-
if (typeof code === "string")
|
|
7
|
+
if (typeof code === "string") {
|
|
6
8
|
return code;
|
|
7
|
-
|
|
9
|
+
}
|
|
10
|
+
if (typeof code === "number") {
|
|
8
11
|
return String(code);
|
|
12
|
+
}
|
|
9
13
|
return undefined;
|
|
10
14
|
}
|
|
15
|
+
export function readErrorName(err) {
|
|
16
|
+
if (!err || typeof err !== "object") {
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
const name = err.name;
|
|
20
|
+
return typeof name === "string" ? name : "";
|
|
21
|
+
}
|
|
22
|
+
export function collectErrorGraphCandidates(err, resolveNested) {
|
|
23
|
+
const queue = [err];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
const candidates = [];
|
|
26
|
+
while (queue.length > 0) {
|
|
27
|
+
const current = queue.shift();
|
|
28
|
+
if (current == null || seen.has(current)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
seen.add(current);
|
|
32
|
+
candidates.push(current);
|
|
33
|
+
if (!current || typeof current !== "object" || !resolveNested) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
for (const nested of resolveNested(current)) {
|
|
37
|
+
if (nested != null && !seen.has(nested)) {
|
|
38
|
+
queue.push(nested);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return candidates;
|
|
43
|
+
}
|
|
11
44
|
/**
|
|
12
45
|
* Type guard for NodeJS.ErrnoException (any error with a `code` property).
|
|
13
46
|
*/
|
|
@@ -21,27 +54,34 @@ export function hasErrnoCode(err, code) {
|
|
|
21
54
|
return isErrno(err) && err.code === code;
|
|
22
55
|
}
|
|
23
56
|
export function formatErrorMessage(err) {
|
|
57
|
+
let formatted;
|
|
24
58
|
if (err instanceof Error) {
|
|
25
|
-
|
|
59
|
+
formatted = err.message || err.name || "Error";
|
|
26
60
|
}
|
|
27
|
-
if (typeof err === "string")
|
|
28
|
-
|
|
29
|
-
if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") {
|
|
30
|
-
return String(err);
|
|
61
|
+
else if (typeof err === "string") {
|
|
62
|
+
formatted = err;
|
|
31
63
|
}
|
|
32
|
-
|
|
33
|
-
|
|
64
|
+
else if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") {
|
|
65
|
+
formatted = String(err);
|
|
34
66
|
}
|
|
35
|
-
|
|
36
|
-
|
|
67
|
+
else {
|
|
68
|
+
try {
|
|
69
|
+
formatted = JSON.stringify(err);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
formatted = Object.prototype.toString.call(err);
|
|
73
|
+
}
|
|
37
74
|
}
|
|
75
|
+
// Security: best-effort token redaction before returning/logging.
|
|
76
|
+
return redactSensitiveText(formatted);
|
|
38
77
|
}
|
|
39
78
|
export function formatUncaughtError(err) {
|
|
40
79
|
if (extractErrorCode(err) === "INVALID_CONFIG") {
|
|
41
80
|
return formatErrorMessage(err);
|
|
42
81
|
}
|
|
43
82
|
if (err instanceof Error) {
|
|
44
|
-
|
|
83
|
+
const stack = err.stack ?? err.message ?? err.name;
|
|
84
|
+
return redactSensitiveText(stack);
|
|
45
85
|
}
|
|
46
86
|
return formatErrorMessage(err);
|
|
47
87
|
}
|