@poolzin/pool-bot 2026.3.16 → 2026.3.18

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 (88) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/agents/tools/web-fetch.js +1 -1
  3. package/dist/build-info.json +3 -3
  4. package/dist/commands/skills-openclaw.command.js +123 -0
  5. package/dist/config/paths.js +7 -0
  6. package/dist/infra/net/fetch-guard.js +191 -146
  7. package/dist/media/fetch.js +83 -112
  8. package/dist/media/inbound-path-policy.js +90 -97
  9. package/dist/media/read-response-with-limit.js +49 -26
  10. package/dist/media-understanding/attachments.js +1 -1
  11. package/dist/plugin-sdk/audio.js +7 -0
  12. package/dist/plugin-sdk/bluebubbles.js +7 -0
  13. package/dist/plugin-sdk/browser.js +7 -0
  14. package/dist/plugin-sdk/canvas.js +7 -0
  15. package/dist/plugin-sdk/cron.js +7 -0
  16. package/dist/plugin-sdk/discord-actions.js +6 -0
  17. package/dist/plugin-sdk/discord.js +7 -0
  18. package/dist/plugin-sdk/image.js +7 -0
  19. package/dist/plugin-sdk/imessage.js +6 -0
  20. package/dist/plugin-sdk/keyed-async-queue.js +35 -0
  21. package/dist/plugin-sdk/media.js +8 -0
  22. package/dist/plugin-sdk/memory.js +7 -0
  23. package/dist/plugin-sdk/pdf.js +7 -0
  24. package/dist/plugin-sdk/sessions.js +7 -0
  25. package/dist/plugin-sdk/signal.js +6 -0
  26. package/dist/plugin-sdk/slack-actions.js +7 -0
  27. package/dist/plugin-sdk/slack.js +7 -0
  28. package/dist/plugin-sdk/telegram-actions.js +6 -0
  29. package/dist/plugin-sdk/telegram.js +6 -0
  30. package/dist/plugin-sdk/test-utils.js +110 -0
  31. package/dist/plugin-sdk/tts.js +7 -0
  32. package/dist/plugin-sdk/whatsapp.js +6 -0
  33. package/dist/providers/github-copilot-auth.js +53 -76
  34. package/dist/providers/github-copilot-models.js +63 -35
  35. package/dist/providers/github-copilot-token.js +46 -89
  36. package/dist/security/audit-findings.js +165 -0
  37. package/dist/security/audit.js +141 -572
  38. package/dist/skills/openclaw-skill-loader.js +191 -0
  39. package/dist/slack/monitor/media.js +2 -1
  40. package/docs/branding-evaluation-2026-03-12.md +285 -0
  41. package/docs/improvements/OPENCLAW-IMPLEMENTATION.md +45 -0
  42. package/docs/skills/openclaw-integration.md +295 -0
  43. package/docs/testing/TEST-PLAN-2026-03-13.md +338 -0
  44. package/docs/version-2026.3.16-evaluation.md +190 -0
  45. package/extensions/acpx/package.json +19 -0
  46. package/extensions/acpx/poolbot.plugin.json +9 -0
  47. package/extensions/acpx/src/index.ts +34 -0
  48. package/extensions/bluebubbles/src/runtime.ts +1 -0
  49. package/extensions/diffs/package.json +15 -0
  50. package/extensions/diffs/poolbot.plugin.json +10 -0
  51. package/extensions/diffs/src/index.ts +106 -0
  52. package/extensions/discord/src/runtime.ts +1 -0
  53. package/extensions/feishu/src/runtime.ts +1 -0
  54. package/extensions/github-copilot/package.json +28 -0
  55. package/extensions/github-copilot/poolbot.plugin.json +29 -0
  56. package/extensions/github-copilot/src/index.ts +126 -0
  57. package/extensions/github-copilot/tsconfig.json +10 -0
  58. package/extensions/googlechat/src/runtime.ts +1 -0
  59. package/extensions/imessage/src/runtime.ts +1 -0
  60. package/extensions/irc/src/runtime.ts +1 -0
  61. package/extensions/line/src/runtime.ts +1 -0
  62. package/extensions/matrix/src/runtime.ts +1 -0
  63. package/extensions/mattermost/src/mattermost/monitor-helpers.ts +10 -1
  64. package/extensions/mattermost/src/runtime.ts +6 -3
  65. package/extensions/msteams/src/runtime.ts +1 -0
  66. package/extensions/nextcloud-talk/src/runtime.ts +1 -0
  67. package/extensions/nostr/src/runtime.ts +5 -2
  68. package/extensions/ollama/package.json +20 -0
  69. package/extensions/ollama/poolbot.plugin.json +14 -0
  70. package/extensions/ollama/src/index.ts +95 -0
  71. package/extensions/sglang/package.json +18 -0
  72. package/extensions/sglang/poolbot.plugin.json +13 -0
  73. package/extensions/sglang/src/index.ts +62 -0
  74. package/extensions/signal/src/runtime.ts +1 -0
  75. package/extensions/slack/src/runtime.ts +1 -0
  76. package/extensions/telegram/src/runtime.ts +1 -0
  77. package/extensions/test-utils/package.json +17 -0
  78. package/extensions/test-utils/poolbot.plugin.json +16 -0
  79. package/extensions/test-utils/src/index.ts +220 -0
  80. package/extensions/tlon/src/runtime.ts +1 -0
  81. package/extensions/twitch/src/runtime.ts +1 -0
  82. package/extensions/vllm/package.json +19 -0
  83. package/extensions/vllm/poolbot.plugin.json +13 -0
  84. package/extensions/vllm/src/index.ts +90 -0
  85. package/extensions/whatsapp/src/runtime.ts +1 -0
  86. package/extensions/zalo/src/runtime.ts +1 -0
  87. package/extensions/zalouser/src/runtime.ts +1 -0
  88. package/package.json +77 -3
@@ -1,21 +1,26 @@
1
+ /**
2
+ * Secure Media Fetch with SSRF Protection
3
+ */
1
4
  import path from "node:path";
2
- import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
5
+ import { fetchWithGuard } from "../infra/net/fetch-guard.js";
3
6
  import { detectMime, extensionForMime } from "./mime.js";
7
+ import { readResponseWithLimit } from "./read-response-with-limit.js";
4
8
  export class MediaFetchError extends Error {
5
9
  code;
6
- constructor(code, message) {
10
+ httpStatus;
11
+ constructor(code, message, httpStatus) {
7
12
  super(message);
8
13
  this.code = code;
9
14
  this.name = "MediaFetchError";
15
+ this.httpStatus = httpStatus;
10
16
  }
11
17
  }
12
18
  function stripQuotes(value) {
13
19
  return value.replace(/^["']|["']$/g, "");
14
20
  }
15
21
  function parseContentDispositionFileName(header) {
16
- if (!header) {
22
+ if (!header)
17
23
  return undefined;
18
- }
19
24
  const starMatch = /filename\*\s*=\s*([^;]+)/i.exec(header);
20
25
  if (starMatch?.[1]) {
21
26
  const cleaned = stripQuotes(starMatch[1].trim());
@@ -36,139 +41,105 @@ function parseContentDispositionFileName(header) {
36
41
  async function readErrorBodySnippet(res, maxChars = 200) {
37
42
  try {
38
43
  const text = await res.text();
39
- if (!text) {
44
+ if (!text)
40
45
  return undefined;
41
- }
42
46
  const collapsed = text.replace(/\s+/g, " ").trim();
43
- if (!collapsed) {
47
+ if (!collapsed)
44
48
  return undefined;
45
- }
46
- if (collapsed.length <= maxChars) {
47
- return collapsed;
48
- }
49
- return `${collapsed.slice(0, maxChars)}…`;
49
+ return collapsed.length <= maxChars ? collapsed : `${collapsed.slice(0, maxChars)}…`;
50
50
  }
51
51
  catch {
52
52
  return undefined;
53
53
  }
54
54
  }
55
55
  export async function fetchRemoteMedia(options) {
56
- const { url, fetchImpl, filePathHint, maxBytes, maxRedirects, ssrfPolicy, lookupFn } = options;
57
- let res;
58
- let finalUrl = url;
59
- let release = null;
56
+ const { url, maxBytes = 100 * 1024 * 1024, readIdleTimeoutMs = 30000, maxRedirects = 5, timeoutMs = 60000, allowPrivateIPs = false, filePathHint, fetchImpl: _fetchImpl, ssrfPolicy, } = options;
57
+ let parsedUrl;
60
58
  try {
61
- const result = await fetchWithSsrFGuard({
62
- url,
63
- fetchImpl,
64
- maxRedirects,
65
- policy: ssrfPolicy,
66
- lookupFn,
67
- });
68
- res = result.response;
69
- finalUrl = result.finalUrl;
70
- release = result.release;
59
+ parsedUrl = new URL(url);
71
60
  }
72
- catch (err) {
73
- throw new MediaFetchError("fetch_failed", `Failed to fetch media from ${url}: ${String(err)}`);
61
+ catch (error) {
62
+ throw new MediaFetchError("fetch_failed", `Invalid URL: ${error instanceof Error ? error.message : String(error)}`);
74
63
  }
64
+ let response;
75
65
  try {
76
- if (!res.ok) {
77
- const statusText = res.statusText ? ` ${res.statusText}` : "";
78
- const redirected = finalUrl !== url ? ` (redirected to ${finalUrl})` : "";
79
- let detail = `HTTP ${res.status}${statusText}`;
80
- if (!res.body) {
81
- detail = `HTTP ${res.status}${statusText}; empty response body`;
82
- }
83
- else {
84
- const snippet = await readErrorBodySnippet(res);
85
- if (snippet) {
86
- detail += `; body: ${snippet}`;
87
- }
66
+ const guardOptions = {
67
+ allowPrivateIPs,
68
+ maxRedirects,
69
+ timeoutMs,
70
+ ...ssrfPolicy,
71
+ };
72
+ response = await fetchWithGuard(url, undefined, guardOptions);
73
+ }
74
+ catch (error) {
75
+ if (error instanceof Error && error.name.includes("FetchGuard")) {
76
+ const guardError = error;
77
+ if (guardError.code === "ssrf_blocked") {
78
+ throw new MediaFetchError("ssrf_blocked", "SSRF protection blocked this request");
88
79
  }
89
- throw new MediaFetchError("http_error", `Failed to fetch media from ${url}${redirected}: ${detail}`);
90
- }
91
- const contentLength = res.headers.get("content-length");
92
- if (maxBytes && contentLength) {
93
- const length = Number(contentLength);
94
- if (Number.isFinite(length) && length > maxBytes) {
95
- throw new MediaFetchError("max_bytes", `Failed to fetch media from ${url}: content length ${length} exceeds maxBytes ${maxBytes}`);
80
+ if (guardError.code === "timeout") {
81
+ throw new MediaFetchError("timeout", "Request timeout");
96
82
  }
97
83
  }
98
- const buffer = maxBytes
99
- ? await readResponseWithLimit(res, maxBytes)
100
- : Buffer.from(await res.arrayBuffer());
101
- let fileNameFromUrl;
102
- try {
103
- const parsed = new URL(finalUrl);
104
- const base = path.basename(parsed.pathname);
105
- fileNameFromUrl = base || undefined;
106
- }
107
- catch {
108
- // ignore parse errors; leave undefined
109
- }
110
- const headerFileName = parseContentDispositionFileName(res.headers.get("content-disposition"));
111
- let fileName = headerFileName || fileNameFromUrl || (filePathHint ? path.basename(filePathHint) : undefined);
112
- const filePathForMime = headerFileName && path.extname(headerFileName) ? headerFileName : (filePathHint ?? finalUrl);
113
- const contentType = await detectMime({
114
- buffer,
115
- headerMime: res.headers.get("content-type"),
116
- filePath: filePathForMime,
117
- });
118
- if (fileName && !path.extname(fileName) && contentType) {
119
- const ext = extensionForMime(contentType);
120
- if (ext) {
121
- fileName = `${fileName}${ext}`;
122
- }
123
- }
124
- return {
125
- buffer,
126
- contentType: contentType ?? undefined,
127
- fileName,
128
- };
84
+ throw new MediaFetchError("fetch_failed", error instanceof Error ? error.message : String(error));
129
85
  }
130
- finally {
131
- if (release) {
132
- await release();
133
- }
86
+ if (!response.ok) {
87
+ const errorBody = await readErrorBodySnippet(response);
88
+ throw new MediaFetchError("http_error", `HTTP ${response.status}${errorBody ? `: ${errorBody}` : ""}`, response.status);
134
89
  }
135
- }
136
- async function readResponseWithLimit(res, maxBytes) {
137
- const body = res.body;
138
- if (!body || typeof body.getReader !== "function") {
139
- const fallback = Buffer.from(await res.arrayBuffer());
140
- if (fallback.length > maxBytes) {
141
- throw new MediaFetchError("max_bytes", `Failed to fetch media from ${res.url || "response"}: payload exceeds maxBytes ${maxBytes}`);
90
+ const contentType = response.headers.get("content-type") || undefined;
91
+ const contentDisposition = response.headers.get("content-disposition");
92
+ let fileName = parseContentDispositionFileName(contentDisposition);
93
+ if (!fileName && filePathHint) {
94
+ fileName = path.basename(filePathHint);
95
+ }
96
+ if (!fileName) {
97
+ const urlPath = parsedUrl.pathname;
98
+ if (urlPath && urlPath !== "/") {
99
+ fileName = path.basename(urlPath);
142
100
  }
143
- return fallback;
144
101
  }
145
- const reader = body.getReader();
146
- const chunks = [];
147
- let total = 0;
102
+ let buffer;
148
103
  try {
149
- while (true) {
150
- const { done, value } = await reader.read();
151
- if (done) {
152
- break;
104
+ buffer = await readResponseWithLimit(response, {
105
+ maxBytes,
106
+ readIdleTimeoutMs,
107
+ });
108
+ }
109
+ catch (error) {
110
+ if (error instanceof Error && error.name === "ReadResponseError") {
111
+ const readError = error;
112
+ if (readError.code === "max_bytes") {
113
+ throw new MediaFetchError("max_bytes", `Download exceeds size limit (${maxBytes} bytes)`);
153
114
  }
154
- if (value?.length) {
155
- total += value.length;
156
- if (total > maxBytes) {
157
- try {
158
- await reader.cancel();
159
- }
160
- catch { }
161
- throw new MediaFetchError("max_bytes", `Failed to fetch media from ${res.url || "response"}: payload exceeds maxBytes ${maxBytes}`);
162
- }
163
- chunks.push(value);
115
+ if (readError.code === "timeout") {
116
+ throw new MediaFetchError("timeout", "Download timeout - connection stalled");
164
117
  }
165
118
  }
119
+ throw new MediaFetchError("fetch_failed", error instanceof Error ? error.message : String(error));
166
120
  }
167
- finally {
168
- try {
169
- reader.releaseLock();
121
+ let finalContentType = contentType;
122
+ if (!finalContentType) {
123
+ finalContentType = await detectMime({ buffer });
124
+ }
125
+ if (fileName && finalContentType) {
126
+ const ext = extensionForMime(finalContentType);
127
+ if (ext && !fileName.toLowerCase().endsWith(ext.toLowerCase())) {
128
+ fileName = fileName + ext;
170
129
  }
171
- catch { }
172
130
  }
173
- return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), total);
131
+ return {
132
+ buffer,
133
+ contentType: finalContentType,
134
+ fileName,
135
+ size: buffer.length,
136
+ };
137
+ }
138
+ export async function quickFetchMedia(url) {
139
+ return fetchRemoteMedia({
140
+ url,
141
+ maxBytes: 50 * 1024 * 1024,
142
+ readIdleTimeoutMs: 15000,
143
+ timeoutMs: 30000,
144
+ });
174
145
  }
@@ -1,114 +1,107 @@
1
+ /**
2
+ * Inbound Path Policy
3
+ *
4
+ * Validates and sanitizes file paths to prevent path traversal attacks
5
+ */
1
6
  import path from "node:path";
2
- const WILDCARD_SEGMENT = "*";
3
- const WINDOWS_DRIVE_ABS_RE = /^[A-Za-z]:\//;
4
- const WINDOWS_DRIVE_ROOT_RE = /^[A-Za-z]:$/;
5
- export const DEFAULT_IMESSAGE_ATTACHMENT_ROOTS = ["/Users/*/Library/Messages/Attachments"];
6
- function normalizePosixAbsolutePath(value) {
7
- const trimmed = value.trim();
8
- if (!trimmed || trimmed.includes("\0")) {
9
- return undefined;
7
+ export class InboundPathError extends Error {
8
+ code;
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.code = code;
12
+ this.name = "InboundPathError";
10
13
  }
11
- const normalized = path.posix.normalize(trimmed.replaceAll("\\", "/"));
12
- const isAbsolute = normalized.startsWith("/") || WINDOWS_DRIVE_ABS_RE.test(normalized);
13
- if (!isAbsolute || normalized === "/") {
14
- return undefined;
15
- }
16
- const withoutTrailingSlash = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
17
- if (WINDOWS_DRIVE_ROOT_RE.test(withoutTrailingSlash)) {
18
- return undefined;
19
- }
20
- return withoutTrailingSlash;
21
- }
22
- function splitPathSegments(value) {
23
- return value.split("/").filter(Boolean);
24
14
  }
25
- function matchesRootPattern(params) {
26
- const candidateSegments = splitPathSegments(params.candidatePath);
27
- const rootSegments = splitPathSegments(params.rootPattern);
28
- if (candidateSegments.length < rootSegments.length) {
29
- return false;
15
+ /**
16
+ * Validate and resolve an inbound file path
17
+ */
18
+ export function validateInboundPath(inputPath, options) {
19
+ const { baseDir, allowOutside = false, allowedExtensions, maxLength = 1024, } = options;
20
+ // Check path length
21
+ if (inputPath.length > maxLength) {
22
+ throw new InboundPathError("too_long", `Path exceeds maximum length (${maxLength} chars)`);
30
23
  }
31
- for (let idx = 0; idx < rootSegments.length; idx += 1) {
32
- const expected = rootSegments[idx];
33
- const actual = candidateSegments[idx];
34
- if (expected === WILDCARD_SEGMENT) {
35
- continue;
36
- }
37
- if (expected !== actual) {
38
- return false;
39
- }
40
- }
41
- return true;
42
- }
43
- export function isValidInboundPathRootPattern(value) {
44
- const normalized = normalizePosixAbsolutePath(value);
45
- if (!normalized) {
46
- return false;
47
- }
48
- const segments = splitPathSegments(normalized);
49
- if (segments.length === 0) {
50
- return false;
24
+ // Check for invalid characters
25
+ const invalidChars = /["<>:"|?*]/g;
26
+ if (invalidChars.test(inputPath)) {
27
+ throw new InboundPathError("invalid_chars", "Path contains invalid characters");
51
28
  }
52
- return segments.every((segment) => segment === WILDCARD_SEGMENT || !segment.includes("*"));
53
- }
54
- export function normalizeInboundPathRoots(roots) {
55
- const normalized = [];
56
- const seen = new Set();
57
- for (const root of roots ?? []) {
58
- if (typeof root !== "string") {
59
- continue;
60
- }
61
- if (!isValidInboundPathRootPattern(root)) {
62
- continue;
63
- }
64
- const candidate = normalizePosixAbsolutePath(root);
65
- if (!candidate || seen.has(candidate)) {
66
- continue;
29
+ // Normalize and resolve path
30
+ const normalizedPath = path.normalize(inputPath);
31
+ const resolvedPath = path.resolve(baseDir, normalizedPath);
32
+ // Check for path traversal
33
+ if (!allowOutside) {
34
+ const normalizedBaseDir = path.resolve(baseDir);
35
+ // Ensure resolved path starts with base directory
36
+ if (!resolvedPath.startsWith(normalizedBaseDir + path.sep) &&
37
+ resolvedPath !== normalizedBaseDir) {
38
+ throw new InboundPathError("path_traversal", "Path traversal detected - path must be within base directory");
67
39
  }
68
- seen.add(candidate);
69
- normalized.push(candidate);
70
40
  }
71
- return normalized;
72
- }
73
- export function mergeInboundPathRoots(...rootsLists) {
74
- const merged = [];
75
- const seen = new Set();
76
- for (const roots of rootsLists) {
77
- const normalized = normalizeInboundPathRoots(roots);
78
- for (const root of normalized) {
79
- if (seen.has(root)) {
80
- continue;
81
- }
82
- seen.add(root);
83
- merged.push(root);
41
+ // Check file extension
42
+ if (allowedExtensions && allowedExtensions.length > 0) {
43
+ const ext = path.extname(resolvedPath).toLowerCase();
44
+ if (!allowedExtensions.includes(ext)) {
45
+ throw new InboundPathError("invalid_extension", `Extension ${ext} not allowed. Allowed: ${allowedExtensions.join(", ")}`);
84
46
  }
85
47
  }
86
- return merged;
48
+ return resolvedPath;
87
49
  }
88
- export function isInboundPathAllowed(params) {
89
- const candidatePath = normalizePosixAbsolutePath(params.filePath);
90
- if (!candidatePath) {
91
- return false;
50
+ /**
51
+ * Sanitize a filename for safe storage
52
+ */
53
+ export function sanitizeFilename(filename) {
54
+ // Remove path components
55
+ let safe = path.basename(filename);
56
+ // Remove or replace unsafe characters
57
+ safe = safe.replace(/["<>:"|?*]/g, "_");
58
+ // Trim leading/trailing spaces and dots
59
+ safe = safe.replace(/^[\s.]+|[\s.]+$/g, "");
60
+ // Limit length
61
+ if (safe.length > 255) {
62
+ const ext = path.extname(safe);
63
+ const name = path.basename(safe, ext);
64
+ safe = name.slice(0, 255 - ext.length) + ext;
92
65
  }
93
- const roots = normalizeInboundPathRoots(params.roots);
94
- const effectiveRoots = roots.length > 0 ? roots : normalizeInboundPathRoots(params.fallbackRoots ?? undefined);
95
- if (effectiveRoots.length === 0) {
96
- return false;
66
+ // Ensure not empty
67
+ if (!safe) {
68
+ safe = "unnamed";
97
69
  }
98
- return effectiveRoots.some((rootPattern) => matchesRootPattern({ candidatePath, rootPattern }));
70
+ return safe;
99
71
  }
100
- function resolveIMessageAccountConfig(params) {
101
- const accountId = params.accountId?.trim();
102
- if (!accountId) {
103
- return undefined;
72
+ /**
73
+ * Check if a path is safe (within allowed directory)
74
+ */
75
+ export function isPathSafe(targetPath, baseDir) {
76
+ try {
77
+ validateInboundPath(targetPath, { baseDir });
78
+ return true;
79
+ }
80
+ catch {
81
+ return false;
104
82
  }
105
- return params.cfg.channels?.imessage?.accounts?.[accountId];
106
83
  }
107
- export function resolveIMessageAttachmentRoots(params) {
108
- const accountConfig = resolveIMessageAccountConfig(params);
109
- return mergeInboundPathRoots(accountConfig?.attachmentRoots, params.cfg.channels?.imessage?.attachmentRoots, DEFAULT_IMESSAGE_ATTACHMENT_ROOTS);
84
+ /**
85
+ * Check if a path matches allowed patterns
86
+ */
87
+ export function isValidInboundPathRootPattern(pattern) {
88
+ // Basic validation - should not contain traversal sequences
89
+ return !pattern.includes("..") && !pattern.includes("\\");
110
90
  }
111
- export function resolveIMessageRemoteAttachmentRoots(params) {
112
- const accountConfig = resolveIMessageAccountConfig(params);
113
- return mergeInboundPathRoots(accountConfig?.remoteAttachmentRoots, params.cfg.channels?.imessage?.remoteAttachmentRoots, accountConfig?.attachmentRoots, params.cfg.channels?.imessage?.attachmentRoots, DEFAULT_IMESSAGE_ATTACHMENT_ROOTS);
91
+ /**
92
+ * Check if an inbound path is allowed
93
+ * Supports both (targetPath, baseDir) and ({ filePath, roots }) signatures
94
+ */
95
+ export function isInboundPathAllowed(targetPathOrOptions, baseDir) {
96
+ // Handle object signature: { filePath, roots }
97
+ if (typeof targetPathOrOptions === "object") {
98
+ const { filePath, roots } = targetPathOrOptions;
99
+ // Check if path is within any of the allowed roots
100
+ return roots.some((root) => isPathSafe(filePath, root));
101
+ }
102
+ // Handle string signature: (targetPath, baseDir)
103
+ if (baseDir) {
104
+ return isPathSafe(targetPathOrOptions, baseDir);
105
+ }
106
+ return false;
114
107
  }
@@ -1,41 +1,64 @@
1
- export async function readResponseWithLimit(res, maxBytes, opts) {
2
- const onOverflow = opts?.onOverflow ??
3
- ((params) => new Error(`Content too large: ${params.size} bytes (limit: ${params.maxBytes} bytes)`));
4
- const body = res.body;
5
- if (!body || typeof body.getReader !== "function") {
6
- const fallback = Buffer.from(await res.arrayBuffer());
7
- if (fallback.length > maxBytes) {
8
- throw onOverflow({ size: fallback.length, maxBytes, res });
9
- }
10
- return fallback;
1
+ /**
2
+ * Read HTTP response body with limits and idle timeout
3
+ *
4
+ * Prevents hanging on slow/stalled downloads
5
+ */
6
+ export class ReadResponseError extends Error {
7
+ code;
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.code = code;
11
+ this.name = "ReadResponseError";
12
+ }
13
+ }
14
+ export async function readResponseWithLimit(response, options = {}) {
15
+ const { maxBytes = 100 * 1024 * 1024, // 100MB default
16
+ readIdleTimeoutMs = 30000, // 30s default
17
+ } = options;
18
+ if (!response.body) {
19
+ throw new ReadResponseError("read_failed", "Response body is null");
11
20
  }
12
- const reader = body.getReader();
13
21
  const chunks = [];
14
- let total = 0;
22
+ let totalBytes = 0;
23
+ let lastReadTime = Date.now();
24
+ const reader = response.body.getReader();
15
25
  try {
16
26
  while (true) {
17
- const { done, value } = await reader.read();
27
+ // Check idle timeout
28
+ const idleTime = Date.now() - lastReadTime;
29
+ if (idleTime > readIdleTimeoutMs) {
30
+ throw new ReadResponseError("timeout", `Read idle timeout exceeded (${readIdleTimeoutMs}ms)`);
31
+ }
32
+ const { done, value } = await Promise.race([
33
+ reader.read(),
34
+ new Promise((_, reject) => {
35
+ setTimeout(() => {
36
+ reject(new ReadResponseError("timeout", "Read timeout"));
37
+ }, readIdleTimeoutMs);
38
+ }),
39
+ ]);
40
+ lastReadTime = Date.now();
18
41
  if (done) {
19
42
  break;
20
43
  }
21
- if (value?.length) {
22
- total += value.length;
23
- if (total > maxBytes) {
24
- try {
25
- await reader.cancel();
26
- }
27
- catch { }
28
- throw onOverflow({ size: total, maxBytes, res });
44
+ if (value) {
45
+ totalBytes += value.length;
46
+ if (totalBytes > maxBytes) {
47
+ throw new ReadResponseError("max_bytes", `Response size exceeds limit (${maxBytes} bytes)`);
29
48
  }
30
49
  chunks.push(value);
31
50
  }
32
51
  }
33
52
  }
34
53
  finally {
35
- try {
36
- reader.releaseLock();
37
- }
38
- catch { }
54
+ reader.releaseLock();
55
+ }
56
+ // Concatenate chunks
57
+ const result = new Uint8Array(totalBytes);
58
+ let offset = 0;
59
+ for (const chunk of chunks) {
60
+ result.set(chunk, offset);
61
+ offset += chunk.length;
39
62
  }
40
- return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), total);
63
+ return Buffer.from(result);
41
64
  }
@@ -205,7 +205,7 @@ export class MediaAttachmentCache {
205
205
  throw new MediaUnderstandingSkipError("empty", `Attachment ${params.attachmentIndex + 1} has no path or URL.`);
206
206
  }
207
207
  try {
208
- const fetchImpl = (input, init) => fetchWithTimeout(resolveRequestUrl(input), init ?? {}, params.timeoutMs, fetch);
208
+ const fetchImpl = ((input, init) => fetchWithTimeout(resolveRequestUrl(input), init ?? {}, params.timeoutMs, fetch));
209
209
  const fetched = await fetchRemoteMedia({ url, fetchImpl, maxBytes: params.maxBytes });
210
210
  entry.buffer = fetched.buffer;
211
211
  entry.bufferMime =
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Audio
3
+ *
4
+ * Re-exports audio-related plugin utilities
5
+ */
6
+ export { transcribeAudio, isVoiceCompatibleAudio, } from "../media/audio.js";
7
+ export { extractAudioTags, } from "../media/audio-tags.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - BlueBubbles
3
+ *
4
+ * Re-exports BlueBubbles-specific plugin utilities
5
+ */
6
+ export { createBlueBubblesChannel, } from "../bluebubbles/channel.js";
7
+ export { BLUEBUBBLES_ACTIONS, BLUEBUBBLES_ACTION_NAMES, BLUEBUBBLES_GROUP_ACTIONS, } from "../bluebubbles/actions.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Browser
3
+ *
4
+ * Re-exports browser-related plugin utilities
5
+ */
6
+ export { createBrowserTool, } from "../browser/browser-tool.js";
7
+ export { resolveBrowserConfig, resolveProfile, } from "../browser/config.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Canvas
3
+ *
4
+ * Re-exports canvas-related plugin utilities
5
+ */
6
+ export { createCanvasTool, } from "../canvas-host/canvas-tool.js";
7
+ export { sendCanvasMessage, } from "../canvas-host/canvas-send.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Cron
3
+ *
4
+ * Re-exports cron-related plugin utilities
5
+ */
6
+ export { createCronTool, } from "../cron/cron-tool.js";
7
+ export { scheduleCronJob, cancelCronJob, listCronJobs, } from "../cron/cron-manager.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Discord Actions
3
+ *
4
+ * Re-exports Discord action utilities
5
+ */
6
+ export { discordSendMessage, discordEditMessage, discordDeleteMessage, discordCreateThread, discordPinMessage, discordUnpinMessage, discordAddReaction, discordRemoveReaction, } from "../discord/actions.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Discord
3
+ *
4
+ * Re-exports Discord-specific plugin utilities
5
+ */
6
+ export { createDiscordChannel, } from "../discord/channel.js";
7
+ export { autoBindSpawnedDiscordSubagent, listThreadBindingsBySessionKey, unbindThreadBindingsBySessionKey, } from "../discord/monitor/thread-bindings.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - Image
3
+ *
4
+ * Re-exports image-related plugin utilities
5
+ */
6
+ export { resizeToJpeg, getImageMetadata, } from "../media/image-ops.js";
7
+ export { createImageTool, createImageGenerateTool, } from "../media/image-tool.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Pool Bot Plugin SDK - iMessage
3
+ *
4
+ * Re-exports iMessage-specific plugin utilities
5
+ */
6
+ export { createIMessageChannel, } from "../imessage/channel.js";