@poolzin/pool-bot 2026.3.22 → 2026.3.24

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 (159) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/.buildstamp +1 -1
  3. package/dist/acp/bindings-store.js +209 -0
  4. package/dist/acp/control-plane/runtime-cache.js +54 -0
  5. package/dist/acp/control-plane/runtime-options.js +215 -0
  6. package/dist/acp/control-plane/session-actor-queue.js +36 -0
  7. package/dist/acp/policy.js +52 -0
  8. package/dist/acp/runtime/errors.js +47 -0
  9. package/dist/acp/runtime/registry.js +86 -0
  10. package/dist/acp/runtime/types.js +1 -0
  11. package/dist/acp/translator.js +97 -0
  12. package/dist/agents/btw.js +280 -0
  13. package/dist/agents/failover-error.js +145 -47
  14. package/dist/agents/fast-mode.js +24 -0
  15. package/dist/agents/live-model-errors.js +23 -0
  16. package/dist/agents/model-auth-env-vars.js +44 -0
  17. package/dist/agents/model-auth-markers.js +69 -0
  18. package/dist/agents/models-config.providers.discovery.js +180 -0
  19. package/dist/agents/models-config.providers.static.js +480 -0
  20. package/dist/auto-reply/reply/typing-policy.js +15 -0
  21. package/dist/browser/browser-profile-manager.js +319 -0
  22. package/dist/browser/cdp-proxy-bypass.js +129 -0
  23. package/dist/browser/cdp-timeouts.js +41 -0
  24. package/dist/browser/chrome-extension-validator.js +406 -0
  25. package/dist/browser/chrome-mcp-snapshot.js +222 -0
  26. package/dist/browser/chrome-mcp.js +421 -0
  27. package/dist/browser/chrome-mcp.snapshot.js +133 -0
  28. package/dist/browser/errors.js +67 -0
  29. package/dist/browser/form-fields.js +22 -0
  30. package/dist/browser/output-atomic.js +44 -0
  31. package/dist/browser/profile-capabilities.js +47 -0
  32. package/dist/browser/safe-filename.js +25 -0
  33. package/dist/browser/snapshot-roles.js +60 -0
  34. package/dist/build-info.json +3 -3
  35. package/dist/channels/account-snapshot-fields.js +176 -0
  36. package/dist/channels/draft-stream-controls.js +89 -0
  37. package/dist/channels/inbound-debounce-policy.js +28 -0
  38. package/dist/channels/typing-lifecycle.js +39 -0
  39. package/dist/cli/program/command-registry.js +52 -0
  40. package/dist/commands/agent-binding.js +123 -0
  41. package/dist/commands/agents.commands.bind.js +280 -0
  42. package/dist/commands/backup-shared.js +186 -0
  43. package/dist/commands/backup-verify.js +236 -0
  44. package/dist/commands/backup.js +166 -0
  45. package/dist/commands/channel-account-context.js +15 -0
  46. package/dist/commands/channel-account.js +190 -0
  47. package/dist/commands/gateway-install-token.js +117 -0
  48. package/dist/commands/oauth-tls-preflight.js +121 -0
  49. package/dist/commands/ollama-setup.js +402 -0
  50. package/dist/commands/security-owner-only.js +86 -0
  51. package/dist/commands/self-hosted-provider-setup.js +207 -0
  52. package/dist/commands/session-store-targets.js +12 -0
  53. package/dist/commands/sessions-cleanup.js +97 -0
  54. package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
  55. package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
  56. package/dist/control-ui/index.html +1 -1
  57. package/dist/cron/cron-filters.js +150 -0
  58. package/dist/cron/heartbeat-policy.js +26 -0
  59. package/dist/gateway/device-pairing-security.js +197 -0
  60. package/dist/gateway/event-deduplication.js +167 -0
  61. package/dist/gateway/hooks-mapping.js +46 -7
  62. package/dist/gateway/run-tracker.js +253 -0
  63. package/dist/gateway/server-methods/nodes.js +14 -0
  64. package/dist/gateway/websocket-preauth-security.js +188 -0
  65. package/dist/hooks/module-loader.js +28 -0
  66. package/dist/infra/agent-command-binding.js +144 -0
  67. package/dist/infra/backup.js +328 -0
  68. package/dist/infra/channel-account-context.js +173 -0
  69. package/dist/infra/errors.js +53 -13
  70. package/dist/infra/exec-approvals-security.js +217 -0
  71. package/dist/infra/security/command-analyzer.js +257 -0
  72. package/dist/infra/session-cleanup.js +143 -0
  73. package/dist/plugins/loader.js +16 -8
  74. package/dist/security/external-content.js +51 -1
  75. package/dist/sessions/session-costs.js +228 -0
  76. package/dist/shared/param-key.js +16 -0
  77. package/dist/shared/poll-params.js +58 -0
  78. package/dist/shared/polls.js +55 -0
  79. package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
  80. package/docs/FEATURES.md +523 -0
  81. package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
  82. package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
  83. package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
  84. package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
  85. package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
  86. package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
  87. package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
  88. package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
  89. package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
  90. package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
  91. package/docs/MIKRODASH-ANALYSIS.md +412 -0
  92. package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
  93. package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
  94. package/docs/PHASE-7-SUMMARY.md +144 -0
  95. package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
  96. package/docs/PROJECT-FINAL-STATUS.md +237 -0
  97. package/docs/README.md +116 -0
  98. package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
  99. package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
  100. package/docs/channels/googlechat.md +235 -206
  101. package/docs/channels/irc.md +332 -0
  102. package/docs/channels/nostr.md +255 -168
  103. package/docs/components/command-palette.md +166 -0
  104. package/docs/components/login-gate.md +219 -0
  105. package/docs/getting-started/installation.md +191 -0
  106. package/docs/getting-started/introduction.md +120 -0
  107. package/docs/improvements/USAGE-GUIDE.md +359 -0
  108. package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
  109. package/docs/reference/deadcode-detection.md +72 -0
  110. package/extensions/acpx/node_modules/.bin/acpx +21 -0
  111. package/extensions/agency-agents/node_modules/.bin/vite +4 -4
  112. package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
  113. package/extensions/googlechat/node_modules/.bin/tsc +21 -0
  114. package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
  115. package/extensions/googlechat/node_modules/.bin/vitest +21 -0
  116. package/extensions/googlechat/package.json +11 -28
  117. package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
  118. package/extensions/googlechat/src/googlechat-channel.ts +120 -0
  119. package/extensions/googlechat/src/index.ts +14 -0
  120. package/extensions/irc/node_modules/.bin/tsc +21 -0
  121. package/extensions/irc/node_modules/.bin/tsserver +21 -0
  122. package/extensions/irc/node_modules/.bin/vitest +21 -0
  123. package/extensions/irc/package.json +16 -8
  124. package/extensions/irc/src/index.ts +14 -0
  125. package/extensions/irc/src/irc-channel.test.ts +43 -0
  126. package/extensions/irc/src/irc-channel.ts +191 -0
  127. package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
  128. package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
  129. package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
  130. package/extensions/keyed-async-queue/package.json +20 -0
  131. package/extensions/keyed-async-queue/src/index.ts +14 -0
  132. package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
  133. package/extensions/keyed-async-queue/src/queue.ts +200 -0
  134. package/extensions/memory-core/node_modules/.bin/tsc +21 -0
  135. package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
  136. package/extensions/memory-core/node_modules/.bin/vitest +21 -0
  137. package/extensions/memory-core/package.json +11 -8
  138. package/extensions/memory-core/src/index.ts +14 -0
  139. package/extensions/memory-core/src/memory-manager.test.ts +124 -0
  140. package/extensions/memory-core/src/memory-manager.ts +186 -0
  141. package/extensions/nostr/node_modules/.bin/tsc +2 -2
  142. package/extensions/nostr/node_modules/.bin/tsserver +2 -2
  143. package/extensions/nostr/node_modules/.bin/vitest +21 -0
  144. package/extensions/nostr/package.json +15 -24
  145. package/extensions/nostr/src/index.ts +14 -0
  146. package/extensions/nostr/src/nostr-channel.test.ts +55 -0
  147. package/extensions/nostr/src/nostr-channel.ts +228 -0
  148. package/extensions/page-agent/node_modules/.bin/vitest +2 -2
  149. package/extensions/test-utils/node_modules/.bin/jiti +21 -0
  150. package/extensions/test-utils/node_modules/.bin/playwright +21 -0
  151. package/extensions/test-utils/node_modules/.bin/tsx +21 -0
  152. package/extensions/test-utils/node_modules/.bin/vite +21 -0
  153. package/extensions/test-utils/node_modules/.bin/vitest +21 -0
  154. package/extensions/test-utils/node_modules/.bin/yaml +21 -0
  155. package/extensions/xyops/node_modules/.bin/vitest +2 -2
  156. package/package.json +2 -1
  157. package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
  158. package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
  159. package/extensions/memory-core/node_modules/.bin/poolbot +0 -21
@@ -6,7 +6,7 @@
6
6
  <title>Poolbot Control</title>
7
7
  <meta name="color-scheme" content="dark light" />
8
8
  <link rel="icon" href="./favicon.ico" sizes="any" />
9
- <script type="module" crossorigin src="./assets/index-Dvkl4Xlx.js"></script>
9
+ <script type="module" crossorigin src="./assets/index-D7shnQwQ.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="./assets/index-CSfXd2LO.css">
11
11
  </head>
12
12
  <body>
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Advanced Cron Filters
3
+ * Filter cron jobs by channel, user, session, and other criteria
4
+ */
5
+ /**
6
+ * Check if a cron job matches a filter
7
+ */
8
+ export function matchesFilter(job, filter) {
9
+ if (!filter.enabled) {
10
+ return true; // Disabled filters don't block
11
+ }
12
+ const criteria = filter.criteria;
13
+ // Channel filters
14
+ if (criteria.channelId && job.channelId !== criteria.channelId) {
15
+ return false;
16
+ }
17
+ if (criteria.channelIds && !criteria.channelIds.includes(job.channelId || "")) {
18
+ return false;
19
+ }
20
+ if (criteria.channelType) {
21
+ // Would need channel type from job metadata
22
+ const jobType = job.metadata?.channelType || "";
23
+ if (jobType !== criteria.channelType) {
24
+ return false;
25
+ }
26
+ }
27
+ // User filters
28
+ if (criteria.userId && job.userId !== criteria.userId) {
29
+ return false;
30
+ }
31
+ if (criteria.userIds && !criteria.userIds.includes(job.userId || "")) {
32
+ return false;
33
+ }
34
+ if (criteria.userRole) {
35
+ const jobRole = job.metadata?.userRole || "";
36
+ if (jobRole !== criteria.userRole) {
37
+ return false;
38
+ }
39
+ }
40
+ // Session filters
41
+ if (criteria.sessionId && job.sessionId !== criteria.sessionId) {
42
+ return false;
43
+ }
44
+ if (criteria.sessionIds && !criteria.sessionIds.includes(job.sessionId || "")) {
45
+ return false;
46
+ }
47
+ if (criteria.sessionMode) {
48
+ const jobMode = job.metadata?.sessionMode || "normal";
49
+ if (jobMode !== criteria.sessionMode) {
50
+ return false;
51
+ }
52
+ }
53
+ // Agent filters
54
+ if (criteria.agentId && job.agentId !== criteria.agentId) {
55
+ return false;
56
+ }
57
+ if (criteria.agentIds && !criteria.agentIds.includes(job.agentId || "")) {
58
+ return false;
59
+ }
60
+ // Custom filters
61
+ if (criteria.custom) {
62
+ for (const [key, value] of Object.entries(criteria.custom)) {
63
+ const jobValue = job.metadata?.[key];
64
+ if (jobValue !== value) {
65
+ return false;
66
+ }
67
+ }
68
+ }
69
+ return true;
70
+ }
71
+ /**
72
+ * Filter cron jobs by multiple filters
73
+ */
74
+ export function filterCronJobs(jobs, filters) {
75
+ return jobs.filter((job) => {
76
+ // Job must match ALL enabled filters
77
+ for (const filter of filters) {
78
+ if (!matchesFilter(job, filter)) {
79
+ return false;
80
+ }
81
+ }
82
+ return true;
83
+ });
84
+ }
85
+ /**
86
+ * Create a new cron filter
87
+ */
88
+ export function createCronFilter(type, criteria, enabled = true) {
89
+ return {
90
+ id: `filter_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
91
+ type,
92
+ enabled,
93
+ criteria,
94
+ createdAt: Date.now(),
95
+ };
96
+ }
97
+ /**
98
+ * Update an existing cron filter
99
+ */
100
+ export function updateCronFilter(filter, updates) {
101
+ return {
102
+ ...filter,
103
+ ...updates,
104
+ updatedAt: Date.now(),
105
+ };
106
+ }
107
+ /**
108
+ * Validate a cron filter
109
+ */
110
+ export function validateCronFilter(filter) {
111
+ const errors = [];
112
+ // Check required fields
113
+ if (!filter.id) {
114
+ errors.push("Filter ID is required");
115
+ }
116
+ if (!filter.type) {
117
+ errors.push("Filter type is required");
118
+ }
119
+ if (!["channel", "user", "session", "agent", "custom"].includes(filter.type)) {
120
+ errors.push(`Invalid filter type: ${filter.type}`);
121
+ }
122
+ // Check criteria based on type
123
+ if (filter.type === "channel" && !filter.criteria.channelId && !filter.criteria.channelIds) {
124
+ errors.push("Channel filter requires channelId or channelIds");
125
+ }
126
+ if (filter.type === "user" && !filter.criteria.userId && !filter.criteria.userIds) {
127
+ errors.push("User filter requires userId or userIds");
128
+ }
129
+ if (filter.type === "session" && !filter.criteria.sessionId && !filter.criteria.sessionIds) {
130
+ errors.push("Session filter requires sessionId or sessionIds");
131
+ }
132
+ return {
133
+ valid: errors.length === 0,
134
+ errors,
135
+ };
136
+ }
137
+ /**
138
+ * Get filter statistics
139
+ */
140
+ export function getFilterStats(filters) {
141
+ return {
142
+ total: filters.length,
143
+ enabled: filters.filter((f) => f.enabled).length,
144
+ disabled: filters.filter((f) => !f.enabled).length,
145
+ byType: filters.reduce((acc, f) => {
146
+ acc[f.type] = (acc[f.type] || 0) + 1;
147
+ return acc;
148
+ }, {}),
149
+ };
150
+ }
@@ -0,0 +1,26 @@
1
+ import { stripHeartbeatToken } from "../auto-reply/heartbeat.js";
2
+ export function shouldSkipHeartbeatOnlyDelivery(payloads, ackMaxChars) {
3
+ if (payloads.length === 0) {
4
+ return true;
5
+ }
6
+ const hasAnyMedia = payloads.some((payload) => (payload.mediaUrls?.length ?? 0) > 0 || Boolean(payload.mediaUrl));
7
+ if (hasAnyMedia) {
8
+ return false;
9
+ }
10
+ return payloads.some((payload) => {
11
+ const result = stripHeartbeatToken(payload.text, {
12
+ mode: "heartbeat",
13
+ maxAckChars: ackMaxChars,
14
+ });
15
+ return result.shouldSkip;
16
+ });
17
+ }
18
+ export function shouldEnqueueCronMainSummary(params) {
19
+ const summaryText = params.summaryText?.trim();
20
+ return Boolean(summaryText &&
21
+ params.isCronSystemEvent(summaryText) &&
22
+ params.deliveryRequested &&
23
+ !params.delivered &&
24
+ params.deliveryAttempted !== true &&
25
+ !params.suppressMainSummary);
26
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Device Pairing Security Hardening
3
+ *
4
+ * Security fixes for device pairing:
5
+ * - Single-use bootstrap setup codes
6
+ * - Short-lived bootstrap tokens
7
+ * - Fail-closed for reused tokens
8
+ */
9
+ import crypto from "node:crypto";
10
+ const DEFAULT_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
11
+ const CLEANUP_INTERVAL_MS = 60 * 1000; // 1 minute
12
+ /**
13
+ * Generate a secure single-use setup code
14
+ *
15
+ * Format: XXXX-YYYY (8 chars, 2 groups for readability)
16
+ * Entropy: ~36 bits (sufficient for short-lived codes)
17
+ */
18
+ export function generateSetupCode() {
19
+ const bytes = crypto.randomBytes(4);
20
+ const part1 = bytes.readUInt16BE(0) % 10000;
21
+ const part2 = bytes.readUInt16BE(2) % 10000;
22
+ return `${part1.toString().padStart(4, '0')}-${part2.toString().padStart(4, '0')}`;
23
+ }
24
+ /**
25
+ * Create a new single-use bootstrap token
26
+ */
27
+ export function createBootstrapToken(params) {
28
+ const expiryMs = params?.expiryMs ?? DEFAULT_EXPIRY_MS;
29
+ const purpose = params?.purpose ?? "pairing";
30
+ const now = Date.now();
31
+ return {
32
+ id: crypto.randomUUID(),
33
+ code: generateSetupCode(),
34
+ createdAt: now,
35
+ expiresAt: now + expiryMs,
36
+ used: false,
37
+ purpose,
38
+ };
39
+ }
40
+ /**
41
+ * Initialize bootstrap token store with automatic cleanup
42
+ */
43
+ export function createBootstrapTokenStore() {
44
+ const store = {
45
+ tokens: new Map(),
46
+ };
47
+ // Start automatic cleanup
48
+ store.cleanupInterval = setInterval(() => {
49
+ cleanupExpiredTokens(store);
50
+ }, CLEANUP_INTERVAL_MS);
51
+ // Cleanup on process exit
52
+ process.on("exit", () => {
53
+ if (store.cleanupInterval) {
54
+ clearInterval(store.cleanupInterval);
55
+ }
56
+ });
57
+ return store;
58
+ }
59
+ /**
60
+ * Add a bootstrap token to the store
61
+ */
62
+ export function addBootstrapToken(store, token) {
63
+ store.tokens.set(token.id, token);
64
+ }
65
+ /**
66
+ * Validate and consume a bootstrap token (single-use)
67
+ *
68
+ * Returns true if token is valid and was consumed, false otherwise
69
+ * Marks token as used to prevent reuse
70
+ */
71
+ export function validateAndConsumeBootstrapToken(params) {
72
+ const { store, tokenId, deviceId } = params;
73
+ const token = store.tokens.get(tokenId);
74
+ if (!token) {
75
+ return false;
76
+ }
77
+ const now = Date.now();
78
+ // Check if already used (single-use enforcement)
79
+ if (token.used) {
80
+ return false;
81
+ }
82
+ // Check if expired
83
+ if (now > token.expiresAt) {
84
+ // Remove expired token
85
+ store.tokens.delete(tokenId);
86
+ return false;
87
+ }
88
+ // Mark as used
89
+ token.used = true;
90
+ token.usedAt = now;
91
+ token.deviceId = deviceId; // Store for audit trail (marked as _deviceId to satisfy linter)
92
+ const _deviceId = deviceId; // Keep for future audit log expansion
93
+ return true;
94
+ }
95
+ /**
96
+ * Validate a bootstrap token without consuming it (for status checks)
97
+ */
98
+ export function validateBootstrapToken(params) {
99
+ const { store, tokenId } = params;
100
+ const token = store.tokens.get(tokenId);
101
+ if (!token) {
102
+ return { valid: false, reason: "token_not_found" };
103
+ }
104
+ const now = Date.now();
105
+ if (token.used) {
106
+ return { valid: false, reason: "token_already_used" };
107
+ }
108
+ if (now > token.expiresAt) {
109
+ return { valid: false, reason: "token_expired" };
110
+ }
111
+ return { valid: true };
112
+ }
113
+ /**
114
+ * Remove a bootstrap token from the store
115
+ */
116
+ export function removeBootstrapToken(store, tokenId) {
117
+ return store.tokens.delete(tokenId);
118
+ }
119
+ /**
120
+ * Cleanup expired and used tokens
121
+ */
122
+ export function cleanupExpiredTokens(store) {
123
+ const now = Date.now();
124
+ let removed = 0;
125
+ for (const [tokenId, token] of store.tokens.entries()) {
126
+ // Remove if expired or used (single-use tokens are one-time only)
127
+ if (now > token.expiresAt || token.used) {
128
+ store.tokens.delete(tokenId);
129
+ removed++;
130
+ }
131
+ }
132
+ return removed;
133
+ }
134
+ /**
135
+ * Get statistics about bootstrap tokens
136
+ */
137
+ export function getBootstrapTokenStats(store) {
138
+ const now = Date.now();
139
+ let unused = 0;
140
+ let used = 0;
141
+ let expired = 0;
142
+ for (const token of store.tokens.values()) {
143
+ if (token.used) {
144
+ used++;
145
+ }
146
+ else if (now > token.expiresAt) {
147
+ expired++;
148
+ }
149
+ else {
150
+ unused++;
151
+ }
152
+ }
153
+ return {
154
+ total: store.tokens.size,
155
+ unused,
156
+ used,
157
+ expired,
158
+ };
159
+ }
160
+ /**
161
+ * Create a secure device pairing session
162
+ */
163
+ export function createSecureDevicePairing(store) {
164
+ const token = createBootstrapToken({
165
+ expiryMs: DEFAULT_EXPIRY_MS,
166
+ purpose: "pairing",
167
+ });
168
+ addBootstrapToken(store, token);
169
+ return {
170
+ setupCode: token.code,
171
+ tokenId: token.id,
172
+ expiresAt: token.expiresAt,
173
+ isSingleUse: true,
174
+ securityLevel: "high",
175
+ };
176
+ }
177
+ /**
178
+ * Validate device pairing request with security checks
179
+ */
180
+ export function validateDevicePairingRequest(params) {
181
+ const { store, tokenId, deviceId, setupCode } = params;
182
+ // First validate the token
183
+ const tokenValidation = validateBootstrapToken({ store, tokenId });
184
+ if (!tokenValidation.valid) {
185
+ return tokenValidation;
186
+ }
187
+ // Get the token and verify setup code
188
+ const token = store.tokens.get(tokenId);
189
+ if (!token) {
190
+ return { valid: false, reason: "token_not_found" };
191
+ }
192
+ // Verify setup code matches
193
+ if (token.code !== setupCode) {
194
+ return { valid: false, reason: "setup_code_mismatch" };
195
+ }
196
+ return { valid: true };
197
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Event Deduplication Layer
3
+ *
4
+ * Prevents duplicate event processing within a configurable time window.
5
+ * Uses LRU-style eviction to prevent memory leaks.
6
+ */
7
+ export class EventDeduplicator {
8
+ seen = new Map();
9
+ TTL_MS;
10
+ MAX_SIZE;
11
+ cleanupInterval = null;
12
+ constructor(options) {
13
+ this.TTL_MS = options?.ttlMs ?? 5000; // 5 segundos default
14
+ this.MAX_SIZE = options?.maxSize ?? 10000;
15
+ if (options?.autoCleanup !== false) {
16
+ this.startCleanupInterval();
17
+ }
18
+ }
19
+ /**
20
+ * Check if event is duplicate and mark as seen
21
+ * @param eventId - Unique event identifier
22
+ * @returns true if duplicate (seen within TTL), false if new
23
+ */
24
+ isDuplicate(eventId) {
25
+ const now = Date.now();
26
+ const existing = this.seen.get(eventId);
27
+ if (existing && now - existing.timestamp < this.TTL_MS) {
28
+ existing.count++;
29
+ return true;
30
+ }
31
+ // Evict oldest if at capacity
32
+ if (this.seen.size >= this.MAX_SIZE) {
33
+ this.evictOldest();
34
+ }
35
+ this.seen.set(eventId, { timestamp: now, count: 1 });
36
+ return false;
37
+ }
38
+ /**
39
+ * Check if event was seen without marking it
40
+ */
41
+ hasSeen(eventId) {
42
+ const existing = this.seen.get(eventId);
43
+ if (!existing)
44
+ return false;
45
+ return Date.now() - existing.timestamp < this.TTL_MS;
46
+ }
47
+ /**
48
+ * Get duplicate count for an event
49
+ */
50
+ getDuplicateCount(eventId) {
51
+ const existing = this.seen.get(eventId);
52
+ return existing?.count ?? 0;
53
+ }
54
+ /**
55
+ * Clear specific event from seen set
56
+ */
57
+ clear(eventId) {
58
+ this.seen.delete(eventId);
59
+ }
60
+ /**
61
+ * Clear all seen events
62
+ */
63
+ clearAll() {
64
+ this.seen.clear();
65
+ }
66
+ /**
67
+ * Get statistics about deduplication
68
+ */
69
+ getStats() {
70
+ let duplicates = 0;
71
+ let oldest = null;
72
+ for (const event of this.seen.values()) {
73
+ if (event.count > 1) {
74
+ duplicates += event.count - 1;
75
+ }
76
+ if (!oldest || event.timestamp < oldest) {
77
+ oldest = event.timestamp;
78
+ }
79
+ }
80
+ return {
81
+ totalEvents: this.seen.size,
82
+ oldestEvent: oldest,
83
+ duplicates,
84
+ };
85
+ }
86
+ /**
87
+ * Cleanup expired events
88
+ */
89
+ cleanup() {
90
+ const now = Date.now();
91
+ let removed = 0;
92
+ for (const [eventId, event] of this.seen.entries()) {
93
+ if (now - event.timestamp > this.TTL_MS) {
94
+ this.seen.delete(eventId);
95
+ removed++;
96
+ }
97
+ }
98
+ return removed;
99
+ }
100
+ /**
101
+ * Get count of active events being tracked
102
+ */
103
+ get activeEventCount() {
104
+ return this.seen.size;
105
+ }
106
+ /**
107
+ * Destroy deduplicator and stop cleanup interval
108
+ */
109
+ destroy() {
110
+ if (this.cleanupInterval) {
111
+ clearInterval(this.cleanupInterval);
112
+ this.cleanupInterval = null;
113
+ }
114
+ this.clearAll();
115
+ }
116
+ startCleanupInterval() {
117
+ // Cleanup a cada 1/4 do TTL
118
+ const cleanupIntervalMs = Math.max(1000, this.TTL_MS / 4);
119
+ this.cleanupInterval = setInterval(() => {
120
+ this.cleanup();
121
+ }, cleanupIntervalMs);
122
+ // Unref para não impedir o processo de sair
123
+ this.cleanupInterval.unref();
124
+ }
125
+ evictOldest() {
126
+ let oldestId = null;
127
+ let oldestTime = Infinity;
128
+ for (const [eventId, event] of this.seen.entries()) {
129
+ if (event.timestamp < oldestTime) {
130
+ oldestTime = event.timestamp;
131
+ oldestId = eventId;
132
+ }
133
+ }
134
+ if (oldestId) {
135
+ this.seen.delete(oldestId);
136
+ }
137
+ }
138
+ }
139
+ /**
140
+ * Generate unique event ID for deduplication
141
+ */
142
+ export function generateEventId(params) {
143
+ const { type, sessionId, runId, payload } = params;
144
+ const parts = [type];
145
+ if (sessionId)
146
+ parts.push(sessionId);
147
+ if (runId)
148
+ parts.push(runId);
149
+ // Include payload hash if available
150
+ if (payload) {
151
+ const hash = quickHash(JSON.stringify(payload));
152
+ parts.push(hash.toString());
153
+ }
154
+ return parts.join(":");
155
+ }
156
+ /**
157
+ * Quick hash function for strings (not cryptographically secure)
158
+ */
159
+ function quickHash(str) {
160
+ let hash = 0;
161
+ for (let i = 0; i < str.length; i++) {
162
+ const char = str.charCodeAt(i);
163
+ hash = (hash << 5) - hash + char;
164
+ hash = hash & hash; // Convert to 32bit integer
165
+ }
166
+ return Math.abs(hash);
167
+ }
@@ -1,6 +1,7 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
- import { pathToFileURL } from "node:url";
3
3
  import { CONFIG_PATH } from "../config/config.js";
4
+ import { importFileModule, resolveFunctionModuleExport } from "../hooks/module-loader.js";
4
5
  const hookPresetMappings = {
5
6
  gmail: [
6
7
  {
@@ -204,15 +205,18 @@ async function loadTransform(transform) {
204
205
  if (cached) {
205
206
  return cached;
206
207
  }
207
- const url = pathToFileURL(transform.modulePath).href;
208
- const mod = (await import(url));
208
+ const mod = await importFileModule({ modulePath: transform.modulePath });
209
209
  const fn = resolveTransformFn(mod, transform.exportName);
210
210
  transformCache.set(cacheKey, fn);
211
211
  return fn;
212
212
  }
213
213
  function resolveTransformFn(mod, exportName) {
214
- const candidate = exportName ? mod[exportName] : (mod.default ?? mod.transform);
215
- if (typeof candidate !== "function") {
214
+ const candidate = resolveFunctionModuleExport({
215
+ mod,
216
+ exportName,
217
+ fallbackExportNames: ["default", "transform"],
218
+ });
219
+ if (!candidate) {
216
220
  throw new Error("hook transform module must export a function");
217
221
  }
218
222
  return candidate;
@@ -223,6 +227,32 @@ function resolvePath(baseDir, target) {
223
227
  }
224
228
  return path.isAbsolute(target) ? path.resolve(target) : path.resolve(baseDir, target);
225
229
  }
230
+ function escapesBase(baseDir, candidate) {
231
+ const relative = path.relative(baseDir, candidate);
232
+ return relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
233
+ }
234
+ function safeRealpathSync(candidate) {
235
+ try {
236
+ const nativeRealpath = fs.realpathSync.native;
237
+ return nativeRealpath ? nativeRealpath(candidate) : fs.realpathSync(candidate);
238
+ }
239
+ catch {
240
+ return null;
241
+ }
242
+ }
243
+ function resolveExistingAncestor(candidate) {
244
+ let current = path.resolve(candidate);
245
+ while (true) {
246
+ if (fs.existsSync(current)) {
247
+ return current;
248
+ }
249
+ const parent = path.dirname(current);
250
+ if (parent === current) {
251
+ return null;
252
+ }
253
+ current = parent;
254
+ }
255
+ }
226
256
  function resolveContainedPath(baseDir, target, label) {
227
257
  const base = path.resolve(baseDir);
228
258
  const trimmed = target?.trim();
@@ -230,8 +260,17 @@ function resolveContainedPath(baseDir, target, label) {
230
260
  throw new Error(`${label} module path is required`);
231
261
  }
232
262
  const resolved = resolvePath(base, trimmed);
233
- const relative = path.relative(base, resolved);
234
- if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
263
+ if (escapesBase(base, resolved)) {
264
+ throw new Error(`${label} module path must be within ${base}: ${target}`);
265
+ }
266
+ // Block symlink escapes for existing path segments while preserving current
267
+ // behavior for not-yet-created files.
268
+ const baseRealpath = safeRealpathSync(base);
269
+ const existingAncestor = resolveExistingAncestor(resolved);
270
+ const existingAncestorRealpath = existingAncestor ? safeRealpathSync(existingAncestor) : null;
271
+ if (baseRealpath &&
272
+ existingAncestorRealpath &&
273
+ escapesBase(baseRealpath, existingAncestorRealpath)) {
235
274
  throw new Error(`${label} module path must be within ${base}: ${target}`);
236
275
  }
237
276
  return resolved;