@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
|
@@ -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-
|
|
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,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
|
+
}
|