@poolzin/pool-bot 2026.3.21 → 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.
Files changed (124) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/acp/bindings-store.js +209 -0
  3. package/dist/acp/control-plane/runtime-cache.js +54 -0
  4. package/dist/acp/control-plane/runtime-options.js +215 -0
  5. package/dist/acp/control-plane/session-actor-queue.js +36 -0
  6. package/dist/acp/runtime/errors.js +47 -0
  7. package/dist/acp/runtime/registry.js +86 -0
  8. package/dist/acp/runtime/types.js +1 -0
  9. package/dist/acp/translator.js +97 -0
  10. package/dist/agents/failover-error.js +145 -47
  11. package/dist/browser/browser-profile-manager.js +319 -0
  12. package/dist/browser/cdp-proxy-bypass.js +129 -0
  13. package/dist/browser/cdp-timeouts.js +41 -0
  14. package/dist/browser/chrome-extension-validator.js +406 -0
  15. package/dist/browser/chrome-mcp-snapshot.js +222 -0
  16. package/dist/browser/chrome-mcp.js +421 -0
  17. package/dist/browser/chrome-mcp.snapshot.js +133 -0
  18. package/dist/browser/errors.js +67 -0
  19. package/dist/browser/form-fields.js +22 -0
  20. package/dist/browser/output-atomic.js +44 -0
  21. package/dist/browser/profile-capabilities.js +47 -0
  22. package/dist/browser/safe-filename.js +25 -0
  23. package/dist/browser/snapshot-roles.js +60 -0
  24. package/dist/build-info.json +3 -3
  25. package/dist/commands/security-owner-only.js +86 -0
  26. package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
  27. package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
  28. package/dist/control-ui/index.html +1 -1
  29. package/dist/cron/cron-filters.js +150 -0
  30. package/dist/gateway/device-pairing-security.js +197 -0
  31. package/dist/gateway/event-deduplication.js +167 -0
  32. package/dist/gateway/run-tracker.js +253 -0
  33. package/dist/gateway/server-methods/nodes.js +14 -0
  34. package/dist/gateway/websocket-preauth-security.js +188 -0
  35. package/dist/infra/errors.js +53 -13
  36. package/dist/infra/exec-approvals-security.js +217 -0
  37. package/dist/infra/security/command-analyzer.js +257 -0
  38. package/dist/plugins/loader.js +16 -8
  39. package/dist/security/external-content.js +51 -1
  40. package/dist/sessions/session-costs.js +228 -0
  41. package/dist/shared/param-key.js +16 -0
  42. package/dist/shared/poll-params.js +58 -0
  43. package/dist/shared/polls.js +55 -0
  44. package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
  45. package/docs/FEATURES.md +523 -0
  46. package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
  47. package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
  48. package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
  49. package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
  50. package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
  51. package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
  52. package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
  53. package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
  54. package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
  55. package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
  56. package/docs/MIKRODASH-ANALYSIS.md +412 -0
  57. package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
  58. package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
  59. package/docs/PHASE-7-SUMMARY.md +144 -0
  60. package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
  61. package/docs/PROJECT-FINAL-STATUS.md +237 -0
  62. package/docs/README.md +116 -0
  63. package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
  64. package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
  65. package/docs/channels/googlechat.md +235 -206
  66. package/docs/channels/irc.md +332 -0
  67. package/docs/channels/nostr.md +255 -168
  68. package/docs/components/command-palette.md +166 -0
  69. package/docs/components/login-gate.md +219 -0
  70. package/docs/getting-started/installation.md +191 -0
  71. package/docs/getting-started/introduction.md +120 -0
  72. package/docs/improvements/USAGE-GUIDE.md +359 -0
  73. package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
  74. package/docs/reference/deadcode-detection.md +72 -0
  75. package/extensions/acpx/node_modules/.bin/acpx +21 -0
  76. package/extensions/agency-agents/node_modules/.bin/vite +4 -4
  77. package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
  78. package/extensions/googlechat/node_modules/.bin/tsc +21 -0
  79. package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
  80. package/extensions/googlechat/node_modules/.bin/vitest +21 -0
  81. package/extensions/googlechat/package.json +11 -28
  82. package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
  83. package/extensions/googlechat/src/googlechat-channel.ts +120 -0
  84. package/extensions/googlechat/src/index.ts +14 -0
  85. package/extensions/irc/node_modules/.bin/tsc +21 -0
  86. package/extensions/irc/node_modules/.bin/tsserver +21 -0
  87. package/extensions/irc/node_modules/.bin/vitest +21 -0
  88. package/extensions/irc/package.json +16 -8
  89. package/extensions/irc/src/index.ts +14 -0
  90. package/extensions/irc/src/irc-channel.test.ts +43 -0
  91. package/extensions/irc/src/irc-channel.ts +191 -0
  92. package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
  93. package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
  94. package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
  95. package/extensions/keyed-async-queue/package.json +20 -0
  96. package/extensions/keyed-async-queue/src/index.ts +14 -0
  97. package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
  98. package/extensions/keyed-async-queue/src/queue.ts +200 -0
  99. package/extensions/memory-core/node_modules/.bin/tsc +21 -0
  100. package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
  101. package/extensions/memory-core/node_modules/.bin/vitest +21 -0
  102. package/extensions/memory-core/package.json +11 -8
  103. package/extensions/memory-core/src/index.ts +14 -0
  104. package/extensions/memory-core/src/memory-manager.test.ts +124 -0
  105. package/extensions/memory-core/src/memory-manager.ts +186 -0
  106. package/extensions/nostr/node_modules/.bin/tsc +2 -2
  107. package/extensions/nostr/node_modules/.bin/tsserver +2 -2
  108. package/extensions/nostr/node_modules/.bin/vitest +21 -0
  109. package/extensions/nostr/package.json +15 -24
  110. package/extensions/nostr/src/index.ts +14 -0
  111. package/extensions/nostr/src/nostr-channel.test.ts +55 -0
  112. package/extensions/nostr/src/nostr-channel.ts +228 -0
  113. package/extensions/page-agent/node_modules/.bin/vitest +2 -2
  114. package/extensions/test-utils/node_modules/.bin/jiti +21 -0
  115. package/extensions/test-utils/node_modules/.bin/playwright +21 -0
  116. package/extensions/test-utils/node_modules/.bin/tsx +21 -0
  117. package/extensions/test-utils/node_modules/.bin/vite +21 -0
  118. package/extensions/test-utils/node_modules/.bin/vitest +21 -0
  119. package/extensions/test-utils/node_modules/.bin/yaml +21 -0
  120. package/extensions/xyops/node_modules/.bin/vitest +2 -2
  121. package/package.json +2 -1
  122. package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
  123. package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
  124. 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
+ }
@@ -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
- if (typeof code === "number")
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
- return err.message || err.name || "Error";
59
+ formatted = err.message || err.name || "Error";
26
60
  }
27
- if (typeof err === "string")
28
- return err;
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
- try {
33
- return JSON.stringify(err);
64
+ else if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") {
65
+ formatted = String(err);
34
66
  }
35
- catch {
36
- return Object.prototype.toString.call(err);
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
- return err.stack ?? err.message ?? err.name;
83
+ const stack = err.stack ?? err.message ?? err.name;
84
+ return redactSensitiveText(stack);
45
85
  }
46
86
  return formatErrorMessage(err);
47
87
  }