@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
@@ -7,6 +7,11 @@ import { randomBytes } from "node:crypto";
7
7
  *
8
8
  * SECURITY: External content should NEVER be directly interpolated into
9
9
  * system prompts or treated as trusted instructions.
10
+ *
11
+ * SECURITY HARDENING (GHSA-external-content-markers):
12
+ * - Zero-width character stripping from boundary markers
13
+ * - Soft-hyphen character removal
14
+ * - Unicode format character escaping
10
15
  */
11
16
  /**
12
17
  * Patterns that may indicate prompt injection attempts.
@@ -110,8 +115,53 @@ function foldMarkerChar(char) {
110
115
  }
111
116
  return char;
112
117
  }
118
+ /**
119
+ * Unicode invisible/format characters that can be used for spoofing boundary markers.
120
+ * GHSA-external-content-markers: Strip these to prevent marker bypass attacks.
121
+ */
122
+ const INVISIBLE_UNICODE_RANGES = [
123
+ [0x0000, 0x001f], // C0 control characters
124
+ [0x007f, 0x009f], // Delete and C1 control
125
+ [0x00ad, 0x00ad], // Soft hyphen
126
+ [0x0600, 0x0605], // Arabic number signs
127
+ [0x061c, 0x061c], // Arabic letter mark
128
+ [0x06dd, 0x06dd], // Ayah
129
+ [0x070f, 0x070f], // Syriac abbreviation
130
+ [0x180e, 0x180e], // Mongolian vowel separator
131
+ [0x200b, 0x200f], // Zero-width chars
132
+ [0x2028, 0x202e], // Line/paragraph separators
133
+ [0x2060, 0x206f], // Invisible operators
134
+ [0xfeff, 0xfeff], // BOM/zero-width no-break space
135
+ [0xfff0, 0xfff8], // Specials
136
+ [0xd800, 0xdfff], // Surrogate pairs
137
+ ];
138
+ /**
139
+ * Strip invisible Unicode characters from content.
140
+ *
141
+ * SECURITY: Prevents spoofing of boundary markers via zero-width characters.
142
+ * GHSA-external-content-markers
143
+ */
144
+ function stripInvisibleUnicode(text) {
145
+ let result = "";
146
+ for (const char of text) {
147
+ const code = char.codePointAt(0) ?? 0;
148
+ let isInvisible = false;
149
+ for (const [start, end] of INVISIBLE_UNICODE_RANGES) {
150
+ if (code >= start && code <= end) {
151
+ isInvisible = true;
152
+ break;
153
+ }
154
+ }
155
+ if (!isInvisible) {
156
+ result += char;
157
+ }
158
+ }
159
+ return result;
160
+ }
113
161
  function foldMarkerText(input) {
114
- return input.replace(/[\uFF21-\uFF3A\uFF41-\uFF5A\uFF1C\uFF1E\u2329\u232A\u3008\u3009\u2039\u203A\u27E8\u27E9\uFE64\uFE65]/g, (char) => foldMarkerChar(char));
162
+ // SECURITY HARDENING: Strip invisible Unicode before folding
163
+ const stripped = stripInvisibleUnicode(input);
164
+ return stripped.replace(/[\uFF21-\uFF3A\uFF41-\uFF5A\uFF1C\uFF1E\u2329\u232A\u3008\u3009\u2039\u203A\u27E8\u27E9\uFE64\uFE65]/g, (char) => foldMarkerChar(char));
115
165
  }
116
166
  function replaceMarkers(content) {
117
167
  const folded = foldMarkerText(content);
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Session Cost Tracking
3
+ * Track token costs, time costs, and generate cost reports
4
+ */
5
+ /**
6
+ * Calculate total cost from cost units
7
+ */
8
+ export function calculateTotalCost(costs, rates) {
9
+ const defaultRates = {
10
+ tokens: 0.00002, // $0.00002 per token
11
+ time: 0.001, // $0.001 per second
12
+ api_calls: 0.001, // $0.001 per API call
13
+ custom: 0,
14
+ };
15
+ const effectiveRates = { ...defaultRates, ...rates };
16
+ return costs.reduce((total, cost) => {
17
+ const rate = effectiveRates[cost.type] || 0;
18
+ return total + cost.amount * rate;
19
+ }, 0);
20
+ }
21
+ /**
22
+ * Track session costs
23
+ */
24
+ export class SessionCostTracker {
25
+ sessions = new Map();
26
+ rates = {};
27
+ constructor(rates) {
28
+ if (rates) {
29
+ this.rates = rates;
30
+ }
31
+ }
32
+ /**
33
+ * Start tracking a session
34
+ */
35
+ startSession(sessionId, agentId, channelId) {
36
+ this.sessions.set(sessionId, {
37
+ sessionId,
38
+ agentId,
39
+ channelId,
40
+ costs: [],
41
+ startTime: Date.now(),
42
+ metadata: {},
43
+ });
44
+ }
45
+ /**
46
+ * Add cost to a session
47
+ */
48
+ addCost(sessionId, cost) {
49
+ const session = this.sessions.get(sessionId);
50
+ if (!session) {
51
+ throw new Error(`Session ${sessionId} not found`);
52
+ }
53
+ session.costs.push(cost);
54
+ session.totalCost = calculateTotalCost(session.costs, this.rates);
55
+ }
56
+ /**
57
+ * Add token cost
58
+ */
59
+ addTokenCost(sessionId, tokens, metadata) {
60
+ this.addCost(sessionId, {
61
+ type: "tokens",
62
+ amount: tokens,
63
+ metadata,
64
+ });
65
+ }
66
+ /**
67
+ * Add time cost
68
+ */
69
+ addTimeCost(sessionId, seconds, metadata) {
70
+ this.addCost(sessionId, {
71
+ type: "time",
72
+ amount: seconds,
73
+ metadata,
74
+ });
75
+ }
76
+ /**
77
+ * Add API call cost
78
+ */
79
+ addApiCallCost(sessionId, count = 1, metadata) {
80
+ this.addCost(sessionId, {
81
+ type: "api_calls",
82
+ amount: count,
83
+ metadata,
84
+ });
85
+ }
86
+ /**
87
+ * End session tracking
88
+ */
89
+ endSession(sessionId) {
90
+ const session = this.sessions.get(sessionId);
91
+ if (!session) {
92
+ return undefined;
93
+ }
94
+ session.endTime = Date.now();
95
+ // Add final time cost
96
+ const durationSeconds = (session.endTime - session.startTime) / 1000;
97
+ this.addTimeCost(sessionId, durationSeconds, { type: "session_duration" });
98
+ return session;
99
+ }
100
+ /**
101
+ * Get session cost
102
+ */
103
+ getSessionCost(sessionId) {
104
+ return this.sessions.get(sessionId);
105
+ }
106
+ /**
107
+ * Get cost breakdown
108
+ */
109
+ getCostBreakdown(sessionIds) {
110
+ const sessions = sessionIds
111
+ ? sessionIds.map((id) => this.sessions.get(id)).filter((s) => !!s)
112
+ : Array.from(this.sessions.values());
113
+ const breakdown = {
114
+ byType: {},
115
+ byAgent: {},
116
+ byChannel: {},
117
+ bySession: {},
118
+ total: 0,
119
+ };
120
+ for (const session of sessions) {
121
+ const sessionTotal = calculateTotalCost(session.costs, this.rates);
122
+ // By type
123
+ for (const cost of session.costs) {
124
+ breakdown.byType[cost.type] =
125
+ (breakdown.byType[cost.type] || 0) + cost.amount * (this.rates[cost.type] || 0);
126
+ }
127
+ // By agent
128
+ if (session.agentId) {
129
+ breakdown.byAgent[session.agentId] =
130
+ (breakdown.byAgent[session.agentId] || 0) + sessionTotal;
131
+ }
132
+ // By channel
133
+ if (session.channelId) {
134
+ breakdown.byChannel[session.channelId] =
135
+ (breakdown.byChannel[session.channelId] || 0) + sessionTotal;
136
+ }
137
+ // By session
138
+ breakdown.bySession[session.sessionId] = sessionTotal;
139
+ // Total
140
+ breakdown.total += sessionTotal;
141
+ }
142
+ return breakdown;
143
+ }
144
+ /**
145
+ * Export costs to different formats
146
+ */
147
+ exportCosts(format, sessionIds) {
148
+ const sessions = sessionIds
149
+ ? sessionIds.map((id) => this.getSessionCost(id)).filter((s) => !!s)
150
+ : Array.from(this.sessions.values());
151
+ switch (format) {
152
+ case "csv":
153
+ return this.exportToCSV(sessions);
154
+ case "json":
155
+ return JSON.stringify(sessions, null, 2);
156
+ case "markdown":
157
+ return this.exportToMarkdown(sessions);
158
+ default:
159
+ throw new Error(`Unknown format: ${format}`);
160
+ }
161
+ }
162
+ exportToCSV(sessions) {
163
+ const lines = [
164
+ "Session ID,Agent ID,Channel ID,Start Time,End Time,Total Cost,Token Cost,Time Cost,API Calls",
165
+ ];
166
+ for (const session of sessions) {
167
+ const tokenCost = session.costs
168
+ .filter((c) => c.type === "tokens")
169
+ .reduce((sum, c) => sum + c.amount, 0);
170
+ const timeCost = session.costs
171
+ .filter((c) => c.type === "time")
172
+ .reduce((sum, c) => sum + c.amount, 0);
173
+ const apiCalls = session.costs
174
+ .filter((c) => c.type === "api_calls")
175
+ .reduce((sum, c) => sum + c.amount, 0);
176
+ lines.push(`${session.sessionId},${session.agentId || ""},${session.channelId || ""},${session.startTime},${session.endTime || ""},${session.totalCost || 0},${tokenCost},${timeCost},${apiCalls}`);
177
+ }
178
+ return lines.join("\n");
179
+ }
180
+ exportToMarkdown(sessions) {
181
+ const lines = ["# Session Cost Report", ""];
182
+ const breakdown = this.getCostBreakdown(sessions.map((s) => s.sessionId));
183
+ lines.push("## Summary", "");
184
+ lines.push(`- **Total Cost**: $${breakdown.total.toFixed(4)}`);
185
+ lines.push(`- **Sessions**: ${sessions.length}`);
186
+ lines.push("");
187
+ lines.push("## By Type", "");
188
+ lines.push("| Type | Cost |");
189
+ lines.push("|------|------|");
190
+ for (const [type, cost] of Object.entries(breakdown.byType)) {
191
+ lines.push(`| ${type} | $${cost.toFixed(4)} |`);
192
+ }
193
+ lines.push("");
194
+ lines.push("## Sessions", "");
195
+ lines.push("| Session | Agent | Channel | Total Cost |");
196
+ lines.push("|---------|-------|---------|------------|");
197
+ for (const session of sessions) {
198
+ lines.push(`| ${session.sessionId} | ${session.agentId || "N/A"} | ${session.channelId || "N/A"} | $${(session.totalCost || 0).toFixed(4)} |`);
199
+ }
200
+ return lines.join("\n");
201
+ }
202
+ /**
203
+ * Clear all sessions
204
+ */
205
+ clear() {
206
+ this.sessions.clear();
207
+ }
208
+ /**
209
+ * Get statistics
210
+ */
211
+ getStats() {
212
+ const sessions = Array.from(this.sessions.values());
213
+ const _activeSessions = sessions.filter((s) => !s.endTime).length;
214
+ const _totalCost = sessions.reduce((sum, s) => sum + (s.totalCost || 0), 0);
215
+ return {
216
+ totalSessions: sessions.length,
217
+ activeSessions: _activeSessions,
218
+ totalCost: _totalCost,
219
+ avgCostPerSession: sessions.length > 0 ? _totalCost / sessions.length : 0,
220
+ };
221
+ }
222
+ }
223
+ /**
224
+ * Create default cost tracker
225
+ */
226
+ export function createSessionCostTracker(rates) {
227
+ return new SessionCostTracker(rates);
228
+ }
@@ -0,0 +1,16 @@
1
+ export function toSnakeCaseKey(key) {
2
+ return key
3
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
4
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
5
+ .toLowerCase();
6
+ }
7
+ export function readSnakeCaseParamRaw(params, key) {
8
+ if (Object.hasOwn(params, key)) {
9
+ return params[key];
10
+ }
11
+ const snakeKey = toSnakeCaseKey(key);
12
+ if (snakeKey !== key && Object.hasOwn(params, snakeKey)) {
13
+ return params[snakeKey];
14
+ }
15
+ return undefined;
16
+ }
@@ -0,0 +1,58 @@
1
+ import { readSnakeCaseParamRaw } from "./param-key.js";
2
+ export const POLL_CREATION_PARAM_DEFS = {
3
+ pollQuestion: { kind: "string" },
4
+ pollOption: { kind: "stringArray" },
5
+ pollDurationHours: { kind: "number" },
6
+ pollMulti: { kind: "boolean" },
7
+ pollDurationSeconds: { kind: "number", telegramOnly: true },
8
+ pollAnonymous: { kind: "boolean", telegramOnly: true },
9
+ pollPublic: { kind: "boolean", telegramOnly: true },
10
+ };
11
+ export const POLL_CREATION_PARAM_NAMES = Object.keys(POLL_CREATION_PARAM_DEFS);
12
+ function readPollParamRaw(params, key) {
13
+ return readSnakeCaseParamRaw(params, key);
14
+ }
15
+ export function resolveTelegramPollVisibility(params) {
16
+ if (params.pollAnonymous && params.pollPublic) {
17
+ throw new Error("pollAnonymous and pollPublic are mutually exclusive");
18
+ }
19
+ return params.pollAnonymous ? true : params.pollPublic ? false : undefined;
20
+ }
21
+ export function hasPollCreationParams(params) {
22
+ for (const key of POLL_CREATION_PARAM_NAMES) {
23
+ const def = POLL_CREATION_PARAM_DEFS[key];
24
+ const value = readPollParamRaw(params, key);
25
+ if (def.kind === "string" && typeof value === "string" && value.trim().length > 0) {
26
+ return true;
27
+ }
28
+ if (def.kind === "stringArray") {
29
+ if (Array.isArray(value) &&
30
+ value.some((entry) => typeof entry === "string" && entry.trim())) {
31
+ return true;
32
+ }
33
+ if (typeof value === "string" && value.trim().length > 0) {
34
+ return true;
35
+ }
36
+ }
37
+ if (def.kind === "number") {
38
+ if (typeof value === "number" && Number.isFinite(value)) {
39
+ return true;
40
+ }
41
+ if (typeof value === "string") {
42
+ const trimmed = value.trim();
43
+ if (trimmed.length > 0 && Number.isFinite(Number(trimmed))) {
44
+ return true;
45
+ }
46
+ }
47
+ }
48
+ if (def.kind === "boolean") {
49
+ if (value === true) {
50
+ return true;
51
+ }
52
+ if (typeof value === "string" && value.trim().toLowerCase() === "true") {
53
+ return true;
54
+ }
55
+ }
56
+ }
57
+ return false;
58
+ }
@@ -0,0 +1,55 @@
1
+ export function resolvePollMaxSelections(optionCount, allowMultiselect) {
2
+ return allowMultiselect ? Math.max(2, optionCount) : 1;
3
+ }
4
+ export function normalizePollInput(input, options = {}) {
5
+ const question = input.question.trim();
6
+ if (!question) {
7
+ throw new Error("Poll question is required");
8
+ }
9
+ const pollOptions = (input.options ?? []).map((option) => option.trim());
10
+ const cleaned = pollOptions.filter(Boolean);
11
+ if (cleaned.length < 2) {
12
+ throw new Error("Poll requires at least 2 options");
13
+ }
14
+ if (options.maxOptions !== undefined && cleaned.length > options.maxOptions) {
15
+ throw new Error(`Poll supports at most ${options.maxOptions} options`);
16
+ }
17
+ const maxSelectionsRaw = input.maxSelections;
18
+ const maxSelections = typeof maxSelectionsRaw === "number" && Number.isFinite(maxSelectionsRaw)
19
+ ? Math.floor(maxSelectionsRaw)
20
+ : 1;
21
+ if (maxSelections < 1) {
22
+ throw new Error("maxSelections must be at least 1");
23
+ }
24
+ if (maxSelections > cleaned.length) {
25
+ throw new Error("maxSelections cannot exceed option count");
26
+ }
27
+ const durationSecondsRaw = input.durationSeconds;
28
+ const durationSeconds = typeof durationSecondsRaw === "number" && Number.isFinite(durationSecondsRaw)
29
+ ? Math.floor(durationSecondsRaw)
30
+ : undefined;
31
+ if (durationSeconds !== undefined && durationSeconds < 1) {
32
+ throw new Error("durationSeconds must be at least 1");
33
+ }
34
+ const durationRaw = input.durationHours;
35
+ const durationHours = typeof durationRaw === "number" && Number.isFinite(durationRaw)
36
+ ? Math.floor(durationRaw)
37
+ : undefined;
38
+ if (durationHours !== undefined && durationHours < 1) {
39
+ throw new Error("durationHours must be at least 1");
40
+ }
41
+ if (durationSeconds !== undefined && durationHours !== undefined) {
42
+ throw new Error("durationSeconds and durationHours are mutually exclusive");
43
+ }
44
+ return {
45
+ question,
46
+ options: cleaned,
47
+ maxSelections,
48
+ durationSeconds,
49
+ durationHours,
50
+ };
51
+ }
52
+ export function normalizePollDurationHours(value, options) {
53
+ const base = typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : options.defaultHours;
54
+ return Math.min(Math.max(base, 1), options.maxHours);
55
+ }